V7 super admin
This commit is contained in:
parent
48f139a135
commit
d9157663a8
4
db/migrations/007_add_second_super_admin.sql
Normal file
4
db/migrations/007_add_second_super_admin.sql
Normal 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'
|
||||
159
public/app.js
159
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 = `
|
||||
<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();
|
||||
@ -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>
|
||||
Loading…
x
Reference in New Issue
Block a user