// ─── 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 `
`;
}
function emptyHTML(msg = 'No records found') {
return ``;
}
// ─── 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');
}