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'
|
||||||
149
public/app.js
149
public/app.js
@ -1,5 +1,68 @@
|
|||||||
const API_URL = '/api/v1/index.php?request=';
|
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 = {
|
const state = {
|
||||||
user: JSON.parse(localStorage.getItem('user')) || null,
|
user: JSON.parse(localStorage.getItem('user')) || null,
|
||||||
token: localStorage.getItem('token') || null,
|
token: localStorage.getItem('token') || null,
|
||||||
@ -16,17 +79,52 @@ const routes = {
|
|||||||
'/super-admin': superAdminPage,
|
'/super-admin': superAdminPage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Initialization ---
|
||||||
async function init() {
|
async function init() {
|
||||||
window.addEventListener('hashchange', router);
|
window.addEventListener('hashchange', router);
|
||||||
|
window.addEventListener('online', syncData);
|
||||||
|
window.addEventListener('offline', () => updateOnlineStatus(false));
|
||||||
|
|
||||||
|
updateOnlineStatus(navigator.onLine);
|
||||||
router();
|
router();
|
||||||
updateNav();
|
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() {
|
function router() {
|
||||||
const hash = window.location.hash || '#/';
|
const hash = window.location.hash || '#/';
|
||||||
const path = hash.substring(1);
|
const path = hash.substring(1);
|
||||||
|
|
||||||
// Auth guard
|
|
||||||
if (!state.token && path !== '/login') {
|
if (!state.token && path !== '/login') {
|
||||||
window.location.hash = '#/login';
|
window.location.hash = '#/login';
|
||||||
return;
|
return;
|
||||||
@ -38,6 +136,8 @@ function router() {
|
|||||||
|
|
||||||
function updateNav() {
|
function updateNav() {
|
||||||
const navLinks = document.getElementById('nav-links');
|
const navLinks = document.getElementById('nav-links');
|
||||||
|
if (!navLinks) return;
|
||||||
|
|
||||||
if (!state.token) {
|
if (!state.token) {
|
||||||
navLinks.innerHTML = `
|
navLinks.innerHTML = `
|
||||||
<li class="nav-item"><a class="nav-link" href="#/login">Login</a></li>
|
<li class="nav-item"><a class="nav-link" href="#/login">Login</a></li>
|
||||||
@ -77,28 +177,61 @@ function logout() {
|
|||||||
updateNav();
|
updateNav();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function apiFetch(endpoint, options = {}) {
|
async function apiFetch(endpoint, options = {}, forceOnline = false) {
|
||||||
const url = API_URL + endpoint;
|
const url = API_URL + endpoint;
|
||||||
|
const method = options.method || 'GET';
|
||||||
|
const body = options.body || null;
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(state.token ? { 'Authorization': `Bearer ${state.token}` } : {})
|
...(state.token ? { 'Authorization': `Bearer ${state.token}` } : {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (method === 'GET' && !forceOnline) {
|
||||||
|
try {
|
||||||
const response = await fetch(url, { ...defaultOptions, ...options });
|
const response = await fetch(url, { ...defaultOptions, ...options });
|
||||||
if (response.status === 401) {
|
if (response.status === 401) { logout(); throw new Error('Unauthorized'); }
|
||||||
logout();
|
const data = await response.json();
|
||||||
throw new Error('Unauthorized');
|
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();
|
const data = await response.json();
|
||||||
if (!response.ok) throw new Error(data.error || 'API Error');
|
if (!response.ok) throw new Error(data.error || 'API Error');
|
||||||
return data;
|
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) {
|
function render(html) {
|
||||||
document.getElementById('content').innerHTML = html;
|
const content = document.getElementById('content');
|
||||||
|
if (content) content.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Page Renderers ---
|
||||||
|
|
||||||
async function homePage() {
|
async function homePage() {
|
||||||
render(`
|
render(`
|
||||||
<div class="p-4 mb-4 bg-white rounded-4 shadow-sm">
|
<div class="p-4 mb-4 bg-white rounded-4 shadow-sm">
|
||||||
@ -177,7 +310,7 @@ function loginPage() {
|
|||||||
const data = await apiFetch('/auth/login', {
|
const data = await apiFetch('/auth/login', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ email, password })
|
body: JSON.stringify({ email, password })
|
||||||
});
|
}, true); // forceOnline for login
|
||||||
|
|
||||||
if (data.token) {
|
if (data.token) {
|
||||||
localStorage.setItem('token', data.token);
|
localStorage.setItem('token', data.token);
|
||||||
@ -268,7 +401,7 @@ async function superAdminPage() {
|
|||||||
html += '</tbody></table></div></div>';
|
html += '</tbody></table></div></div>';
|
||||||
render(html);
|
render(html);
|
||||||
} catch (err) {
|
} 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>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,13 @@
|
|||||||
background-color: var(--bs-primary);
|
background-color: var(--bs-primary);
|
||||||
border-color: var(--bs-primary);
|
border-color: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
#offline-indicator {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-light">
|
<body class="bg-light">
|
||||||
@ -60,7 +67,17 @@
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</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="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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user