registerPage('reports', async (content, params = {}) => { document.getElementById('topbar-title').textContent = 'Reports'; let currentData = null; let currentMode = null; let clients = [], sites = []; // Pre-load clients + sites const [cr, sr] = await Promise.all([ api('clients.php', { action: 'list' }), api('sites.php', { action: 'list' }), ]); clients = cr.clients || []; sites = sr.sites || []; function daysLabel(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 === 0) return `Today`; if (n <= 30) return `${label}`; if (n <= 60) return `${label}`; if (n <= 180) return `${label}`; return `${label}`; } function resultBadge(r) { const v = (r || '').toUpperCase(); if (v.includes('COMP') || v.includes('PASS')) return `${r}`; if (v.includes('NYC') || v.includes('FAIL')) return `${r}`; return r ? `${r}` : '—'; } function recBadge(t) { return t === 'assessment' ? 'Assessment' : 'Test'; } // ── Render functions ─────────────────────────────────────────────────── function renderEmployeeMode(data) { const withRecords = (data.employees || []).filter(emp => emp.records?.length > 0); if (!withRecords.length) return emptyHTML('No employees with assessments or tests found'); return withRecords.map((emp, ei) => `
${emp.client_employees_name} ${emp.surname} ${emp.i_doc_passport || ''} ${emp.clients_name || ''}
${emp.records?.length || 0} records
${renderRecordsTable(emp.records)}
`).join(''); } function renderClientMode(data) { return renderEmployeeMode(data); } function renderDaterangeMode(data) { if (!data.grouped || !Object.keys(data.grouped).length) return emptyHTML('No records found in that date range'); return Object.entries(data.grouped).map(([client, rows], gi) => `
${client} ${rows.length}
${rows.map(r => ``).join('')}
TypeNameSurnameID Assessment / TestVehicleDateResult ExpiryAssessorBookingSite
${recBadge(r.rec_type)} ${r.client_employees_name || '—'} ${r.surname || '—'} ${r.i_doc_passport || '—'} ${r.name} ${r.vehicle_model || '—'} ${(r.date || '').slice(0, 10)} ${resultBadge(r.results)} ${daysLabel(r.days_left)} ${r.assessor || '—'} ${r.booking_number || '—'} ${r.client_sites_name || '—'}
`).join(''); } function renderSiteMode(data) { const rows = data.rows || []; if (!rows.length) return emptyHTML('No assessment records found for this site'); return `
${rows.map(r => ``).join('')}
NameSurnameID/PassportOccupation Assessment / TestVehicleDate ResultScoreExpiry DateExpiry Booking #Assessor
${r.client_employees_name || '—'} ${r.surname || '—'} ${r.i_doc_passport || '—'} ${r.occupation || '—'} ${r.name || '—'} ${r.vehicle_model || '—'} ${(r.date || '').slice(0, 10)} ${resultBadge(r.results)} ${r.current_mark || '—'}/${r.passmark || '—'} ${r.expiry_date || '—'} ${daysLabel(r.days_left)} ${r.booking_number || '—'} ${r.assessor || '—'}
`; } function renderRecordsTable(records) { if (!records?.length) return `
No records
`; return `
${records.map(r => ``).join('')}
TypeAssessment / TestVehicleDate ResultScoreExpiry DateExpiryAssessor
${recBadge(r.rec_type)} ${r.name} ${r.vehicle_model || '—'} ${(r.date || '').slice(0, 10)} ${resultBadge(r.results)} ${r.current_mark || '—'} / ${r.passmark || '—'} ${r.expiry_date || '—'} ${daysLabel(r.days_left)} ${r.assessor || '—'}
`; } window.toggleRptGrp = (i) => { const b = document.getElementById(`rpt-body-${i}`); const a = document.getElementById(`rpt-arrow-${i}`); if (!b) return; const open = b.style.display !== 'none'; b.style.display = open ? 'none' : ''; if (a) a.style.transform = open ? 'rotate(-90deg)' : ''; }; // ── Render the page shell ───────────────────────────────────────────── content.innerHTML = `
${['employee', 'client', 'daterange', 'site'].map(m => ` `).join('')}
`; function buildFilters(mode) { const container = document.getElementById('report-filters'); if (mode === 'employee') { container.innerHTML = `
`; document.getElementById('rf-emp-search').addEventListener('keydown', e => { if (e.key === 'Enter') runReport(); }); } else if (mode === 'client') { container.innerHTML = `
`; } else if (mode === 'daterange') { const today = new Date().toISOString().slice(0, 10); const m1ago = new Date(Date.now() - 30 * 864e5).toISOString().slice(0, 10); container.innerHTML = `
`; } else if (mode === 'site') { container.innerHTML = `
`; } } window.setMode = (mode) => { currentMode = mode; ['employee', 'client', 'daterange', 'site'].forEach(m => { const b = document.getElementById(`mode-btn-${m}`); if (b) { b.className = `btn btn-sm ${m === mode ? 'btn-primary' : 'btn-secondary'}`; b.style.textTransform = 'capitalize'; } }); buildFilters(mode); document.getElementById('report-results').innerHTML = ''; document.getElementById('report-actions').style.display = 'none'; currentData = null; }; window.runReport = async () => { const res = document.getElementById('report-results'); res.innerHTML = loadingHTML(); document.getElementById('report-actions').style.display = 'none'; let r, params = {}; if (currentMode === 'employee') { const q = document.getElementById('rf-emp-search')?.value?.trim(); if (!q) { res.innerHTML = emptyHTML('Enter a search term'); return; } r = await api('reports.php', { mode: 'employee', search: q }); } else if (currentMode === 'client') { const cid = document.getElementById('rf-client')?.value; if (!cid) { res.innerHTML = emptyHTML('Select a client'); return; } r = await api('reports.php', { mode: 'client', clients_id: cid }); } else if (currentMode === 'daterange') { params = { mode: 'daterange', from: document.getElementById('rf-from')?.value || '', to: document.getElementById('rf-to')?.value || '', clients_id: document.getElementById('rf-client2')?.value || '', }; r = await api('reports.php', params); } else if (currentMode === 'site') { const sid = document.getElementById('rf-site')?.value; if (!sid) { res.innerHTML = emptyHTML('Select a site'); return; } r = await api('reports.php', { mode: 'site', client_sites_id: sid }); } if (!r?.success) { res.innerHTML = emptyHTML('Error: ' + (r?.error || 'unknown')); return; } currentData = r; let html = ''; let count = 0; if (currentMode === 'employee' || currentMode === 'client') { html = renderEmployeeMode(r); count = r.employees?.length || 0; } else if (currentMode === 'daterange') { html = renderDaterangeMode(r); count = r.total || 0; } else if (currentMode === 'site') { html = renderSiteMode(r); count = r.total || 0; } res.innerHTML = html; document.getElementById('report-count').textContent = `${count} ${currentMode === 'daterange' ? 'records' : currentMode === 'site' ? 'assessments' : 'employees'}`; document.getElementById('report-actions').style.display = ''; }; window.expandAllRpt = () => { document.querySelectorAll('[id^="rpt-body-"]').forEach(b => { b.style.display = ''; }); }; window.collapseAllRpt = () => { document.querySelectorAll('[id^="rpt-body-"]').forEach(b => { b.style.display = 'none'; }); }; // ── Flatten data for export ─────────────────────────────────────────── function flattenData() { if (!currentData) return { headers: [], rows: [] }; const headers = ['Name', 'Surname', 'ID/Passport', 'Client', 'Assessment/Test', 'Vehicle Model', 'Date', 'Result', 'Score', 'Expiry Date', 'Days Left', 'Assessor']; const rows = []; const push = (emp, rec) => rows.push([ emp.client_employees_name || rec.client_employees_name || '', emp.surname || rec.surname || '', emp.i_doc_passport || rec.i_doc_passport || '', emp.clients_name || rec.clients_name || '', rec.name || '', rec.vehicle_model || '', (rec.date || '').slice(0, 10), rec.results || '', (rec.current_mark || '') + '/' + (rec.passmark || ''), rec.expiry_date || '', rec.days_left ?? '', rec.assessor || rec.assessor_name || '', // end of fields ]); if (currentMode === 'employee' || currentMode === 'client') { (currentData.employees || []).forEach(emp => (emp.records || []).forEach(rec => push(emp, rec))); } else if (currentMode === 'daterange') { Object.values(currentData.grouped || {}).forEach(recs => recs.forEach(rec => push(rec, rec))); } else if (currentMode === 'site') { (currentData.rows || []).forEach(r => rows.push([ r.client_employees_name || '', r.surname || '', r.i_doc_passport || '', r.clients_name || '', r.name || '', r.vehicle_model || '', (r.date || '').slice(0, 10), r.results || '', (r.current_mark || '') + '/' + (r.passmark || ''), r.expiry_date || '', r.days_left ?? '', r.assessor || '' ])); } return { headers, rows }; } // ── Excel ───────────────────────────────────────────────────────────── window.reportExcel = () => { const { headers, rows } = flattenData(); if (!rows.length) { toast('No data to export', 'error'); return; } generateExcel('report_' + currentMode + '_' + new Date().toISOString().slice(0, 10), headers, rows); }; // ── PDF ─────────────────────────────────────────────────────────────── window.reportPDF = () => { const { headers, rows } = flattenData(); if (!rows.length) { toast('No data to export', 'error'); return; } const date = new Date().toLocaleDateString('en-ZA', { day: 'numeric', month: 'short', year: 'numeric' }); const modeLabel = { employee: 'Employee', client: 'Client', daterange: 'Date Range', site: 'Site' }[currentMode] || ''; const headHtml = headers.map(h => `${h}`).join(''); const bodyHtml = rows.map((r, i) => `${r.map(v => `${v ?? ''}`).join('')}`).join(''); const html = `Report
SafeSure
${modeLabel} Report
Date: ${date}
Records: ${rows.length}
${headHtml}${bodyHtml}
SafeSure Competency Assessment SystemConfidential — ${modeLabel} Report ${date}