recreate project
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
<?= $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>
|
||||
<?= $this->include('partials/academic_filter') ?>
|
||||
|
||||
<?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', () => {
|
||||
// --- FixedHeader helpers / assets ---
|
||||
function getFixedHeaderOffset() {
|
||||
let total = 0;
|
||||
const stack = [];
|
||||
const header = document.querySelector('header.navbar.sticky-top, header.navbar.fixed-top');
|
||||
if (header) stack.push(header);
|
||||
const mgmt = document.getElementById('navbarManagement');
|
||||
if (mgmt && (mgmt.classList.contains('sticky-top') || mgmt.classList.contains('fixed-top'))) stack.push(mgmt);
|
||||
document.querySelectorAll('.navbar.sticky-top, .navbar.fixed-top').forEach(el => { if (!stack.includes(el)) stack.push(el); });
|
||||
stack.forEach(el => { const h = el.offsetHeight || el.getBoundingClientRect().height || 0; total += Math.max(0, Math.round(h)); });
|
||||
return total;
|
||||
}
|
||||
|
||||
function loadScript(src, id) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (id && document.getElementById(id)) return resolve();
|
||||
const s = document.createElement('script');
|
||||
if (id) s.id = id;
|
||||
s.src = src;
|
||||
s.onload = resolve;
|
||||
s.onerror = reject;
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
}
|
||||
function loadCss(href, id) {
|
||||
return new Promise((resolve) => {
|
||||
if (id && document.getElementById(id)) return resolve();
|
||||
const l = document.createElement('link');
|
||||
if (id) l.id = id;
|
||||
l.rel = 'stylesheet';
|
||||
l.href = href;
|
||||
l.onload = resolve;
|
||||
document.head.appendChild(l);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureFixedHeaderAssets() {
|
||||
const hasFH = !!(window.jQuery && window.jQuery.fn && window.jQuery.fn.dataTable && window.jQuery.fn.dataTable.FixedHeader);
|
||||
if (hasFH) return Promise.resolve();
|
||||
return Promise.all([
|
||||
loadScript('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader@3.4.0/js/dataTables.fixedHeader.min.js', 'dt-fixedheader'),
|
||||
loadCss('https://cdn.jsdelivr.net/npm/datatables.net-fixedheader-bs5@3.4.0/css/fixedHeader.bootstrap5.min.css', 'dt-fixedheader-css')
|
||||
]).catch(() => {});
|
||||
}
|
||||
|
||||
// Start fetching FixedHeader in parallel
|
||||
ensureFixedHeaderAssets();
|
||||
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 = [];
|
||||
|
||||
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 destroyDataTable = () => {
|
||||
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
|
||||
return;
|
||||
}
|
||||
if (window.jQuery.fn.DataTable.isDataTable(tableSelector)) {
|
||||
window.jQuery(tableSelector).DataTable().clear().destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const initDataTable = () => {
|
||||
if (!window.jQuery || !window.jQuery.fn || !window.jQuery.fn.DataTable) {
|
||||
return;
|
||||
}
|
||||
const opts = {
|
||||
pageLength: 25,
|
||||
responsive: true,
|
||||
// Sort by Scheduled At desc, then Created At desc by default
|
||||
order: [[5, 'desc'], [7, 'desc']],
|
||||
columnDefs: [
|
||||
{
|
||||
targets: [2],
|
||||
orderable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (window.jQuery.fn.dataTable && window.jQuery.fn.dataTable.FixedHeader) {
|
||||
opts.fixedHeader = { header: true, headerOffset: getFixedHeaderOffset() };
|
||||
}
|
||||
|
||||
const dt = window.jQuery(tableSelector).DataTable(opts);
|
||||
|
||||
// Attach FixedHeader if it loads slightly later
|
||||
if (!opts.fixedHeader) {
|
||||
ensureFixedHeaderAssets().then(() => {
|
||||
if (window.jQuery.fn.dataTable && window.jQuery.fn.dataTable.FixedHeader) {
|
||||
try { new window.jQuery.fn.dataTable.FixedHeader(dt, { header: true, headerOffset: getFixedHeaderOffset() }); } catch (_) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
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 = () => {
|
||||
destroyDataTable();
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (!Array.isArray(lastPayload) || lastPayload.length === 0) {
|
||||
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);
|
||||
initDataTable();
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
|
||||
initDataTable();
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
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.');
|
||||
destroyDataTable();
|
||||
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() ?>
|
||||
Reference in New Issue
Block a user