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` : ''}
${rows.map(r => ``).join('')}
TypeNameSurnameID/Passport Assessment / TestDone DateExpiry DateStatus
${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(''); } 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})

${rows.map((r, i) => ``).join('')}
TypeName IDAssessment/Test DoneExpires Status
${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(''); const html = `Expiry Report
SafeSure
Expiry Report
Date: ${date}
Total: ${allRows.length} records
${groupBlocks}
SafeSure Competency Assessment SystemConfidential — Expiry Report ${date}