diff --git a/db/migrations/007_add_second_super_admin.sql b/db/migrations/007_add_second_super_admin.sql new file mode 100644 index 0000000..98b94d9 --- /dev/null +++ b/db/migrations/007_add_second_super_admin.sql @@ -0,0 +1,4 @@ +-- Migration: Add second default super admin +INSERT IGNORE INTO users (email, password, role, school_id) VALUES +('admin@flatlogic.com', '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 'Super Admin', NULL); +-- password is 'password' diff --git a/public/app.js b/public/app.js index be6a38c..82ba72c 100644 --- a/public/app.js +++ b/public/app.js @@ -1,5 +1,68 @@ 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, @@ -16,17 +79,52 @@ const routes = { '/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); - // Auth guard if (!state.token && path !== '/login') { window.location.hash = '#/login'; return; @@ -38,6 +136,8 @@ function router() { function updateNav() { const navLinks = document.getElementById('nav-links'); + if (!navLinks) return; + if (!state.token) { navLinks.innerHTML = `