registerPage('expiry', async (content, params = {}) => {
document.getElementById('topbar-title').textContent = 'Expiry Report';
let clients = [];
let allRows = [];
let grouped = {};
function daysBadge(d) {
const n = parseInt(d);
if (isNaN(n)) return `Unknown`;
const abs = Math.abs(n);
const months = Math.round(abs / 30.44);
const label = abs < 30 ? `${abs}d` : `${months}mo`;
if (n < 0) return `Expired ${label} ago`;
if (n === 0) return `Expires today`;
if (n <= 30) return `${label} left`;
if (n <= 60) return `${label} left`;
if (n <= 180) return `${label} left`;
return `${label} left`;
}
function renderGroups(grp) {
const container = document.getElementById('expiry-table');
if (!container) return;
if (!Object.keys(grp).length) { container.innerHTML = emptyHTML('No records match your filters'); return; }
container.innerHTML = Object.entries(grp).map(([client, rows], ci) => `
${client}
${rows.length} records
${rows.filter(r => parseInt(r.days_left) < 0).length
? `
${rows.filter(r => parseInt(r.days_left) < 0).length} expired` : ''}
${rows.filter(r => { const n = parseInt(r.days_left); return n >= 0 && n <= 30; }).length
? `
${rows.filter(r => { const n = parseInt(r.days_left); return n >= 0 && n <= 30; }).length} critical` : ''}
| Type | Name | Surname | ID/Passport |
Assessment / Test | Done Date | Expiry Date | Status |
${rows.map(r => `
| ${r.rec_type === 'assessment' ? 'Assessment' : 'Test'} |
${r.client_employees_name} |
${r.surname} |
${r.i_doc_passport || '—'} |
${r.name} |
${(r.done_date || '').slice(0, 10)} |
${r.expiry_date || '—'} |
${daysBadge(r.days_left)} |
`).join('')}
`).join('');
}
window.toggleExpGrp = (ci) => {
const body = document.getElementById(`exp-body-${ci}`);
const arrow = document.getElementById(`exp-arrow-${ci}`);
if (!body) return;
const open = body.style.display !== 'none';
body.style.display = open ? 'none' : '';
if (arrow) arrow.style.transform = open ? 'rotate(-90deg)' : '';
};
async function load() {
const clientId = document.getElementById('expiry-client').value || '';
const search = document.getElementById('expiry-search').value || '';
const days = document.getElementById('expiry-days').value || '90';
document.getElementById('expiry-table').innerHTML = loadingHTML();
const r = await api('expiry.php', { action: 'list', clients_id: clientId, search, days_warn: days });
if (!r.success) { document.getElementById('expiry-table').innerHTML = emptyHTML('Error: ' + r.error); return; }
allRows = r.rows;
grouped = r.grouped;
document.getElementById('expiry-count').textContent = r.total + ' records';
renderGroups(grouped);
}
content.innerHTML = `
${loadingHTML()}
`;
// Load clients for filter
const cr = await api('clients.php', { action: 'list' });
if (cr.success) {
const sel = document.getElementById('expiry-client');
cr.clients.forEach(c => sel.innerHTML += ``);
}
let debounce;
document.getElementById('expiry-search').addEventListener('input', () => { clearTimeout(debounce); debounce = setTimeout(load, 350); });
document.getElementById('expiry-client').addEventListener('change', load);
document.getElementById('expiry-days').addEventListener('change', load);
load();
// ── Excel export ─────────────────────────────────────────────────────────
window.expiryExcelDl = () => {
if (!allRows.length) { toast('No data to export', 'error'); return; }
const headers = ['Type', 'Name', 'Surname', 'ID/Passport', 'Client', 'Assessment/Test', 'Done Date', 'Expiry Date', 'Days Left'];
const rows = allRows.map(r => [
r.rec_type, r.client_employees_name, r.surname, r.i_doc_passport || '',
r.clients_name || '', r.name, (r.done_date || '').slice(0, 10),
r.expiry_date || '', r.days_left
]);
const wb = XLSX.utils.book_new();
const ws = XLSX.utils.aoa_to_sheet([headers, ...rows]);
XLSX.utils.book_append_sheet(wb, ws, 'Expiry');
XLSX.writeFile(wb, 'expiry_report_' + new Date().toISOString().slice(0, 10) + '.xlsx');
};
// ── PDF export ────────────────────────────────────────────────────────────
window.expiryPDFDl = () => {
if (!Object.keys(grouped).length) { toast('No data to export', 'error'); return; }
function dayLabel(d) {
const n = parseInt(d);
if (isNaN(n)) return '';
const abs = Math.abs(n);
const months = Math.round(abs / 30.44);
const label = abs < 30 ? `${abs}d` : `${months}mo`;
if (n < 0) return `Expired ${label} ago`;
if (n <= 30) return `${label} left`;
if (n <= 60) return `${label} left`;
return `${label} left`;
}
const date = new Date().toLocaleDateString('en-ZA', { day: 'numeric', month: 'short', year: 'numeric' });
const groupBlocks = Object.entries(grouped).map(([client, rows]) => `
${client} (${rows.length})
| Type | Name |
ID | Assessment/Test |
Done | Expires |
Status |
${rows.map((r, i) => `
| ${r.rec_type === 'assessment' ? 'Assessment' : 'Test'} |
${r.client_employees_name} ${r.surname} |
${r.i_doc_passport || ''} |
${r.name} |
${(r.done_date || '').slice(0, 10)} |
${r.expiry_date || ''} |
${dayLabel(r.days_left)} |
`).join('')}
`).join('');
const html = `Expiry Report
${groupBlocks}