Files
root ba87598d3a
Tests / PHPUnit (push) Failing after 1m13s
fix pages and add distribution system
2026-07-15 23:03:51 -04:00

297 lines
10 KiB
PHP

<?= $this->extend('layout/management_layout') ?>
<?= $this->section('content') ?>
<?php
helper('url');
$endpoint = $notificationsEndpoint ?? site_url('api/notifications/active');
$defaultTarget = isset($defaultTargetGroup) ? (string) $defaultTargetGroup : '';
?>
<div class="container-fluid px-0 mt-4"
data-notifications-endpoint="<?= esc($endpoint) ?>"
data-default-target-group="<?= esc($defaultTarget) ?>">
<h2 class="text-center mt-4 mb-3">Active Notifications</h2>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success"><?= esc(session()->getFlashdata('success')) ?></div>
<?php endif; ?>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= esc(session()->getFlashdata('error')) ?></div>
<?php endif; ?>
<div id="notificationsAlert" class="alert alert-danger d-none" role="alert"></div>
<div class="d-flex align-items-center gap-2 mb-3 flex-wrap">
<label for="targetGroupInput" class="form-label mb-0">Target Group:</label>
<input type="text"
class="form-control w-auto"
id="targetGroupInput"
placeholder="All groups"
value="<?= esc($defaultTarget) ?>">
<button type="button" class="btn btn-outline-primary" id="applyFilterBtn">Apply</button>
<button type="button" class="btn btn-outline-secondary" id="resetFilterBtn">Reset</button>
</div>
<div class="table-responsive">
<table id="activeNotificationsTable" class="table table-striped table-bordered align-middle w-100">
<thead class="table-dark">
<tr>
<th>#</th>
<th>Title</th>
<th>Message</th>
<th>Priority</th>
<th>Target Group</th>
<th>Scheduled At</th>
<th>Expires At</th>
<th>Created At</th>
</tr>
</thead>
<tbody id="notificationsTableBody">
<tr>
<td colspan="8" class="text-center text-muted">Loading notifications…</td>
</tr>
</tbody>
</table>
</div>
</div>
<?= $this->endSection() ?>
<?= $this->section('scripts') ?>
<script>
document.addEventListener('DOMContentLoaded', () => {
const container = document.querySelector('[data-notifications-endpoint]');
if (!container) {
return;
}
const endpoint = container.dataset.notificationsEndpoint;
const defaultTargetGroup = container.dataset.defaultTargetGroup || '';
const alertBox = document.getElementById('notificationsAlert');
const tableBody = document.getElementById('notificationsTableBody');
const tableSelector = '#activeNotificationsTable';
const targetInput = document.getElementById('targetGroupInput');
const applyBtn = document.getElementById('applyFilterBtn');
const resetBtn = document.getElementById('resetFilterBtn');
let lastPayload = [];
let dataTable = null;
const showError = (message) => {
if (!alertBox) {
return;
}
alertBox.textContent = message;
alertBox.classList.remove('d-none');
};
const clearError = () => {
if (!alertBox) {
return;
}
alertBox.classList.add('d-none');
alertBox.textContent = '';
};
const initDataTable = () => {
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
return null;
}
if (dataTable) {
return dataTable;
}
dataTable = window.jQuery(tableSelector).DataTable({
pageLength: 25,
order: [[5, 'desc'], [7, 'desc']],
columnDefs: [
{
targets: [2],
orderable: false,
},
],
});
return dataTable;
};
const formatDateTime = (value) => {
if (!value) {
return '—';
}
const date = new Date(value.replace(' ', 'T'));
if (Number.isNaN(date.valueOf())) {
return value;
}
const pad = (num) => String(num).padStart(2, '0');
const datePart = `${pad(date.getMonth() + 1)}-${pad(date.getDate())}-${date.getFullYear()}`;
const hasTime = /[T ]\d{2}:\d{2}/.test(String(value));
if (!hasTime) {
return datePart;
}
const timePart = `${pad(date.getHours())}:${pad(date.getMinutes())}`;
return `${datePart} ${timePart}`;
};
const getOrderValue = (value) => {
if (!value) return 0;
const d = new Date((value + '').replace(' ', 'T'));
const t = d.valueOf();
return Number.isNaN(t) ? 0 : t;
};
const renderTable = () => {
if (!dataTable && window.jQuery && window.jQuery.fn && window.jQuery.fn.DataTable) {
tableBody.innerHTML = '';
}
const dt = initDataTable();
if (dt) {
dt.clear();
} else {
tableBody.innerHTML = '';
}
if (!Array.isArray(lastPayload) || lastPayload.length === 0) {
if (dt) {
dt.draw();
} else {
const row = document.createElement('tr');
const cell = document.createElement('td');
cell.colSpan = 8;
cell.classList.add('text-center', 'text-muted');
cell.textContent = 'No active notifications found.';
row.appendChild(cell);
tableBody.appendChild(row);
}
return;
}
const rows = [];
lastPayload.forEach((entry, index) => {
const row = document.createElement('tr');
if (entry.isExpired) {
row.classList.add('table-danger');
}
const cells = [];
// #
cells.push({ text: String(index + 1) });
// Title
cells.push({ text: entry.title ?? '—' });
// Message
cells.push({ text: entry.message ?? '—' });
// Priority
cells.push({ text: entry.priority ?? '—' });
// Target Group
cells.push({ text: entry.target_group ?? '—' });
// Scheduled At (with order value)
cells.push({
text: formatDateTime(entry.scheduled_at),
order: getOrderValue(entry.scheduled_at)
});
// Expires At (with order value)
cells.push({
text: entry.expires_at ? formatDateTime(entry.expires_at) : 'No expiry',
order: getOrderValue(entry.expires_at)
});
// Created At (with order value)
cells.push({
text: formatDateTime(entry.created_at),
order: getOrderValue(entry.created_at)
});
cells.forEach((cellData) => {
const cell = document.createElement('td');
cell.textContent = cellData.text;
if (typeof cellData.order !== 'undefined') {
cell.setAttribute('data-order', String(cellData.order));
}
row.appendChild(cell);
});
if (!dt) {
tableBody.appendChild(row);
}
rows.push(row);
});
if (dt) {
dt.rows.add(rows).draw();
}
};
const buildUrl = (target) => {
try {
const url = new URL(endpoint, window.location.origin);
if (target) {
url.searchParams.set('target_group', target);
}
return url.toString();
} catch (error) {
const separator = endpoint.includes('?') ? '&' : '?';
return target
? `${endpoint}${separator}target_group=${encodeURIComponent(target)}`
: endpoint;
}
};
const fetchNotifications = (targetGroup) => {
clearError();
const url = buildUrl(targetGroup);
if (dataTable) {
dataTable.clear().draw();
} else {
tableBody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-muted">Loading notifications…</td>
</tr>
`;
}
return fetch(url, {
headers: {
'Accept': 'application/json',
},
credentials: 'same-origin',
})
.then((response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
})
.then((payload) => {
lastPayload = Array.isArray(payload?.notifications) ? payload.notifications : [];
renderTable();
})
.catch((error) => {
showError(error.message || 'Unable to load notifications.');
const dt = initDataTable();
if (dt) {
dt.clear().draw();
} else {
tableBody.innerHTML = `
<tr>
<td colspan="8" class="text-center text-danger">Failed to load notifications.</td>
</tr>
`;
}
});
};
applyBtn.addEventListener('click', () => {
const value = targetInput.value.trim();
fetchNotifications(value);
});
resetBtn.addEventListener('click', () => {
targetInput.value = '';
fetchNotifications('');
});
fetchNotifications(defaultTargetGroup);
});
</script>
<?= $this->endSection() ?>