V7 super admin

This commit is contained in:
Flatlogic Bot 2026-01-31 14:46:12 +00:00
parent 48f139a135
commit d9157663a8
3 changed files with 168 additions and 14 deletions

View File

@ -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'

View File

@ -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 = `
<li class="nav-item"><a class="nav-link" href="#/login">Login</a></li>
@ -77,28 +177,61 @@ function logout() {
updateNav();
}
async function apiFetch(endpoint, options = {}) {
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}` } : {})
}
};
const response = await fetch(url, { ...defaultOptions, ...options });
if (response.status === 401) {
logout();
throw new Error('Unauthorized');
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' };
}
}
const data = await response.json();
if (!response.ok) throw new Error(data.error || 'API Error');
return data;
}
function render(html) {
document.getElementById('content').innerHTML = html;
const content = document.getElementById('content');
if (content) content.innerHTML = html;
}
// --- Page Renderers ---
async function homePage() {
render(`
<div class="p-4 mb-4 bg-white rounded-4 shadow-sm">
@ -177,7 +310,7 @@ function loginPage() {
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);
@ -268,7 +401,7 @@ async function superAdminPage() {
html += '</tbody></table></div></div>';
render(html);
} catch (err) {
render('<div class="alert alert-danger">Access Denied: Super Admin only</div>');
render('<div class="alert alert-danger">Access Denied: Super Admin only (or offline)</div>');
}
}
@ -455,4 +588,4 @@ async function leaderboardPage() {
}
}
init();
init();

View File

@ -29,6 +29,13 @@
background-color: var(--bs-primary);
border-color: var(--bs-primary);
}
#offline-indicator {
display: none;
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
}
</style>
</head>
<body class="bg-light">
@ -60,7 +67,17 @@
</main>
</div>
<div id="offline-indicator" class="alert alert-warning shadow-lg rounded-4">
<i class="bi bi-wifi-off me-2"></i>
<strong>Offline Mode:</strong> Data is being saved locally and will sync when online.
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="app.js?v=1"></script>
<script src="app.js?v=20260130"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}
</script>
</body>
</html>