○ Hors-ligne
TVA
finale * * Principe : * - localStorage reste la source de vérité locale (app fonctionne hors-ligne) * - À chaque saveAll(), les données sont aussi envoyées au serveur * - Au chargement, on récupère les données du serveur (plus récentes) * - Si pas de réseau → localStorage utilisé en fallback */ // ── Configuration ───────────────────────────────────────────────────────── const API_URL = 'https://besnierconstructions.fr/suivi/api.php'; // ← À changer let _syncEnabled = false; // Activé seulement si connecté let _syncUser = null; let _syncPending = false; // Évite les doubles envois // ── Helpers API ─────────────────────────────────────────────────────────── async function apiCall(action, body = {}) { try { const res = await fetch(`${API_URL}?action=${action}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(body) }); return await res.json(); } catch (e) { console.warn('[Sync] Hors-ligne ou erreur réseau:', e.message); return { ok: false, error: 'network' }; } } // ── Auth ────────────────────────────────────────────────────────────────── async function syncCheckSession() { const r = await apiCall('check'); if (r.ok) { _syncEnabled = true; _syncUser = r.username; console.log('[Sync] Connecté en tant que', r.username); await syncPullFromServer(); showSyncStatus('connected'); } else { showLoginScreen(); } } async function syncLogin(username, password) { const r = await apiCall('login', { username, password }); if (r.ok) { _syncEnabled = true; _syncUser = r.username; await syncPullFromServer(); hideLoginScreen(); showSyncStatus('connected'); } else { showLoginError(r.error || 'Identifiants incorrects'); } return r; } async function syncLogout() { await apiCall('logout'); _syncEnabled = false; _syncUser = null; showLoginScreen(); } // ── Sync vers serveur (push) ────────────────────────────────────────────── async function syncPushToServer() { if (!_syncEnabled || _syncPending) return; _syncPending = true; try { // Collecter toutes les clés sc_* const items = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key && key.startsWith('sc_')) { items.push({ key, value: localStorage.getItem(key) }); } } const r = await apiCall('bulk_set', { items }); if (r.ok) { showSyncStatus('synced'); console.log(`[Sync] ${r.synced} clés envoyées`); } else { showSyncStatus('error'); } } finally { _syncPending = false; } } // ── Sync depuis serveur (pull) ──────────────────────────────────────────── async function syncPullFromServer() { const r = await apiCall('bulk_get'); if (!r.ok || !r.items) return; let updated = 0; r.items.forEach(({ key, value }) => { const local = localStorage.getItem(key); if (local !== value) { localStorage.setItem(key, value); updated++; } }); if (updated > 0) { console.log(`[Sync] ${updated} clés mises à jour depuis le serveur`); // Recharger l'état local const pid = ls('sc_current_project'); if (pid) { currentProjectId = pid; loadProjectData(pid); renderCurrentPage(); } } showSyncStatus('synced'); } // ── Surcharger saveAll pour auto-sync ──────────────────────────────────── const _originalSaveAll = saveAll; window.saveAll = function() { _originalSaveAll(); if (_syncEnabled) { // Debounce : attend 800ms avant d'envoyer (évite les appels en rafale) clearTimeout(window._syncTimer); window._syncTimer = setTimeout(syncPushToServer, 800); showSyncStatus('pending'); } }; // ── Surcharger saveProjects ─────────────────────────────────────────────── const _originalSaveProjects = saveProjects; window.saveProjects = function(l) { _originalSaveProjects(l); if (_syncEnabled) { clearTimeout(window._syncTimer); window._syncTimer = setTimeout(syncPushToServer, 800); showSyncStatus('pending'); } }; // Aussi intercepter CA et objectif const _origSaveCAManual = saveCAManual; window.saveCAManual = function(l) { _origSaveCAManual(l); if (_syncEnabled) syncPushToServer(); }; const _origSaveCAObj = saveCAObjectif; window.saveCAObjectif = function(o) { _origSaveCAObj(o); if (_syncEnabled) syncPushToServer(); }; // ── UI : indicateur de sync dans le topbar ──────────────────────────────── function showSyncStatus(status) { const el = document.getElementById('sync-indicator'); if (!el) return; const configs = { connected: { icon: '☁️', text: _syncUser, color: '#2E7D5E' }, synced: { icon: '✓', text: 'Synchronisé', color: '#2E7D5E' }, pending: { icon: '↑', text: 'Sync…', color: '#B8860B' }, error: { icon: '⚠', text: 'Erreur sync', color: '#C62828' }, offline: { icon: '○', text: 'Hors-ligne', color: 'var(--muted)' }, }; const c = configs[status] || configs.offline; el.innerHTML = `${c.icon} ${c.text}`; } // ── Écran de connexion ──────────────────────────────────────────────────── function showLoginScreen() { let overlay = document.getElementById('login-overlay'); if (!overlay) { overlay = document.createElement('div'); overlay.id = 'login-overlay'; overlay.style.cssText = ` position:fixed;inset:0;background:rgba(15,23,42,.95); display:flex;align-items:center;justify-content:center;z-index:9999; `; overlay.innerHTML = `
MOe Suivi
Connectez-vous pour accéder à vos projets
Données chiffrées · Accès sécurisé HTTPS
`; document.body.appendChild(overlay); // Entrée clavier overlay.querySelectorAll('input').forEach(inp => { inp.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); }); }); } overlay.style.display = 'flex'; setTimeout(() => document.getElementById('login-user')?.focus(), 100); } function hideLoginScreen() { const el = document.getElementById('login-overlay'); if (el) el.style.display = 'none'; const sub = document.getElementById('sidebar-username'); if (sub && _syncUser) sub.textContent = '👤 ' + _syncUser; } function showLoginError(msg) { const el = document.getElementById('login-error'); if (el) { el.textContent = msg; el.style.display = 'block'; } const btn = document.getElementById('login-btn'); if (btn) { btn.textContent = 'Se connecter →'; btn.disabled = false; } } async function doLogin() { const user = document.getElementById('login-user')?.value?.trim(); const pass = document.getElementById('login-pass')?.value; const btn = document.getElementById('login-btn'); const errEl = document.getElementById('login-error'); if (errEl) errEl.style.display = 'none'; if (!user || !pass) { showLoginError('Identifiant et mot de passe requis'); return; } if (btn) { btn.textContent = 'Connexion…'; btn.disabled = true; } await syncLogin(user, pass); } // ── Sync manuelle (bouton) ──────────────────────────────────────────────── async function manualSync() { if (!_syncEnabled) { showLoginScreen(); return; } showSyncStatus('pending'); await syncPushToServer(); await syncPullFromServer(); } // ── Init au chargement ──────────────────────────────────────────────────── function renderCurrentPage() { // Re-render la page active après un pull serveur const pages = ['dashboard','crm','ca','contrat-missions','contrat-doc','ao-suivi','chantier-taches','chantier-budget']; const active = pages.find(p => { const el = document.getElementById('page-'+p); return el && el.style.display !== 'none' && el.offsetParent !== null; }); if (active) showPage(active); } // Démarrer la vérification de session après chargement complet window.addEventListener('load', () => { syncCheckSession(); });