const API_URL = '/api/v1/index.php?request='; // --- IndexedDB Configuration --- const dbPromise = new Promise((resolve, reject) => { const request = indexedDB.open('SOMS_DB', 2); request.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains('data')) db.createObjectStore('data'); if (!db.objectStoreNames.contains('sync_queue')) db.createObjectStore('sync_queue', { keyPath: 'id', autoIncrement: true }); }; request.onsuccess = (e) => resolve(e.target.result); request.onerror = (e) => reject(e.target.error); }); async function dbGet(store, key) { const db = await dbPromise; return new Promise((resolve, reject) => { const trans = db.transaction(store, 'readonly'); const req = trans.objectStore(store).get(key); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function dbSet(store, key, value) { const db = await dbPromise; return new Promise((resolve, reject) => { const trans = db.transaction(store, 'readwrite'); const req = trans.objectStore(store).put(value, key); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); } async function dbAdd(store, value) { const db = await dbPromise; return new Promise((resolve, reject) => { const trans = db.transaction(store, 'readwrite'); const req = trans.objectStore(store).add(value); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); } async function dbGetAll(store) { const db = await dbPromise; return new Promise((resolve, reject) => { const trans = db.transaction(store, 'readonly'); const req = trans.objectStore(store).getAll(); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function dbDelete(store, key) { const db = await dbPromise; return new Promise((resolve, reject) => { const trans = db.transaction(store, 'readwrite'); const req = trans.objectStore(store).delete(key); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); } // --- App State --- const state = { user: JSON.parse(localStorage.getItem('user')) || null, token: localStorage.getItem('token') || null, }; const routes = { '/': homePage, '/login': loginPage, '/learners': learnersPage, '/assessments': assessmentsPage, '/events': eventsPage, '/collaboration': collaborationPage, '/leaderboard': leaderboardPage, '/super-admin': superAdminPage, }; // --- Initialization --- async function init() { window.addEventListener('hashchange', router); window.addEventListener('online', syncData); window.addEventListener('offline', () => updateOnlineStatus(false)); updateOnlineStatus(navigator.onLine); router(); updateNav(); if (navigator.onLine) { syncData(); } } function updateOnlineStatus(isOnline) { const indicator = document.getElementById('offline-indicator'); if (indicator) { indicator.style.display = isOnline ? 'none' : 'block'; } } async function syncData() { updateOnlineStatus(true); const queue = await dbGetAll('sync_queue'); if (queue.length === 0) return; console.log('Syncing data...', queue.length, 'items'); for (const item of queue) { try { await apiFetch(item.endpoint, { method: item.method, body: item.body }, true); // forceOnline = true await dbDelete('sync_queue', item.id); } catch (err) { console.error('Failed to sync item:', item, err); break; // Stop syncing if network fails again } } } function router() { const hash = window.location.hash || '#/'; const path = hash.substring(1); if (!state.token && path !== '/login') { window.location.hash = '#/login'; return; } const page = routes[path] || routes['/']; page(); } function updateNav() { const navLinks = document.getElementById('nav-links'); if (!navLinks) return; if (!state.token) { navLinks.innerHTML = ` `; return; } let html = ``; if (state.user.role === 'Super Admin') { html += ``; } html += ` `; navLinks.innerHTML = html; document.getElementById('logout-btn').addEventListener('click', (e) => { e.preventDefault(); logout(); }); } function logout() { localStorage.removeItem('token'); localStorage.removeItem('user'); state.token = null; state.user = null; window.location.hash = '#/login'; updateNav(); } async function apiFetch(endpoint, options = {}, forceOnline = false) { const url = API_URL + endpoint; const method = options.method || 'GET'; const body = options.body || null; const defaultOptions = { headers: { 'Content-Type': 'application/json', ...(state.token ? { 'Authorization': `Bearer ${state.token}` } : {}) } }; if (method === 'GET' && !forceOnline) { try { const response = await fetch(url, { ...defaultOptions, ...options }); if (response.status === 401) { logout(); throw new Error('Unauthorized'); } const data = await response.json(); if (!response.ok) throw new Error(data.error || 'API Error'); // Save to offline storage await dbSet('data', endpoint, data); return data; } catch (err) { console.warn('Network failed, trying offline storage for:', endpoint); const cached = await dbGet('data', endpoint); if (cached) return cached; throw err; } } else { // POST/PUT/DELETE try { const response = await fetch(url, { ...defaultOptions, ...options }); if (response.status === 401) { logout(); throw new Error('Unauthorized'); } const data = await response.json(); if (!response.ok) throw new Error(data.error || 'API Error'); return data; } catch (err) { if (forceOnline) throw err; console.warn('Network failed, queueing request for:', endpoint); await dbAdd('sync_queue', { endpoint, method, body, timestamp: Date.now() }); // Return a "fake" success to keep the UI responsive return { success: true, offline: true, message: 'Data queued for sync' }; } } } function render(html) { const content = document.getElementById('content'); if (content) content.innerHTML = html; } // --- Page Renderers --- async function homePage() { render(`

Welcome back, ${state.user.email}

You are logged in as ${state.user.role}.


Learners
Manage
Assessments
Open Hub
Events
Calendar
Leaderboard
View Rankings
`); } function loginPage() { render(`

SOMS Platform

Demo Credentials
Super Admin: superadmin@system.com / password
Admin: admin@sowetohigh.edu.za / password
`); document.getElementById('login-form').addEventListener('submit', async (e) => { e.preventDefault(); const email = document.getElementById('email').value; const password = document.getElementById('password').value; try { const data = await apiFetch('/auth/login', { method: 'POST', body: JSON.stringify({ email, password }) }, true); // forceOnline for login if (data.token) { localStorage.setItem('token', data.token); localStorage.setItem('user', JSON.stringify(data.user)); state.token = data.token; state.user = data.user; updateNav(); window.location.hash = '#/'; } } catch (err) { alert('Login failed: ' + err.message); } }); } async function superAdminPage() { render('
'); try { const stats = await apiFetch('/schools/stats'); const schools = await apiFetch('/schools'); let html = `

Super Admin Console

Global platform management

Total Schools
${stats.total_schools}
Total Learners
${stats.total_learners}
System Uptime
${stats.uptime}
Data Storage
${stats.storage}
Participating Schools
`; schools.forEach(s => { html += ` `; }); html += '
Name Province District Actions
${s.name} ${s.province} ${s.district}
'; render(html); } catch (err) { render('
Access Denied: Super Admin only (or offline)
'); } } async function learnersPage() { render('
'); try { const learners = await apiFetch('/learners'); let html = `

Learners

`; learners.forEach(l => { html += ` `; }); html += '
Full Name Grade Student ID Actions
${l.full_name} ${l.grade} ${l.student_id}
'; render(html); } catch (err) { render('
Failed to load learners
'); } } async function assessmentsPage() { render('
'); try { const assessments = await apiFetch('/assessments'); let html = `

Assessments

`; assessments.forEach(a => { html += `
${a.title || a.name}
${a.subject || a.grade} — ${a.type}
`; }); html += '
'; render(html); } catch (err) { render('
Failed to load assessments
'); } } async function eventsPage() { render('
'); try { const events = await apiFetch('/events'); let html = `

School Calendar

`; if (events.length === 0) html += '

No upcoming events.

'; events.forEach(e => { html += `
${new Date(e.start_datetime).getDate()}
${new Date(e.start_datetime).toLocaleString('default', { month: 'short' })}
${e.title}

${new Date(e.start_datetime).toLocaleTimeString()} — ${e.location || 'No location'}

`; }); html += '
'; render(html); } catch (err) { render('
Failed to load events
'); } } async function collaborationPage() { render('
'); try { const resources = await apiFetch('/collaboration/resources'); let html = `

Collaboration Hub

`; resources.forEach(r => { html += `
${r.title}
${r.is_public == 1 ? 'Public' : 'School Only'}

${r.description}

${r.teacher_email}
`; }); html += '
'; render(html); } catch (err) { render('
Failed to load resources
'); } } async function leaderboardPage() { render('
'); try { const rankings = await apiFetch('/leaderboard'); let html = `

Student Rankings

`; rankings.forEach((r, index) => { const color = index === 0 ? 'text-warning' : (index === 1 ? 'text-secondary' : (index === 2 ? 'text-brown' : '')); html += ` `; }); html += '
Rank Learner Performance Average
#${index + 1}
${r.full_name}
${parseFloat(r.average_percent).toFixed(1)}%
'; render(html); } catch (err) { render('
Failed to load leaderboard
'); } } init();