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 = `
Login
`;
return;
}
let html = `Dashboard`;
if (state.user.role === 'Super Admin') {
html += `Super Admin`;
}
html += `
Learners
Assessments
Events
Hub
Ranking
Logout
`;
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}.
`);
}
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}
| Name |
Province |
District |
Actions |
`;
schools.forEach(s => {
html += `
| ${s.name} |
${s.province} |
${s.district} |
|
`;
});
html += '
';
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
| Full Name |
Grade |
Student ID |
Actions |
`;
learners.forEach(l => {
html += `
| ${l.full_name} |
${l.grade} |
${l.student_id} |
|
`;
});
html += '
';
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
| Rank |
Learner |
Performance |
Average |
`;
rankings.forEach((r, index) => {
const color = index === 0 ? 'text-warning' : (index === 1 ? 'text-secondary' : (index === 2 ? 'text-brown' : ''));
html += `
| #${index + 1} |
${r.full_name} |
|
${parseFloat(r.average_percent).toFixed(1)}% |
`;
});
html += '
';
render(html);
} catch (err) {
render('Failed to load leaderboard
');
}
}
init();