// ─── API Base ──────────────────────────────────────────────────────────────── const BASE = 'api/'; function getToken() { return sessionStorage.getItem('ss_token') || ''; } function getUser() { try { return JSON.parse(sessionStorage.getItem('ss_user') || 'null'); } catch { return null; } } async function api(endpoint, params = {}, method = 'GET') { const token = getToken(); let r; if (method === 'GET') { const qs = new URLSearchParams({ ...params, token }); r = await fetch(`${BASE}${endpoint}?${qs}`); } else { const body = new FormData(); for (const [k, v] of Object.entries(params)) body.append(k, v); body.append('token', token); r = await fetch(`${BASE}${endpoint}`, { method: 'POST', body }); } const text = await r.text(); try { return JSON.parse(text); } catch (e) { console.error(`[api] ${endpoint} returned non-JSON (HTTP ${r.status}):`); console.error(text); return { success: false, error: `Server returned non-JSON. See console for raw output.` }; } } // ─── Toast ─────────────────────────────────────────────────────────────────── function toast(msg, type = 'default') { const c = document.getElementById('toast-container'); const t = document.createElement('div'); t.className = `toast ${type}`; const icon = type === 'success' ? '✓' : type === 'error' ? '✕' : '●'; t.innerHTML = `${icon}${msg}`; c.appendChild(t); setTimeout(() => { t.style.opacity = '0'; t.style.transform = 'translateX(20px)'; t.style.transition = '.2s'; setTimeout(() => t.remove(), 200); }, 3200); } // ─── Modal ─────────────────────────────────────────────────────────────────── function openModal(id) { document.getElementById(id).classList.add('open'); } function closeModal(id) { document.getElementById(id).classList.remove('open'); } function closeAllModals() { document.querySelectorAll('.modal-bg.open').forEach(m => m.classList.remove('open')); } document.addEventListener('keydown', e => { if (e.key === 'Escape') closeAllModals(); }); document.addEventListener('click', e => { if (e.target.classList.contains('modal-bg')) closeAllModals(); }); // ─── Badge helpers ─────────────────────────────────────────────────────────── const STATUS_CLASS = { 'APPROVED': 'badge-success', 'COMPLETE': 'badge-success', 'DONE': 'badge-success', 'DRAFT': 'badge-gray', 'REJECTED': 'badge-danger', 'NOT APPROVED': 'badge-warning', 'PENDING': 'badge-info', }; const TYPE_CLASS = { 'ADMIN': 'badge-orange', 'ASSESSOR': 'badge-info', 'CLIENT': 'badge-gray', 'CLIENT ADMIN': 'badge-warning' }; function statusBadge(s) { return `${s}`; } function typeBadge(t) { return `${t}`; } // ─── Loading ───────────────────────────────────────────────────────────────── function loadingHTML() { return `
Loading…
`; } function emptyHTML(msg = 'No records found') { return `

${msg}

`; } // ─── Router ────────────────────────────────────────────────────────────────── const PAGES = {}; function registerPage(name, fn) { PAGES[name] = fn; } function navigate(page, params = {}) { const content = document.getElementById('content'); document.querySelectorAll('.nav-item').forEach(n => n.classList.toggle('active', n.dataset.page === page)); content.innerHTML = loadingHTML(); PAGES[page]?.(content, params); window._currentPage = page; } // ─── Auth guard ────────────────────────────────────────────────────────────── function showLogin() { document.getElementById('app').style.display = 'none'; document.getElementById('login-page').style.display = 'flex'; } function showApp(user) { document.getElementById('login-page').style.display = 'none'; document.getElementById('app').style.display = 'flex'; const initials = (user.name || user.username || '?').split(' ').map(w => w[0]).slice(0, 2).join('').toUpperCase(); document.querySelector('.sidebar-user .avatar').textContent = initials; document.querySelector('.sidebar-user .user-name').textContent = user.name || user.username; document.querySelector('.sidebar-user .user-role').textContent = user.user_type_name || ''; } // ─── Login ─────────────────────────────────────────────────────────────────── async function doLogin(username, password) { const body = new FormData(); body.append('username', username); body.append('password', password); const r = await fetch(`${BASE}login.php`, { method: 'POST', body }); const data = await r.json(); if (data.success) { sessionStorage.setItem('ss_token', data.token); sessionStorage.setItem('ss_user', JSON.stringify(data.user)); showApp(data.user); navigate('dashboard'); } else { const err = document.getElementById('login-error'); err.textContent = data.error || 'Login failed'; err.style.display = 'block'; } } async function doLogout() { await api('logout.php', {}, 'POST'); sessionStorage.clear(); showLogin(); } // ─── Init ──────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { document.getElementById('login-form').addEventListener('submit', e => { e.preventDefault(); const u = e.target.elements['username'].value; const p = e.target.elements['password'].value; doLogin(u, p); }); document.querySelectorAll('.nav-item[data-page]').forEach(item => { item.addEventListener('click', () => navigate(item.dataset.page)); }); document.getElementById('logout-btn').addEventListener('click', doLogout); const user = getUser(); if (user && getToken()) { showApp(user); navigate('dashboard'); } else { showLogin(); } }); // ── SheetJS Excel export (loaded from CDN in index.php) ─────────────────────── function generateExcel(filename, headers, rows) { if (typeof XLSX === 'undefined') { toast('Excel library not loaded yet, try again', 'error'); return; } const wsData = [headers, ...rows.map(r => r.map(v => v ?? ''))]; const ws = XLSX.utils.aoa_to_sheet(wsData); // Auto column widths const colWidths = headers.map((h, ci) => { const maxLen = Math.max(h.length, ...rows.map(r => String(r[ci] ?? '').length)); return { wch: Math.min(maxLen + 2, 40) }; }); ws['!cols'] = colWidths; // Style header row bold headers.forEach((_, ci) => { const cell = ws[XLSX.utils.encode_cell({ r: 0, c: ci })]; if (cell) { cell.s = { font: { bold: true } }; } }); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, 'Report'); XLSX.writeFile(wb, filename + '.xlsx'); }