ERP Caissier 2026
Logo
EURL EAU MINERAL SAIDA
CAISSE EMS SAIDA
Caissier: NOUICER HOUARI
Edition: FINAL
Rôle:
Session: Active
Jour:
Réseau: Online

Paramètres du système

Données du jour + Fond + RIB + infos société + PIN par rôle (optionnel)
Gestion de logo
Chargez votre logo et ajustez le rendu: taille, position et clarté (barre haute + impression).
Logo par défaut actif.
Aperçu logo
Aperçu bordereau (live)
Cette feuille montre le rendu réel du logo sur le bordereau imprimé.
Gestion interface PRO
Personnalisez tailles et lisibilité: boutons, champs, onglets et navigation rapide.
Aperçu instantané. Vous pouvez sauvegarder ici sans toucher les autres paramètres.
Les données sont enregistrées dans le navigateur. Utilisez Export pour garder une copie.

Export / Import

Sauvegarde / Restauration (JSON) et export CSV
Auto-backup: aucune sauvegarde locale.
Undo: aucune action à annuler.
Le JSON contient : paramètres + clients + opérations + bordereaux BEA/CPA + jours clôturés.

Accès / Audit

Rôle actif, droits, et journal des actions sensibles
Rôle actif:
Filtré: 0
Admin: 0
Caissier: 0
Audit: 0
Accès refusés: 0
PIN invalides: 0
Alertes: 0
Health: 100/100
Security Health Score
100
Niveau: Excellent
Analyse de sécurité prête.
    HorodatageRôleActionDétail
    Page 1/1

    Gestion des clients

    Liste des clients (ajouter/modifier/supprimer) — le nom se remplit automatiquement dans les opérations
    Modifier Supprimer
    Page 1/1

    Encaissements

    Saisie des encaissements et liaison EMP → BEA et LCA → CPA
    Produit: EMP ➜ BEA | LCA ➜ CPA
    Calculatrice de cash (Fond de caisse espèces) Total: 0 DZD
    Quand vous saisissez les coupures, le champ Fond de caisse se remplit automatiquement.
    Calculatrice pour le rendu de monnaie.
    Calculatrice de cash (Rendu) Total: 0 DZD
    Quand vous saisissez les coupures, le champ Rendu se remplit automatiquement.
    Calculatrice de cash (Encaissement espèces) Total: 0 DZD
    Quand vous saisissez les coupures, le champ Montant se remplit automatiquement.
    Tri multiple: maintenez Shift en cliquant sur les colonnes (max 3).
    # Modifier Supprimer
    Page 1/1
    Supprime uniquement les opérations de la date courante.

    Décaissements (caisse unique)

    Sortie cash/chèque/virement depuis la caisse (sans lien avec les banques)
    Sortie d’argent de la caisse (frais / paiement / …). Sans lien avec BEA ou CPA.
    Calculatrice de cash (Décaissement espèces) Total: 0 DZD
    Quand vous saisissez les coupures, le champ Montant se remplit automatiquement.
    Tri multiple: maintenez Shift en cliquant sur les colonnes (max 3).
    # Modifier Supprimer
    Page 1/1
    Supprime uniquement les décaissements de la date courante.

    Dashboard Exécutif

    Pilotage du jour: volume, cashflow, top clients et alertes opérationnelles
    Volume du jour
    0
    0 clients actifs
    Encaissements totaux
    0
    Cash / Carte / Chèque
    Net cash (avant dépôt)
    0
    Entrées espèces - sorties espèces
    Risque opérationnel
    0
    Aucune alerte
    Ticket moyen: 0 DZD
    Couverture dépôt cash: 0%
    Écart comptage:

    Top Clients (Jour)

    #ClientNb OpsTotal

    Anomalies opérationnelles

    Résumé du jour

    Résumé rapide des calculs + montants de versement par banque
    Total Espèces:
    Total Carte:
    Total Chèque:
    Total Général:
    Fond:
    Décaissement Espèces:
    Décaissement Total:
    Versement BEA (EMP):
    Versement CPA (LCA):
    Caisse avant versement :
    Total des versements (BEA+CPA) :
    Solde de caisse attendu (après versement) :
    Écart:
    Écart = Comptage − Solde attendu. Si le jour est clôturé, il est conseillé d'enregistrer le comptage avant la clôture.
    Attention: le fond de caisse est déduit des espèces, puis de BEA puis CPA (caisse unique).

    Bordereau Versement (2 banques)

    Préparer le bordereau et l’imprimer en PDF pour chaque banque (coupures + opérations)
    BEA (EMP)
    CPA (LCA)
    Jour :
    N° Bordereau:
    Montant à verser: DZD
    Saisissez uniquement les quantités de cette banque.
    TypeDénominationQuantitéSous-total
    TOTAL
    Diff (TOTAL − Montant):
    `); w.document.close(); const waitImages = ()=> new Promise((res)=>{ const imgs = Array.from(w.document.images || []); if(imgs.length===0) return res(); let left = imgs.length; const done = ()=>{ left--; if(left<=0) res(); }; imgs.forEach(im=>{ if(im.complete) done(); else { im.onload = done; im.onerror = done; } }); }); w.onload = async ()=>{ await waitImages(); // Let layout settle (and fitScript run) then print setTimeout(()=>{ try{ w.focus(); w.print(); }catch(e){} }, 180); }; w.onafterprint = ()=>{ try{ w.close(); }catch(e){} }; } function groupCashTxByClient(txs){ // Group cash transactions by client (prefer clientId; fallback to name). const map = new Map(); const order = []; for(const t of (txs||[])){ const cid = String(t.clientId||"").trim(); const cname = String(t.clientName||"").trim(); const key = cid ? ("ID:"+cid) : ("NM:"+(cname.toLowerCase())); if(!map.has(key)){ map.set(key, { clientId: cid, clientName: cname, amount: 0, times: [], receipts: new Set() }); order.push(key); } const g = map.get(key); g.amount += (Number(t.amount)||0); if(t.time) g.times.push(String(t.time)); if(t.receipt) g.receipts.add(String(t.receipt)); } return order.map(k=>{ const g = map.get(k); const times = (g.times||[]).slice().sort((a,b)=> String(a).localeCompare(String(b))); const time = times.length ? (times[0]===times[times.length-1] ? times[0] : (times[0]+"–"+times[times.length-1])) : ""; const receipt = Array.from(g.receipts).join(", "); return { time, clientId: g.clientId, clientName: g.clientName, receipt, amount: g.amount }; }); } function applyTheme(theme){ if(theme==="dark") document.body.setAttribute("data-theme","dark"); else document.body.removeAttribute("data-theme"); } function loadTheme(){ try{ const saved = storageGet(THEME_KEY) || "light"; applyTheme(saved); } catch(e){ applyTheme("light"); } } function toggleTheme(){ const next = document.body.getAttribute("data-theme")==="dark" ? "light" : "dark"; applyTheme(next); try{ storageSet(THEME_KEY, next); } catch(e){} } function normalizeSkin(mode){ const value = String(mode || "").trim().toLowerCase(); return SKIN_ORDER.includes(value) ? value : "default"; } function getSkinMode(){ return normalizeSkin(document.body.getAttribute("data-skin")); } function renderSkinToggle(){ const btn = document.getElementById("cycleSkinBtn"); if(!btn) return; const mode = getSkinMode(); const label = SKIN_LABELS[mode] || mode; btn.textContent = mode === "default" ? "Style PRO" : `Style PRO: ${label}`; btn.title = `Style actuel: ${label}`; } function applySkin(mode){ const skin = normalizeSkin(mode); if(skin === "default") document.body.removeAttribute("data-skin"); else document.body.setAttribute("data-skin", skin); renderSkinToggle(); } function loadSkin(){ try{ const saved = storageGet(SKIN_KEY) || "default"; applySkin(saved); }catch(e){ applySkin("default"); } } function cycleSkin(){ const current = getSkinMode(); const index = SKIN_ORDER.indexOf(current); const next = SKIN_ORDER[(index + 1) % SKIN_ORDER.length]; applySkin(next); try{ storageSet(SKIN_KEY, next); }catch(e){} addAudit("UI_SKIN", next); } let currentRole = SINGLE_USER_MODE ? SINGLE_USER_ROLE : "caissier"; const SESSION_TIMEOUT_DEFAULT_MINUTES = 15; let sessionLocked = false; let sessionInactivityTimer = null; let sessionActivityBound = false; let sessionLastActivityAt = Date.now(); const AUDIT_DEFAULT_PAGE_SIZE = 50; const AUDIT_PAGE_SIZE_OPTIONS = new Set([25, 50, 100, 200]); let auditPage = 1; function roleLabel(role){ return ROLE_LABELS[role] || ROLE_LABELS.caissier; } function auditRoleClass(role){ const r = String(role||"").trim().toLowerCase(); if(r === "admin" || r === "audit") return r; return "caissier"; } function isKnownRole(role){ return !!ROLE_LABELS[String(role||"").trim().toLowerCase()]; } function getSessionTimeoutMinutes(){ return normalizeSessionTimeoutMinutes(state?.params?.sessionTimeoutMin, SESSION_TIMEOUT_DEFAULT_MINUTES); } function clearSessionInactivityTimer(){ if(sessionInactivityTimer){ clearTimeout(sessionInactivityTimer); sessionInactivityTimer = null; } } function scheduleSessionInactivityTimer(){ clearSessionInactivityTimer(); if(SINGLE_USER_MODE) return; if(sessionLocked) return; const ms = getSessionTimeoutMinutes() * 60 * 1000; sessionInactivityTimer = setTimeout(()=>{ lockSession(`inactivité ${getSessionTimeoutMinutes()} min`); }, ms); } function renderSessionStatus(){ const stateEl = document.getElementById("topSessionState"); const lockBtn = document.getElementById("topLockSession"); if(stateEl){ if(SINGLE_USER_MODE){ stateEl.textContent = "Mode solo"; stateEl.className = "ok"; }else if(sessionLocked){ stateEl.textContent = "Verrouillée"; stateEl.className = "bad"; }else{ stateEl.textContent = `Active (${getSessionTimeoutMinutes()} min)`; stateEl.className = "ok"; } } if(lockBtn){ if(SINGLE_USER_MODE){ lockBtn.hidden = true; }else{ lockBtn.hidden = false; lockBtn.textContent = sessionLocked ? "Déverrouiller" : "Verrouiller"; } } } function touchSessionActivity(opts={}){ if(sessionLocked && !opts.force) return; sessionLastActivityAt = Date.now(); scheduleSessionInactivityTimer(); renderSessionStatus(); } function lockSession(reason="manuel", opts={}){ if(SINGLE_USER_MODE){ sessionLocked = false; clearSessionInactivityTimer(); renderSessionStatus(); return; } if(sessionLocked) return; sessionLocked = true; clearSessionInactivityTimer(); if(opts.audit !== false){ addAudit("SESSION_LOCK", asSafeText(reason, 140)); } renderSessionStatus(); if(document.getElementById("day")) renderAll(); else renderAccessAudit(); } function unlockSession(reason="manuel", opts={}){ if(SINGLE_USER_MODE){ sessionLocked = false; touchSessionActivity({force:true}); renderSessionStatus(); return; } if(!sessionLocked) return; sessionLocked = false; touchSessionActivity({force:true}); if(opts.audit !== false){ addAudit("SESSION_UNLOCK", asSafeText(reason, 140)); } renderSessionStatus(); if(document.getElementById("day")) renderAll(); else renderAccessAudit(); } function unlockSessionInteractive(actionLabel="Déverrouiller la session"){ if(SINGLE_USER_MODE) return true; if(!sessionLocked) return true; const role = currentRole; const rolePin = getRolePin(role); if(rolePin){ if(!requirePin(actionLabel, role)) return false; }else{ if(!confirm("Session verrouillée. Aucun PIN défini pour ce rôle. Déverrouiller ?")) return false; } unlockSession(`${actionLabel} (${role})`); return true; } function ensureSessionUnlocked(actionLabel="Action sécurisée"){ if(SINGLE_USER_MODE) return true; if(!sessionLocked) return true; alert("Session verrouillée. Déverrouillage requis."); return unlockSessionInteractive(`Déverrouiller: ${actionLabel}`); } function bindSessionActivityMonitor(){ if(sessionActivityBound) return; sessionActivityBound = true; const onActivity = ()=> touchSessionActivity(); for(const evt of ["pointerdown","keydown","input","touchstart","scroll"]){ window.addEventListener(evt, onActivity, {passive:true}); } touchSessionActivity({force:true}); } function loadRoleSession(){ if(SINGLE_USER_MODE){ currentRole = isKnownRole(SINGLE_USER_ROLE) ? SINGLE_USER_ROLE : "admin"; try{ sessionStorage.setItem(ROLE_SESSION_KEY, currentRole); }catch(e){} return; } let role = "caissier"; try{ const raw = sessionStorage.getItem(ROLE_SESSION_KEY); if(isKnownRole(raw)) role = String(raw).trim().toLowerCase(); }catch(e){} currentRole = role; } function setCurrentRole(role, opts={}){ if(SINGLE_USER_MODE){ const forced = isKnownRole(SINGLE_USER_ROLE) ? SINGLE_USER_ROLE : "admin"; currentRole = forced; try{ sessionStorage.setItem(ROLE_SESSION_KEY, forced); }catch(e){} if(document.getElementById("day")) renderAll(); else renderAccessAudit(); return true; } const next = String(role||"").trim().toLowerCase(); if(!isKnownRole(next)) return false; const prev = currentRole; currentRole = next; try{ sessionStorage.setItem(ROLE_SESSION_KEY, next); }catch(e){} if(!opts.silent && prev !== next){ addAudit("ROLE_SWITCH", `${roleLabel(prev)} -> ${roleLabel(next)}`); } if(document.getElementById("day")) renderAll(); else renderAccessAudit(); return true; } function getRolePin(role){ const r = String(role||"").trim().toLowerCase(); if(r==="admin") return String(state?.params?.pinAdmin || state?.params?.pin || "").trim(); if(r==="caissier") return String(state?.params?.pinCashier || "").trim(); if(r==="audit") return String(state?.params?.pinAudit || "").trim(); return ""; } function hasRoleCapability(cap, role=currentRole){ if(SINGLE_USER_MODE) return true; const caps = ROLE_CAPS[role] || {}; return !!caps[cap]; } function hasCapability(cap){ if(SINGLE_USER_MODE) return true; if(sessionLocked) return false; return hasRoleCapability(cap, currentRole); } function addAudit(action, details=""){ try{ if(!state || !Array.isArray(state.audit)) return; const entry = { id: makeId("audit"), ts: new Date().toISOString(), day: normalizeDate(state?.params?.day, nowDate()), role: currentRole, action: asSafeText(action, 100), details: asSafeText(details, 500) }; state.audit.unshift(entry); if(state.audit.length > 2000) state.audit.length = 2000; save(); }catch(e){ console.warn("audit write failed", e); } } function requireCapability(cap, actionLabel){ if(!ensureSessionUnlocked(actionLabel || cap)) return false; touchSessionActivity(); if(hasCapability(cap)) return true; const msg = `Accès refusé pour le rôle ${roleLabel(currentRole)}.`; alert(msg); addAudit("ACCESS_DENIED", `${asSafeText(actionLabel||cap, 120)} (${currentRole})`); return false; } function switchRoleInteractive(){ if(SINGLE_USER_MODE){ setCurrentRole(SINGLE_USER_ROLE, {silent:true}); alert("Mode utilisateur unique actif: rôle Admin fixe."); return; } touchSessionActivity({force:true}); if(!hasRoleCapability("switchRole", currentRole)){ const msg = `Accès refusé pour le rôle ${roleLabel(currentRole)}.`; alert(msg); addAudit("ACCESS_DENIED", `Changer de rôle (${currentRole})`); return; } const prevRole = currentRole; const raw = prompt("Choisissez le rôle: admin / caissier / audit", currentRole); if(raw === null) return; const role = String(raw).trim().toLowerCase(); if(!isKnownRole(role)) return alert("Rôle invalide. Valeurs autorisées: admin, caissier, audit."); if(role === currentRole) return; if(!requirePin("Changer de rôle", role)) return alert("PIN invalide."); if(!setCurrentRole(role)) return; // If the session was locked, switching role with the target PIN should restore access immediately. if(sessionLocked){ unlockSession(`changement de rôle ${roleLabel(prevRole)} -> ${roleLabel(role)}`); } } function formatAuditTs(ts){ const s = String(ts||"").trim(); const dt = new Date(s); if(!Number.isFinite(dt.getTime())) return escapeHtml(s); const y = dt.getFullYear(); const m = String(dt.getMonth()+1).padStart(2,"0"); const d = String(dt.getDate()).padStart(2,"0"); const hh = String(dt.getHours()).padStart(2,"0"); const mm = String(dt.getMinutes()).padStart(2,"0"); const ss = String(dt.getSeconds()).padStart(2,"0"); return `${y}-${m}-${d} ${hh}:${mm}:${ss}`; } function getAuditDateKey(a){ const day = toIsoDate(a?.day || ""); if(day) return day; const ts = String(a?.ts || "").trim(); if(!ts) return ""; const tsPrefix = toIsoDate(ts.slice(0,10)); if(tsPrefix) return tsPrefix; const dt = new Date(ts); if(!Number.isFinite(dt.getTime())) return ""; const y = dt.getFullYear(); const m = String(dt.getMonth()+1).padStart(2,"0"); const d = String(dt.getDate()).padStart(2,"0"); return `${y}-${m}-${d}`; } function isoDateAddDays(iso, deltaDays){ const base = toIsoDate(iso); if(!base) return ""; const m = base.match(/^(\d{4})-(\d{2})-(\d{2})$/); if(!m) return ""; const dt = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))); dt.setUTCDate(dt.getUTCDate() + Number(deltaDays || 0)); const y = dt.getUTCFullYear(); const mo = String(dt.getUTCMonth() + 1).padStart(2, "0"); const d = String(dt.getUTCDate()).padStart(2, "0"); return `${y}-${mo}-${d}`; } function readAuditPageSize(){ const v = Number(document.getElementById("auditPageSize")?.value || AUDIT_DEFAULT_PAGE_SIZE); return AUDIT_PAGE_SIZE_OPTIONS.has(v) ? v : AUDIT_DEFAULT_PAGE_SIZE; } function setAuditDatePreset(daysBack){ const to = normalizeDate(state?.params?.day, nowDate()); const span = Math.max(1, Math.floor(Number(daysBack) || 1)); const from = (span <= 1) ? to : isoDateAddDays(to, -(span - 1)); const fromEl = document.getElementById("auditDateFrom"); const toEl = document.getElementById("auditDateTo"); if(fromEl) fromEl.value = from; if(toEl) toEl.value = to; auditPage = 1; renderAccessAudit(); } function resetAuditFilters(){ const resetMap = [ ["auditFilter", ""], ["auditRoleFilter", ""], ["auditDateFrom", ""], ["auditDateTo", ""], ["auditPageSize", String(AUDIT_DEFAULT_PAGE_SIZE)] ]; for(const [id, val] of resetMap){ const el = document.getElementById(id); if(el) el.value = val; } auditPage = 1; renderAccessAudit(); } function computeAuditStats(rows){ const stats = { filtered: rows.length, admin: 0, caissier: 0, audit: 0, denied: 0, pinFailed: 0 }; for(const a of rows){ const role = String(a?.role || "").trim().toLowerCase(); if(role === "admin") stats.admin++; else if(role === "audit") stats.audit++; else stats.caissier++; const action = String(a?.action || "").trim().toUpperCase(); if(action === "ACCESS_DENIED") stats.denied++; if(action === "PIN_FAILED") stats.pinFailed++; } return stats; } function getAuditTimestampMs(a){ const ts = String(a?.ts || "").trim(); if(ts){ const dt = new Date(ts); if(Number.isFinite(dt.getTime())) return dt.getTime(); } const day = toIsoDate(a?.day || ""); if(!day) return 0; const dtDay = new Date(`${day}T00:00:00`); return Number.isFinite(dtDay.getTime()) ? dtDay.getTime() : 0; } function detectAuditBurst(rows, action, threshold=3, windowMinutes=10){ const target = String(action || "").trim().toUpperCase(); const windowMs = Math.max(1, Number(windowMinutes || 10)) * 60 * 1000; const tsList = []; for(const a of (rows || [])){ const act = String(a?.action || "").trim().toUpperCase(); if(act !== target) continue; const ms = getAuditTimestampMs(a); if(ms > 0) tsList.push(ms); } tsList.sort((x, y)=> x - y); let left = 0; let bestCount = 0; let bestStart = 0; let bestEnd = 0; for(let right = 0; right < tsList.length; right++){ while(left <= right && (tsList[right] - tsList[left]) > windowMs) left++; const count = right - left + 1; if(count > bestCount){ bestCount = count; bestStart = tsList[left]; bestEnd = tsList[right]; } } return { action: target, count: bestCount, threshold: Math.max(1, Number(threshold || 3)), start: bestStart, end: bestEnd }; } function getTopAuditActions(rows, limit=8){ const tally = new Map(); for(const a of (rows || [])){ const action = String(a?.action || "").trim().toUpperCase() || "N/A"; tally.set(action, (tally.get(action) || 0) + 1); } return Array.from(tally.entries()) .sort((x, y)=> y[1] - x[1] || String(x[0]).localeCompare(String(y[0]))) .slice(0, Math.max(1, Number(limit || 8))) .map(([action, count])=> ({ action, count })); } function computeAuditAnomalies(rows){ const list = Array.isArray(rows) ? rows : []; const out = []; const maxByDay = (m)=>{ let day = ""; let count = 0; for(const [k, v] of m.entries()){ if(v > count){ day = k; count = v; } } return { day, count }; }; const deniedBurst = detectAuditBurst(list, "ACCESS_DENIED", 3, 10); if(deniedBurst.count >= deniedBurst.threshold){ out.push({ severity: "high", title: "Rafale d'accès refusés", details: `${deniedBurst.count} refus en 10 min (seuil ${deniedBurst.threshold}).` }); } const pinBurst = detectAuditBurst(list, "PIN_FAILED", 3, 10); if(pinBurst.count >= pinBurst.threshold){ out.push({ severity: "high", title: "Rafale de PIN invalides", details: `${pinBurst.count} échecs PIN en 10 min (seuil ${pinBurst.threshold}).` }); } const roleSwitchByDay = new Map(); const exportByDay = new Map(); const sensitiveActions = new Set([ "ACCESS_DENIED", "PIN_FAILED", "ROLE_SWITCH", "AUDIT_CLEAR", "AUDIT_EXPORT", "AUDIT_PRINT", "COMPLIANCE_PRINT" ]); const exportActions = new Set([ "EXPORT_JSON", "EXPORT_CSV", "EXPORT_XLS_DAY", "EXPORT_XLS_CLIENTS", "EXPORT_PDF_REQUEST", "AUDIT_EXPORT" ]); let afterHoursSensitive = 0; for(const a of list){ const day = getAuditDateKey(a) || "jour_inconnu"; const action = String(a?.action || "").trim().toUpperCase(); if(action === "ROLE_SWITCH"){ roleSwitchByDay.set(day, (roleSwitchByDay.get(day) || 0) + 1); } if(exportActions.has(action)){ exportByDay.set(day, (exportByDay.get(day) || 0) + 1); } if(sensitiveActions.has(action)){ const ms = getAuditTimestampMs(a); if(ms > 0){ const h = new Date(ms).getHours(); if(h < 6 || h >= 22) afterHoursSensitive++; } } } const switchPeak = maxByDay(roleSwitchByDay); if(switchPeak.count >= 8){ out.push({ severity: "medium", title: "Changements de rôle élevés", details: `${switchPeak.count} bascules le ${switchPeak.day}.` }); } const exportPeak = maxByDay(exportByDay); if(exportPeak.count >= 10){ out.push({ severity: "medium", title: "Volume d'exports élevé", details: `${exportPeak.count} exports le ${exportPeak.day}.` }); } if(afterHoursSensitive >= 1){ out.push({ severity: afterHoursSensitive >= 4 ? "medium" : "low", title: "Actions sensibles hors horaires", details: `${afterHoursSensitive} action(s) sensible(s) entre 22:00 et 06:00.` }); } const severityWeight = { high: 3, medium: 2, low: 1 }; out.sort((a, b)=> (severityWeight[b.severity] || 0) - (severityWeight[a.severity] || 0)); return out.slice(0, 8); } function computeAuditHealth(rows, stats=null, alerts=null){ const list = Array.isArray(rows) ? rows : []; const s = stats || computeAuditStats(list); const a = Array.isArray(alerts) ? alerts : computeAuditAnomalies(list); const severityWeight = { high: 12, medium: 7, low: 3 }; const penalties = []; let score = 100; const deniedPenalty = Math.min(30, s.denied * 2); if(deniedPenalty > 0){ score -= deniedPenalty; penalties.push(`Accès refusés (${s.denied})`); } const pinPenalty = Math.min(30, s.pinFailed * 3); if(pinPenalty > 0){ score -= pinPenalty; penalties.push(`PIN invalides (${s.pinFailed})`); } const alertPenaltyRaw = a.reduce((sum, it)=> sum + (severityWeight[String(it?.severity||"").toLowerCase()] || 0), 0); const alertPenalty = Math.min(40, alertPenaltyRaw); if(alertPenalty > 0){ score -= alertPenalty; penalties.push(`Anomalies détectées (${a.length})`); } let roleSwitchCount = 0; let auditClearCount = 0; let afterHoursSensitive = 0; const sensitiveActions = new Set([ "ACCESS_DENIED", "PIN_FAILED", "ROLE_SWITCH", "AUDIT_CLEAR", "AUDIT_EXPORT", "AUDIT_PRINT", "COMPLIANCE_PRINT" ]); for(const item of list){ const action = String(item?.action || "").trim().toUpperCase(); if(action === "ROLE_SWITCH") roleSwitchCount++; if(action === "AUDIT_CLEAR") auditClearCount++; if(sensitiveActions.has(action)){ const ms = getAuditTimestampMs(item); if(ms > 0){ const h = new Date(ms).getHours(); if(h < 6 || h >= 22) afterHoursSensitive++; } } } if(roleSwitchCount > 6){ const p = Math.min(15, (roleSwitchCount - 6) * 2); score -= p; penalties.push(`Basculements de rôles (${roleSwitchCount})`); } if(auditClearCount > 0){ const p = Math.min(25, auditClearCount * 12); score -= p; penalties.push(`Journal effacé (${auditClearCount})`); } if(afterHoursSensitive > 0){ const p = Math.min(15, afterHoursSensitive * 2); score -= p; penalties.push(`Actions sensibles hors horaires (${afterHoursSensitive})`); } score = Math.max(0, Math.min(100, Math.round(score))); let level = "Excellent"; let tone = "good"; if(score < 40){ level = "Critique"; tone = "risk"; } else if(score < 55){ level = "Faible"; tone = "risk"; } else if(score < 70){ level = "Moyen"; tone = "medium"; } else if(score < 85){ level = "Bon"; tone = "medium"; } const tips = []; if(s.denied > 0) tips.push("Réviser les habilitations des rôles et limiter les opérations sensibles."); if(s.pinFailed > 0) tips.push("Renforcer la politique PIN (complexité, rotation, sensibilisation)."); if(roleSwitchCount >= 8) tips.push("Surveiller les changements de rôle fréquents dans une même période."); if(auditClearCount > 0) tips.push("Restreindre l'effacement du journal et exiger une validation à double contrôle."); if(afterHoursSensitive > 0) tips.push("Vérifier les actions sensibles effectuées entre 22:00 et 06:00."); if(a.some(x=> String(x?.severity||"").toLowerCase()==="high")){ tips.push("Traiter en priorité les alertes de niveau élevé."); } if(score < 60){ tips.push("Lancer une revue de conformité et corriger les causes dans la journée."); } if(tips.length === 0){ tips.push("Aucun risque majeur détecté sur le périmètre filtré."); } const summary = penalties.length ? `Facteurs de risque: ${penalties.slice(0,3).join(" • ")}.` : `Aucun facteur critique détecté sur ce périmètre.`; return { score, level, tone, summary, tips: tips.slice(0, 6) }; } function renderAuditHealth(health, canView){ const scoreEl = document.getElementById("auditHealthScore"); const levelEl = document.getElementById("auditHealthLevel"); const barEl = document.getElementById("auditHealthBar"); const summaryEl = document.getElementById("auditHealthSummary"); const tipsEl = document.getElementById("auditHealthTips"); const scoreKpiEl = document.getElementById("auditHealthScoreKpi"); if(!scoreEl || !levelEl || !barEl || !summaryEl || !tipsEl) return; if(!canView){ if(scoreKpiEl) scoreKpiEl.textContent = "—"; scoreEl.textContent = "—"; levelEl.textContent = "N/A"; summaryEl.textContent = "Analyse indisponible pour ce rôle."; tipsEl.innerHTML = ""; barEl.style.width = "0%"; for(const cls of ["audit-health-score-good","audit-health-score-medium","audit-health-score-risk"]){ scoreEl.classList.remove(cls); } for(const cls of ["audit-health-fill-good","audit-health-fill-medium","audit-health-fill-risk"]){ barEl.classList.remove(cls); } barEl.classList.add("audit-health-fill-risk"); return; } const h = health || { score: 0, level: "N/A", tone: "risk", summary: "", tips: [] }; if(scoreKpiEl) scoreKpiEl.textContent = String(h.score); scoreEl.textContent = String(h.score); levelEl.textContent = String(h.level || "N/A"); summaryEl.textContent = String(h.summary || ""); barEl.style.width = `${Math.max(0, Math.min(100, Number(h.score)||0))}%`; tipsEl.innerHTML = (Array.isArray(h.tips) ? h.tips : []).map(t=>`
  • ${escapeHtml(t)}
  • `).join(""); for(const cls of ["audit-health-score-good","audit-health-score-medium","audit-health-score-risk"]){ scoreEl.classList.remove(cls); } for(const cls of ["audit-health-fill-good","audit-health-fill-medium","audit-health-fill-risk"]){ barEl.classList.remove(cls); } if(h.tone === "risk"){ scoreEl.classList.add("audit-health-score-risk"); barEl.classList.add("audit-health-fill-risk"); }else if(h.tone === "medium"){ scoreEl.classList.add("audit-health-score-medium"); barEl.classList.add("audit-health-fill-medium"); }else{ scoreEl.classList.add("audit-health-score-good"); barEl.classList.add("audit-health-fill-good"); } } function renderAuditAlerts(alerts, canView){ const box = document.getElementById("auditAlerts"); if(!box) return; if(!canView){ box.innerHTML = ""; return; } const arr = Array.isArray(alerts) ? alerts : []; if(arr.length === 0){ box.innerHTML = `
    Surveillance activeAucune anomalie détectée sur ce périmètre.
    `; return; } box.innerHTML = arr.map(a=>`
    ${escapeHtml(a.title || "Alerte")} ${escapeHtml(a.details || "")}
    `).join(""); } function onAuditFiltersChanged(){ auditPage = 1; renderAccessAudit(); } function getAuditFilters(){ const qRaw = String(document.getElementById("auditFilter")?.value || "").trim(); const q = qRaw.toLowerCase(); const rawRole = String(document.getElementById("auditRoleFilter")?.value || "").trim().toLowerCase(); let from = toIsoDate(document.getElementById("auditDateFrom")?.value || ""); let to = toIsoDate(document.getElementById("auditDateTo")?.value || ""); if(from && to && from > to){ const tmp = from; from = to; to = tmp; } const role = isKnownRole(rawRole) ? rawRole : ""; return { qRaw, q, role, from, to }; } function getFilteredAuditRows(){ const { q, role, from, to } = getAuditFilters(); return (state.audit || []).filter(a=>{ const entryRole = String(a.role || "").trim().toLowerCase(); if(role && entryRole !== role) return false; const d = getAuditDateKey(a); if(from && (!d || d < from)) return false; if(to && (!d || d > to)) return false; if(!q) return true; const blob = `${a.ts||""} ${a.day||""} ${a.role||""} ${a.action||""} ${a.details||""}`.toLowerCase(); return blob.includes(q); }); } function renderAccessAudit(){ const topRole = document.getElementById("topRole"); if(topRole) topRole.textContent = roleLabel(currentRole); renderSessionStatus(); const activeRoleLabel = document.getElementById("activeRoleLabel"); if(activeRoleLabel) activeRoleLabel.textContent = roleLabel(currentRole); const switchRoleBtn = document.getElementById("switchRoleBtn"); if(switchRoleBtn) switchRoleBtn.hidden = !!SINGLE_USER_MODE; const topSwitchRoleBtn = document.getElementById("topSwitchRole"); if(topSwitchRoleBtn) topSwitchRoleBtn.hidden = !!SINGLE_USER_MODE; const canViewByRole = hasRoleCapability("viewAudit"); const canClear = hasCapability("clearAudit"); const canView = hasCapability("viewAudit"); const canExportAudit = canView && hasCapability("exportAudit"); const canPrintAudit = canView && hasCapability("printAudit"); const canPrintCompliance = canView && hasCapability("printAudit"); const clearBtn = document.getElementById("clearAuditBtn"); const exportBtn = document.getElementById("exportAuditBtn"); const printBtn = document.getElementById("printAuditBtn"); const complianceBtn = document.getElementById("printComplianceBtn"); const prevPageBtn = document.getElementById("auditPrevPageBtn"); const nextPageBtn = document.getElementById("auditNextPageBtn"); const pageCurrentEl = document.getElementById("auditPageCurrent"); const pageTotalEl = document.getElementById("auditPageTotal"); if(clearBtn) clearBtn.disabled = !canClear; if(exportBtn) exportBtn.disabled = !canExportAudit; if(printBtn) printBtn.disabled = !canPrintAudit; if(complianceBtn) complianceBtn.disabled = !canPrintCompliance; for(const id of [ "auditFilter", "auditRoleFilter", "auditDateFrom", "auditDateTo", "auditPageSize", "auditPresetTodayBtn", "auditPreset7dBtn", "auditPreset30dBtn", "auditResetFiltersBtn", "auditPrevPageBtn", "auditNextPageBtn" ]){ const el = document.getElementById(id); if(el) el.disabled = !canView; } const tbody = document.getElementById("auditTbody"); let totalFiltered = 0; let shownRows = 0; let totalPages = 1; let pageStart = 0; let pageEnd = 0; let stats = computeAuditStats([]); let alerts = []; let filteredRows = []; if(tbody){ if(!canView){ auditPage = 1; tbody.innerHTML = sessionLocked ? `Session verrouillée. Déverrouillez pour consulter le journal.` : `Journal non disponible pour ce rôle.`; }else{ const filtered = getFilteredAuditRows(); filteredRows = filtered; stats = computeAuditStats(filtered); alerts = computeAuditAnomalies(filtered); totalFiltered = filtered.length; const pageSize = readAuditPageSize(); totalPages = Math.max(1, Math.ceil(totalFiltered / pageSize)); if(auditPage > totalPages) auditPage = totalPages; if(auditPage < 1) auditPage = 1; const startIdx = Math.max(0, (auditPage - 1) * pageSize); const list = filtered.slice(startIdx, startIdx + pageSize); shownRows = list.length; if(totalFiltered > 0){ pageStart = startIdx + 1; pageEnd = startIdx + shownRows; } tbody.innerHTML = list.map(a=>` ${formatAuditTs(a.ts)} ${roleLabel(a.role)} ${escapeHtml(a.action||"")} ${escapeHtml(a.details||"")} `).join("") || `Aucun événement.`; } } if(pageCurrentEl) pageCurrentEl.textContent = String(Math.max(1, auditPage)); if(pageTotalEl) pageTotalEl.textContent = String(Math.max(1, totalPages)); if(prevPageBtn) prevPageBtn.disabled = !canView || auditPage <= 1; if(nextPageBtn) nextPageBtn.disabled = !canView || auditPage >= totalPages; const setText = (id, value)=>{ const el = document.getElementById(id); if(el) el.textContent = String(value); }; setText("auditKpiFiltered", canView ? stats.filtered : 0); setText("auditKpiAdmin", canView ? stats.admin : 0); setText("auditKpiCashier", canView ? stats.caissier : 0); setText("auditKpiAudit", canView ? stats.audit : 0); setText("auditKpiDenied", canView ? stats.denied : 0); setText("auditKpiPinFailed", canView ? stats.pinFailed : 0); setText("auditKpiAlerts", canView ? alerts.length : 0); const health = computeAuditHealth(filteredRows, stats, alerts); renderAuditHealth(health, canView); renderAuditAlerts(alerts, canView); const hint = document.getElementById("auditHint"); if(hint){ const total = Array.isArray(state.audit) ? state.audit.length : 0; const range = totalFiltered > 0 ? `${pageStart}-${pageEnd}` : "0"; hint.textContent = canView ? `Rôle actif: ${roleLabel(currentRole)}. Page ${Math.max(1, auditPage)}/${Math.max(1, totalPages)}. Lignes: ${range}/${totalFiltered} (total global: ${total}). Alertes: ${alerts.length}. Health: ${health.score}/100 (${health.level}).` : (sessionLocked ? `Rôle actif: ${roleLabel(currentRole)}. Session verrouillée (${getSessionTimeoutMinutes()} min d'inactivité).` : `Rôle actif: ${roleLabel(currentRole)}.${canViewByRole ? " Déverrouillez la session pour continuer." : ""}`); } // Role-based UI hardening (server-less app: also keep JS permission checks on handlers) const gateButtons = [ ["saveParamsBtn", "manageConfig"], ["saveUiSettingsBtn", "manageConfig"], ["saveLogoSettingsBtn", "manageConfig"], ["resetUiSettingsBtn", "manageConfig"], ["openLogoEditorBtn", "manageConfig"], ["removeCustomLogoBtn", "manageConfig"], ["resetLogoSettingsBtn", "manageConfig"], ["newDayBtn", "manageDay"], ["closeDayBtn", "manageDay"], ["openDayBtn", "manageDay"], ["saveClientBtn", "manageData"], ["cancelClientEditBtn", "manageData"], ["openClientModalBtn", "manageData"], ["clientModalSaveBtn", "manageData"], ["saveTxBtn", "manageData"], ["openTxModalBtn", "manageData"], ["txModalSaveBtn", "manageData"], ["clearDayTxBtn", "manageData"], ["saveDecBtn", "manageData"], ["openDecModalBtn", "manageData"], ["decModalSaveBtn", "manageData"], ["clearDayDecBtn", "manageData"], ["saveDrawerCountBtn", "manageData"], ["resetDenBtn", "manageData"], ["autoDenBtn", "manageData"], ["copyFondRenduBtn", "manageData"], ["newBordBtn", "manageData"], ["navForceSaveBtn", "manageData"], ["undoBtn", "manageData"], ["exportJsonBtn", "export"], ["createAutoBackupBtn", "export"], ["restoreAutoBackupBtn", "manageImports"], ["exportCsvBtn", "export"], ["exportXlsBtn", "export"], ["exportClientsXlsBtn", "export"], ["exportPdfBtn", "export"], ["switchRoleBtn", "switchRole"], ["topSwitchRole", "switchRole"], ["exportAuditBtn", "exportAudit"], ["printAuditBtn", "printAudit"], ["printComplianceBtn", "printAudit"], ["printBordBtn", "printOps"], ["printReportBtn", "printOps"], ["printPackBtn", "printOps"], ["topPrintBord", "printOps"], ["topPrintReport", "printOps"], ["topPrintPack", "printOps"] ]; const canUseGateCapability = (cap)=> cap === "switchRole" ? hasRoleCapability("switchRole", currentRole) : hasCapability(cap); for(const [id, cap] of gateButtons){ const el = document.getElementById(id); if(el) el.disabled = !canUseGateCapability(cap); } const importJson = document.getElementById("importFile"); const importClients = document.getElementById("importClientsFile"); if(importJson) importJson.disabled = !hasCapability("manageImports"); if(importClients) importClients.disabled = !hasCapability("manageImports"); const logoFileInput = document.getElementById("logoFileInput"); if(logoFileInput) logoFileInput.disabled = !hasCapability("manageConfig"); for(const id of [ "logoSize","logoOffsetX","logoOffsetY","logoClarity","logoOpacity", "printLogoSize","printLogoOffsetX","printLogoOffsetY","printLogoOpacity", "watermarkScale","watermarkOffsetX","watermarkOffsetY","watermarkOpacity" ]){ const el = document.getElementById(id); if(el) el.disabled = !hasCapability("manageConfig"); } for(const id of [ "uiBaseFontSize","uiButtonFontSize","uiButtonPadY","uiButtonPadX","uiButtonRadius", "uiFieldFontSize","uiFieldPadY","uiFieldRadius", "uiNavFontSize","uiNavItemMinHeight","uiNavWidth","uiTabFontSize" ]){ const el = document.getElementById(id); if(el) el.disabled = !hasCapability("manageConfig"); } for(const id of [ "logoEditorRotation","logoEditorRotationNumber","logoEditorCropX","logoEditorCropY", "logoEditorCropW","logoEditorCropH","logoRotateLeftBtn","logoRotateRightBtn", "logoEditorFullFrameBtn","logoEditorResetBtn","logoEditorApplyBtn" ]){ const el = document.getElementById(id); if(el) el.disabled = !hasCapability("manageConfig"); } const renduInput = document.getElementById("renduAmount"); if(renduInput) renduInput.disabled = !hasCapability("manageData"); const canEditPins = currentRole === "admin" && hasCapability("manageConfig"); for(const id of ["pin","pinCashier","pinAudit"]){ const el = document.getElementById(id); if(!el) continue; if(canEditPins){ el.disabled = false; }else{ el.value = ""; el.disabled = true; } } renderUndoState(); renderAutoBackupInfo(); renderDensityToggle(); } function isClosed(day){ return !!state.closedDays[day]; } function requirePin(action, roleOverride=null){ if(SINGLE_USER_MODE) return true; const role = isKnownRole(roleOverride) ? String(roleOverride).trim().toLowerCase() : currentRole; const pin = getRolePin(role); if(!pin) return true; const entered = prompt(`PIN ${roleLabel(role)} requis : ${action}`); const ok = entered === pin; if(!ok) addAudit("PIN_FAILED", `${roleLabel(role)} - ${action}`); return ok; } /* French words */ function frUnits(n){ const u = ["zéro","un","deux","trois","quatre","cinq","six","sept","huit","neuf","dix", "onze","douze","treize","quatorze","quinze","seize","dix-sept","dix-huit","dix-neuf"]; return u[n]; } function frBelow100(n){ n=Math.floor(n); if(n<20) return frUnits(n); if(n<70){ const t=Math.floor(n/10), r=n%10; const tens=["","dix","vingt","trente","quarante","cinquante","soixante"]; if(r===0) return tens[t]; if(r===1) return tens[t]+" et un"; return tens[t]+"-"+frUnits(r); } if(n<80){ const r=n-60; if(r===11) return "soixante et onze"; return "soixante-"+frBelow100(r); } if(n===80) return "quatre-vingts"; return "quatre-vingt-"+frBelow100(n-80); } function frBelow1000(n){ n=Math.floor(n); if(n<100) return frBelow100(n); const h=Math.floor(n/100), r=n%100; let s = (h===1) ? "cent" : (frUnits(h)+" cent"); if(r===0){ if(h>1) s+="s"; return s; } return s+" "+frBelow100(r); } function frNumber(n){ n=Math.floor(Math.abs(n)); if(n===0) return "zéro"; const parts=[]; const million=Math.floor(n/1000000); const restM=n%1000000; const thousand=Math.floor(restM/1000); const rest=restM%1000; if(million) parts.push(million===1 ? "un million" : frBelow1000(million)+" millions"); if(thousand) parts.push(thousand===1 ? "mille" : frBelow1000(thousand)+" mille"); if(rest) parts.push(frBelow1000(rest)); return parts.join(" "); } function amountToWordsDZD(n){ const val = round2(parseAmount(n)); let dinars = Math.floor(val); let cents = Math.round((val - dinars) * 100); if(cents===100){ dinars += 1; cents = 0; } const cap = (s)=> s ? (s.charAt(0).toUpperCase()+s.slice(1)) : s; const wD = frNumber(dinars); if(cents>0){ const wC = frNumber(cents); return cap(wD)+" dinars algériens et "+wC+" centimes"; } return cap(wD)+" dinars algériens"; } /* Denoms */ const DENOMS = [ {type:"Billet", value:2000, key:"B_2000"}, {type:"Billet", value:1000, key:"B_1000"}, {type:"Billet", value:500, key:"B_500"}, {type:"Billet", value:200, key:"B_200"}, {type:"Pièce", value:200, key:"P_200"}, {type:"Pièce", value:100, key:"P_100"}, {type:"Pièce", value:50, key:"P_50"}, {type:"Pièce", value:20, key:"P_20"}, {type:"Pièce", value:10, key:"P_10"}, {type:"Pièce", value:5, key:"P_5"}, ]; function getDen(day, bankKey){ if(!state.denoms[bankKey][day]) state.denoms[bankKey][day] = {}; return state.denoms[bankKey][day]; } function getBordExtra(day, bankKey){ if(!state.bordExtra) state.bordExtra = { BEA:{}, CPA:{} }; if(!state.bordExtra[bankKey]) state.bordExtra[bankKey] = {}; if(!state.bordExtra[bankKey][day]) state.bordExtra[bankKey][day] = {}; return state.bordExtra[bankKey][day]; } function denomTotal(day, bankKey){ const den=getDen(day, bankKey); return DENOMS.reduce((s,d)=> s + (Number(den[d.key]||0)*d.value), 0); } function cleanDenoms(obj){ const out = {}; for(const d of DENOMS){ const q = Math.max(0, Math.floor(Number(obj?.[d.key]||0))); if(q) out[d.key] = q; } return out; } function denomsValue(den){ return DENOMS.reduce((s,d)=> s + (Number(den?.[d.key]||0) * d.value), 0); } function denomsSum(list){ const out = {}; for(const d of DENOMS) out[d.key] = 0; for(const o of (list||[])){ if(!o) continue; for(const d of DENOMS){ const q = Math.floor(Number(o[d.key]||0)); if(q) out[d.key] += q; } } return out; } function denomsFromForm(kind){ // kind: "tx" | "dec" try{ return cleanDenoms((formCashCalcState && formCashCalcState[kind]) ? formCashCalcState[kind] : {}); } catch(e){ return {}; } } function denomsFromTx(day, bankKey){ const prod = bankProduct(bankKey); const list = txOfDay(day).filter(t=> t.mode==="Espèces" && t.produit===prod && t.cashDenoms); return denomsSum(list.map(t=> t.cashDenoms)); } function autoDenomsCandidate(day, bankKey){ // 1) From saved encaissements (best) const fromTx = denomsFromTx(day, bankKey); if(denomsValue(fromTx) > 0) return {src:"tx", denoms:fromTx}; // 2) From current Encaissement cash calculator (if produit matches bank) try{ const prodWanted = bankProduct(bankKey); const txMode = document.getElementById("txMode") ? document.getElementById("txMode").value : ""; const txProd = document.getElementById("txProduit") ? document.getElementById("txProduit").value : ""; if(txMode==="Espèces" && txProd===prodWanted){ const d = denomsFromForm("tx"); if(denomsValue(d) > 0) return {src:"txForm", denoms:d}; } }catch(e){} // 3) From current Décaissement cash calculator (applied to active bank) try{ const decMode = document.getElementById("decMode") ? document.getElementById("decMode").value : ""; if(decMode==="Espèces"){ const d = denomsFromForm("dec"); if(denomsValue(d) > 0) return {src:"decForm", denoms:d}; } }catch(e){} return {src:"none", denoms:{}}; } function ensureAutoDenoms(day, bankKey){ const den = getDen(day, bankKey); if(den._manual) return den; // user chose manual editing const cand = autoDenomsCandidate(day, bankKey); den._auto = cand.src; for(const d of DENOMS){ den[d.key] = Math.max(0, Math.floor(Number(cand.denoms?.[d.key]||0))); } save(); return den; } /* Bord seq */ function bordPrefix(bankKey){ return bankKey; } function currentBordNo(day, bankKey){ const cur = Number(state.bordSeq[bankKey][day]||0) || 1; if(!state.bordSeq[bankKey][day]) { state.bordSeq[bankKey][day]=1; save(); } const code = bordPrefix(bankKey); return day.replaceAll("-","")+"-"+code+"-"+String(cur).padStart(3,"0"); } function nextBordNo(day, bankKey){ const cur = Number(state.bordSeq[bankKey][day]||0); const nxt = cur + 1; state.bordSeq[bankKey][day]=nxt; save(); const code = bordPrefix(bankKey); return day.replaceAll("-","")+"-"+code+"-"+String(nxt).padStart(3,"0"); } /* Clients */ function upsertClient(id, name){ id = asPositiveInt(id); name = asSafeText(name, 180); if(!id || !name) return false; const idx=state.clients.findIndex(c=>c.id===id); const existed = idx >= 0; captureUndo(existed ? `Modification client #${id}` : `Ajout client #${id}`); if(idx>=0) state.clients[idx].name=name; else state.clients.push({id, name}); state.clients.sort((a,b)=>a.id-b.id); if(!save()) return false; addAudit(existed ? "CLIENT_UPDATE" : "CLIENT_ADD", `id=${id} nom=${name}`); return true; } function deleteClient(id){ id = asPositiveInt(id); if(!id) return; if(!state.clients.some(c=>c.id===id)) return; const before = state.clients.length; captureUndo(`Suppression client #${id}`); state.clients = state.clients.filter(c=>c.id!==id); if(state.clients.length === before) return; if(editClientId === id) editClientId = null; if(!save()) return; addAudit("CLIENT_DELETE", `id=${id}`); } function clientNameById(id){ id = asPositiveInt(id); const c=state.clients.find(x=>x.id===id); return c?c.name:""; } function clientById(id){ id = asPositiveInt(id); return state.clients.find(x=>x.id===id) || null; } const TX_WARN_AMOUNT_DZD = 500000; const TX_MAX_AMOUNT_DZD = 5000000; const TX_DUP_TIME_WINDOW_MIN = 5; function txReceiptKey(v){ return asSafeText(v, 80).toLowerCase(); } function parseHHMMToMinutes(v){ const s = String(v||"").trim(); const m = s.match(/^([01]\d|2[0-3]):([0-5]\d)$/); if(!m) return -1; return Number(m[1]) * 60 + Number(m[2]); } function validateTxBeforeSave(tx, opts={}){ const editTxId = String(opts.editTxId || "").trim(); const isEdit = !!opts.isEdit; const peers = (state.tx || []).filter(t=> t.day === tx.day && (!editTxId || String(t.id||"") !== editTxId)); const receipt = txReceiptKey(tx.receipt); if(receipt){ const sameReceipt = peers.filter(t=> txReceiptKey(t.receipt) === receipt); if(sameReceipt.length){ const conflictClient = sameReceipt.find(t=> Number(t.clientId||0) !== tx.clientId); if(conflictClient){ alert(`N° reçu déjà utilisé (client ${conflictClient.clientId}) pour ce jour.`); addAudit("TX_VALIDATE_BLOCK", `recu_duplique=${receipt}; client=${tx.clientId}`); return false; } const conflictProduct = sameReceipt.find(t=> String(t.produit||"") !== tx.produit); if(conflictProduct){ alert(`Conflit: même N° reçu avec produit différent (${conflictProduct.produit}).`); addAudit("TX_VALIDATE_BLOCK", `recu_conflit_produit=${receipt}; produit=${tx.produit}`); return false; } if(!isEdit){ if(!confirm(`N° reçu '${tx.receipt}' déjà موجود لنفس العميل في نفس اليوم. Continuer ?`)){ addAudit("TX_VALIDATE_CANCEL", `recu_existant=${receipt}`); return false; } } } }else if(tx.mode !== "Espèces"){ if(!confirm("N° reçu vide pour un mode non espèces. Continuer ?")){ addAudit("TX_VALIDATE_CANCEL", `recu_vide_mode=${tx.mode}`); return false; } } if(tx.amount > TX_MAX_AMOUNT_DZD){ alert(`Montant trop élevé. Limite autorisée: ${fmt(TX_MAX_AMOUNT_DZD)} DZD.`); addAudit("TX_VALIDATE_BLOCK", `montant_hors_limite=${fmt(tx.amount)}`); return false; } if(tx.amount >= TX_WARN_AMOUNT_DZD){ if(!confirm(`Montant élevé (${fmt(tx.amount)} DZD). Confirmer l'enregistrement ?`)){ addAudit("TX_VALIDATE_CANCEL", `montant_eleve=${fmt(tx.amount)}`); return false; } } if(tx.date !== tx.day){ if(!confirm(`Date opération (${tx.date}) différente du jour actif (${tx.day}). Continuer ?`)){ addAudit("TX_VALIDATE_CANCEL", `date_diff_jour=${tx.date}`); return false; } } const similar = peers.filter(t=> Number(t.clientId||0) === tx.clientId && String(t.produit||"") === tx.produit && String(t.mode||"") === tx.mode && round2(Number(t.amount)||0) === round2(tx.amount) ); if(similar.length){ const txM = parseHHMMToMinutes(tx.time); const near = similar.some(t=>{ const m = parseHHMMToMinutes(t.time); if(txM < 0 || m < 0) return false; return Math.abs(m - txM) <= TX_DUP_TIME_WINDOW_MIN; }); if(near){ if(!confirm("Opération très مشابهة لنفس العميل/المبلغ خلال دقائق. Continuer ?")){ addAudit("TX_VALIDATE_CANCEL", `doublon_potentiel_client=${tx.clientId}; montant=${fmt(tx.amount)}`); return false; } } } return true; } function refreshClientFormMode(){ const saveBtn = document.getElementById("saveClientBtn"); const cancelBtn = document.getElementById("cancelClientEditBtn"); const idInput = document.getElementById("clientId"); const nameInput = document.getElementById("clientName"); if(!saveBtn || !idInput || !nameInput) return; const id = asPositiveInt(idInput.value); const exists = !!clientById(id); if(editClientId){ saveBtn.textContent = "Enregistrer modification"; }else if(exists){ saveBtn.textContent = "Modifier (ID existant)"; }else{ saveBtn.textContent = "Ajouter"; } if(cancelBtn){ cancelBtn.style.display = (editClientId || exists) ? "" : "none"; } } function syncClientFormFromId(){ const idInput = document.getElementById("clientId"); const nameInput = document.getElementById("clientName"); if(!idInput || !nameInput) return; const id = asPositiveInt(idInput.value); const found = clientById(id); if(!found){ if(editClientId && editClientId === id) editClientId = null; refreshClientFormMode(); return; } if(!String(nameInput.value||"").trim()){ nameInput.value = found.name; } refreshClientFormMode(); } function startClientEdit(id){ const found = clientById(id); if(!found) return; const idInput = document.getElementById("clientId"); const nameInput = document.getElementById("clientName"); if(!idInput || !nameInput) return; editClientId = found.id; idInput.value = String(found.id); nameInput.value = found.name; refreshClientFormMode(); nameInput.focus(); } function resetClientForm(){ const idInput = document.getElementById("clientId"); const nameInput = document.getElementById("clientName"); editClientId = null; if(idInput) idInput.value = ""; if(nameInput) nameInput.value = ""; refreshClientFormMode(); queueFormDraftSave(60); } function openClientModal(opts={}){ if(!requireCapability("manageData", "Ouvrir la fenêtre client")) return; const backdrop = document.getElementById("clientModalBackdrop"); const idEl = document.getElementById("clientModalId"); const nameEl = document.getElementById("clientModalName"); const titleEl = document.getElementById("clientModalTitle"); const hintEl = document.getElementById("clientModalHint"); if(!backdrop || !idEl || !nameEl || !titleEl || !hintEl) return; const rawId = asPositiveInt(opts.id || 0); const found = rawId ? clientById(rawId) : null; const baseId = found ? found.id : asPositiveInt(document.getElementById("clientId")?.value || 0); const baseName = found ? found.name : asSafeText(document.getElementById("clientName")?.value || "", 180); idEl.value = baseId ? String(baseId) : ""; nameEl.value = baseName || ""; titleEl.textContent = found ? `Modifier client #${found.id}` : "Ajouter un client"; hintEl.textContent = found ? "Modifiez puis enregistrez. Raccourci: Ctrl+S." : "Saisissez les informations du client puis enregistrez. Raccourci: Ctrl+S."; backdrop.style.display = "flex"; backdrop.setAttribute("aria-hidden", "false"); setTimeout(()=>{ if(nameEl.value) nameEl.select(); else idEl.focus(); }, 20); } function closeClientModal(){ const backdrop = document.getElementById("clientModalBackdrop"); if(!backdrop) return; backdrop.style.display = "none"; backdrop.setAttribute("aria-hidden", "true"); } function saveClientFromModal(){ if(!requireCapability("manageData", "Enregistrer client (fenêtre)")) return; if(!requirePin("Enregistrer un client")) return; const id = asPositiveInt(document.getElementById("clientModalId")?.value || 0); const name = asSafeText(document.getElementById("clientModalName")?.value || "", 180); if(!id || !name) return alert("Saisissez le N° et le nom du client."); if(!upsertClient(id, name)) return alert("Impossible d’enregistrer ce client."); editClientId = id; if(document.getElementById("clientId")) $("clientId").value = String(id); if(document.getElementById("clientName")) $("clientName").value = clientNameById(id); closeClientModal(); renderAll(); queueCriticalFullSave(); } function clearInlineTxEditState(){ editId = null; if(document.getElementById("saveTxBtn")) $("saveTxBtn").textContent = "Ajouter"; if(document.getElementById("cancelEditBtn")) $("cancelEditBtn").style.display = "none"; } function clearInlineDecEditState(){ editDecId = null; if(document.getElementById("saveDecBtn")) $("saveDecBtn").textContent = "Ajouter"; if(document.getElementById("cancelDecEditBtn")) $("cancelDecEditBtn").style.display = "none"; } function syncTxModalClientName(){ const idEl = document.getElementById("txModalClientId"); const nameEl = document.getElementById("txModalClientName"); if(!idEl || !nameEl) return; const id = asPositiveInt(idEl.value); nameEl.value = id ? clientNameById(id) : ""; } function openTxModal(opts={}){ if(!requireCapability("manageData", "Ouvrir la fenêtre encaissement")) return; const backdrop = document.getElementById("txModalBackdrop"); const titleEl = document.getElementById("txModalTitle"); const hintEl = document.getElementById("txModalHint"); const saveBtn = document.getElementById("txModalSaveBtn"); if(!backdrop || !titleEl || !hintEl || !saveBtn) return; const rawEditId = String(opts.id || "").trim(); const found = rawEditId ? state.tx.find(x=> String(x.id) === rawEditId) : null; txModalEditId = found ? found.id : null; clearInlineTxEditState(); const day = normalizeDate(state.params.day, nowDate()); const base = found ? { date: found.date, time: found.time, clientId: found.clientId, receipt: found.receipt || "", produit: found.produit, mode: found.mode, amount: round2(found.amount||0).toFixed(2), obs: found.obs || "" } : { date: normalizeDate(document.getElementById("txModalDate")?.value || document.getElementById("txDate")?.value || "", day), time: normalizeTime(document.getElementById("txModalTime")?.value || document.getElementById("txTime")?.value || "", nowTime()), clientId: asPositiveInt(document.getElementById("txModalClientId")?.value || document.getElementById("txClientId")?.value || 0), receipt: asSafeText(document.getElementById("txModalReceipt")?.value || document.getElementById("txReceipt")?.value || "", 80), produit: VALID_PRODUCTS.has(String(document.getElementById("txModalProduit")?.value || document.getElementById("txProduit")?.value || "")) ? String(document.getElementById("txModalProduit")?.value || document.getElementById("txProduit")?.value || "EMP") : "EMP", mode: VALID_TX_MODES.has(String(document.getElementById("txModalMode")?.value || document.getElementById("txMode")?.value || "")) ? String(document.getElementById("txModalMode")?.value || document.getElementById("txMode")?.value || "Espèces") : "Espèces", amount: asSafeText(document.getElementById("txModalAmount")?.value || document.getElementById("txAmount")?.value || "", 40), obs: asSafeText(document.getElementById("txModalObs")?.value || document.getElementById("txObs")?.value || "", 240) }; if(document.getElementById("txModalDate")) $("txModalDate").value = base.date; if(document.getElementById("txModalTime")) $("txModalTime").value = base.time; if(document.getElementById("txModalClientId")) $("txModalClientId").value = base.clientId ? String(base.clientId) : ""; if(document.getElementById("txModalReceipt")) $("txModalReceipt").value = base.receipt; if(document.getElementById("txModalProduit")) $("txModalProduit").value = base.produit; if(document.getElementById("txModalMode")) $("txModalMode").value = base.mode; if(document.getElementById("txModalAmount")) $("txModalAmount").value = base.amount; if(document.getElementById("txModalObs")) $("txModalObs").value = base.obs; syncTxModalClientName(); titleEl.textContent = found ? "Modifier encaissement" : "Ajouter encaissement"; hintEl.textContent = found ? "Mettez à jour les informations, puis enregistrez. Raccourcis: Ctrl+S / Ctrl+Entrée." : "Saisissez les informations de l’opération puis enregistrez. Raccourcis: Ctrl+S / Ctrl+Entrée."; saveBtn.textContent = found ? "Enregistrer les modifications" : "Ajouter"; backdrop.style.display = "flex"; // Restore persisted drafts/sticky fields when opening the modal setTimeout(()=>{ try{ restoreFormDraftPayload(); restoreStickyInputs(); }catch(e){} }, 0); backdrop.setAttribute("aria-hidden", "false"); setTimeout(()=>{ const first = document.getElementById("txModalClientId"); if(first) first.focus(); }, 20); } function closeTxModal(){ const backdrop = document.getElementById("txModalBackdrop"); if(!backdrop) return; backdrop.style.display = "none"; backdrop.setAttribute("aria-hidden", "true"); txModalEditId = null; } function saveTxFromModal(){ if(!requireCapability("manageData", "Enregistrer une opération (fenêtre)")) return; const day = normalizeDate(state.params.day, nowDate()); if(isClosed(day)) return alert("Ce jour est clôturé. Changez la date ou ouvrez un nouveau jour."); const clientId = asPositiveInt(document.getElementById("txModalClientId")?.value || 0); const clientName = asSafeText(clientNameById(clientId), 180); const amount = parseAmount(document.getElementById("txModalAmount")?.value || 0); if(!clientId) return alert("Saisissez le N° client."); if(!clientName) return alert("Client introuvable dans la liste."); if(amount <= 0) return alert("Saisissez un montant valide."); const produit = VALID_PRODUCTS.has(String(document.getElementById("txModalProduit")?.value || "")) ? document.getElementById("txModalProduit").value : "EMP"; const mode = VALID_TX_MODES.has(String(document.getElementById("txModalMode")?.value || "")) ? document.getElementById("txModalMode").value : "Espèces"; const tx = { day, date: normalizeDate(document.getElementById("txModalDate")?.value || "", day), time: normalizeTime(document.getElementById("txModalTime")?.value || "", nowTime()), clientId, clientName, receipt: asSafeText(document.getElementById("txModalReceipt")?.value || "", 80), produit, mode, amount: round2(amount), obs: asSafeText(document.getElementById("txModalObs")?.value || "", 240) }; if(!validateTxBeforeSave(tx, { isEdit: !!txModalEditId, editTxId: txModalEditId })){ return; } if(tx.mode === "Espèces"){ const existing = txModalEditId ? state.tx.find(x=>x.id===txModalEditId) : null; const den = existing && existing.cashDenoms ? cleanDenoms(existing.cashDenoms) : cleanDenoms(formCashCalcState.tx); if(Object.keys(den).length) tx.cashDenoms = den; } if(txModalEditId){ if(!requirePin("Modifier l’opération")) return; if(!updateTx(txModalEditId, tx)) return alert("Échec de la modification."); }else{ tx.id = makeId("tx"); if(!addTx(tx)) return alert("Échec de l'enregistrement."); } closeTxModal(); txPage = 1; renderAll(); queueCriticalFullSave(); } function openDecModal(opts={}){ if(!requireCapability("manageData", "Ouvrir la fenêtre décaissement")) return; const backdrop = document.getElementById("decModalBackdrop"); const titleEl = document.getElementById("decModalTitle"); const hintEl = document.getElementById("decModalHint"); const saveBtn = document.getElementById("decModalSaveBtn"); if(!backdrop || !titleEl || !hintEl || !saveBtn) return; const rawEditId = String(opts.id || "").trim(); const found = rawEditId ? state.dec.find(x=> String(x.id) === rawEditId) : null; decModalEditId = found ? found.id : null; clearInlineDecEditState(); const day = normalizeDate(state.params.day, nowDate()); const base = found ? { date: found.date, time: found.time, label: found.label, mode: found.mode, amount: round2(found.amount||0).toFixed(2), obs: found.obs || "" } : { date: normalizeDate(document.getElementById("decDate")?.value || "", day), time: normalizeTime(document.getElementById("decTime")?.value || "", nowTime()), label: asSafeText(document.getElementById("decLabel")?.value || "", 220), mode: VALID_DEC_MODES.has(String(document.getElementById("decMode")?.value || "")) ? document.getElementById("decMode").value : "Espèces", amount: asSafeText(document.getElementById("decAmount")?.value || "", 40), obs: asSafeText(document.getElementById("decObs")?.value || "", 240) }; if(document.getElementById("decModalDate")) $("decModalDate").value = base.date; if(document.getElementById("decModalTime")) $("decModalTime").value = base.time; if(document.getElementById("decModalLabel")) $("decModalLabel").value = base.label; if(document.getElementById("decModalMode")) $("decModalMode").value = base.mode; if(document.getElementById("decModalAmount")) $("decModalAmount").value = base.amount; if(document.getElementById("decModalObs")) $("decModalObs").value = base.obs; titleEl.textContent = found ? "Modifier décaissement" : "Ajouter décaissement"; hintEl.textContent = found ? "Mettez à jour les informations, puis enregistrez. Raccourcis: Ctrl+S / Ctrl+Entrée." : "Saisissez les informations de la sortie puis enregistrez. Raccourcis: Ctrl+S / Ctrl+Entrée."; saveBtn.textContent = found ? "Enregistrer les modifications" : "Ajouter"; backdrop.style.display = "flex"; backdrop.setAttribute("aria-hidden", "false"); setTimeout(()=>{ const first = document.getElementById("decModalLabel"); if(first) first.focus(); }, 20); } function closeDecModal(){ const backdrop = document.getElementById("decModalBackdrop"); if(!backdrop) return; backdrop.style.display = "none"; backdrop.setAttribute("aria-hidden", "true"); decModalEditId = null; } function saveDecFromModal(){ if(!requireCapability("manageData", "Enregistrer un décaissement (fenêtre)")) return; const day = normalizeDate(state.params.day, nowDate()); if(isClosed(day)) return alert("Ce jour est clôturé. Changez la date ou ouvrez un nouveau jour."); const label = asSafeText(document.getElementById("decModalLabel")?.value || "", 220); const amount = parseAmount(document.getElementById("decModalAmount")?.value || 0); if(!label) return alert("Saisissez le libellé / motif."); if(amount <= 0) return alert("Saisissez un montant valide."); const mode = VALID_DEC_MODES.has(String(document.getElementById("decModalMode")?.value || "")) ? document.getElementById("decModalMode").value : "Espèces"; const dec = { day, date: normalizeDate(document.getElementById("decModalDate")?.value || "", day), time: normalizeTime(document.getElementById("decModalTime")?.value || "", nowTime()), label, mode, amount: round2(amount), obs: asSafeText(document.getElementById("decModalObs")?.value || "", 240) }; if(dec.mode === "Espèces"){ const existing = decModalEditId ? state.dec.find(x=>x.id===decModalEditId) : null; const den = existing && existing.cashDenoms ? cleanDenoms(existing.cashDenoms) : cleanDenoms(formCashCalcState.dec); if(Object.keys(den).length) dec.cashDenoms = den; } if(decModalEditId){ if(!requirePin("Modifier le décaissement")) return; if(!updateDec(decModalEditId, dec)) return alert("Échec de la modification."); }else{ dec.id = makeId("dec"); if(!addDec(dec)) return alert("Échec de l'enregistrement."); } closeDecModal(); decPage = 1; renderAll(); queueCriticalFullSave(); } function isModalVisible(id){ const el = document.getElementById(id); return !!(el && el.style.display === "flex"); } function focusPrimaryModalField(kind){ if(kind === "client"){ const el = document.getElementById("clientModalName") || document.getElementById("clientModalId"); if(el) el.focus(); return; } if(kind === "tx"){ const el = document.getElementById("txModalClientId"); if(el) el.focus(); return; } if(kind === "dec"){ const el = document.getElementById("decModalLabel"); if(el) el.focus(); return; } if(kind === "logo"){ const el = document.getElementById("logoEditorRotation"); if(el) el.focus(); } } /* TX */ function txOfDay(day){ return state.tx.filter(t=>t.day===day); } function addTx(t){ const row = normalizeTx([t], state.clients)[0]; if(!row) return false; captureUndo(`Ajout encaissement #${row.clientId}`); state.tx.unshift(row); if(!save()) return false; addAudit("TX_ADD", `id=${row.id} client=${row.clientId} montant=${fmt(row.amount)}`); return true; } function updateTx(id, patch){ const i=state.tx.findIndex(t=>t.id===id); if(i<0) return false; const merged = { ...state.tx[i], ...patch, id: state.tx[i].id }; const row = normalizeTx([merged], state.clients)[0]; if(!row) return false; captureUndo(`Modification encaissement ${id}`); row.id = state.tx[i].id; state.tx[i]=row; if(!save()) return false; addAudit("TX_UPDATE", `id=${row.id} montant=${fmt(row.amount)}`); return true; } function deleteTx(id){ if(!state.tx.some(t=>t.id===id)) return; captureUndo(`Suppression encaissement ${id}`); state.tx = state.tx.filter(t=>t.id!==id); if(!save()) return; addAudit("TX_DELETE", `id=${id}`); } function clearDayTx(day){ const before = state.tx.length; const next = state.tx.filter(t=>t.day!==day); const removed = before - next.length; if(removed <= 0) return; captureUndo(`Réinitialisation encaissements ${day}`); state.tx = next; if(!save()) return; addAudit("TX_CLEAR_DAY", `jour=${day} supprimés=${removed}`); } // DEC function decOfDay(day){ return state.dec.filter(d=>d.day===day); } function addDec(d){ const row = normalizeDec([d])[0]; if(!row) return false; captureUndo(`Ajout décaissement ${row.label}`); state.dec.unshift(row); if(!save()) return false; addAudit("DEC_ADD", `id=${row.id} montant=${fmt(row.amount)}`); return true; } function updateDec(id, patch){ const i=state.dec.findIndex(x=>x.id===id); if(i<0) return false; const merged = { ...state.dec[i], ...patch, id: state.dec[i].id }; const row = normalizeDec([merged])[0]; if(!row) return false; captureUndo(`Modification décaissement ${id}`); row.id = state.dec[i].id; state.dec[i]=row; if(!save()) return false; addAudit("DEC_UPDATE", `id=${row.id} montant=${fmt(row.amount)}`); return true; } function deleteDec(id){ if(!state.dec.some(x=>x.id===id)) return; captureUndo(`Suppression décaissement ${id}`); state.dec = state.dec.filter(x=>x.id!==id); if(!save()) return; addAudit("DEC_DELETE", `id=${id}`); } function clearDayDec(day){ const before = state.dec.length; const next = state.dec.filter(x=>x.day!==day); const removed = before - next.length; if(removed <= 0) return; captureUndo(`Réinitialisation décaissements ${day}`); state.dec = next; if(!save()) return; addAudit("DEC_CLEAR_DAY", `jour=${day} supprimés=${removed}`); } let editId=null; let editDecId=null; let editClientId=null; let txModalEditId=null; let decModalEditId=null; /* Deposits */ function computeDeposits(day){ const list=txOfDay(day); let cashTotal=0, card=0, cheque=0, beaCash=0, cpaCash=0; for(const t of list){ const a=Number(t.amount)||0; if(t.mode==="Espèces"){ cashTotal += a; if(t.produit==="EMP") beaCash += a; else if(t.produit==="LCA") cpaCash += a; } else if(t.mode==="Carte") card += a; else if(t.mode==="Chèque") cheque += a; } const decs = decOfDay(day); const decCash = decs.filter(x=>x.mode==="Espèces").reduce((s,x)=> s + (Number(x.amount)||0), 0); const decAll = decs.reduce((s,x)=> s + (Number(x.amount)||0), 0); const fond = (document.getElementById("fond") ? Math.max(0, round2(parseAmount($("fond").value))) : (Number(state.params.fond)||0)); // Montants à verser // - BEA (EMP) = TOTAL ENCAISSEMENTS (Espèces) EMP // - CPA (LCA) = TOTAL ENCAISSEMENTS (Espèces) LCA const depBEA = Math.max(0, beaCash); const depCPA = Math.max(0, cpaCash); return {cashTotal, card, cheque, all:cashTotal+card+cheque, depBEA, depCPA, fond, decCash, decAll}; } /* Recon (Comptage caisse) */ function getRecon(day){ if(!state.recon) state.recon = {}; if(!state.recon[day]) state.recon[day] = { drawerCount:"", note:"", savedAt:"", rendu:"" }; return state.recon[day]; } function fmtSigned(n){ const v = Number(n)||0; const sign = v>0 ? "+" : (v<0 ? "−" : ""); return sign + fmt(Math.abs(v)); } function renderRecon(day, expected){ if(!document.getElementById("drawerCount")) return; const r = (state.recon && state.recon[day]) ? state.recon[day] : {drawerCount:"", note:"", rendu:""}; $("drawerCount").value = (r.drawerCount ?? ""); $("drawerNote").value = (r.note ?? ""); // Rendu (enregistré par jour) if(document.getElementById("renduAmount")){ $("renduAmount").value = (r.rendu ?? ""); } updateReconDiff(expected); } function updateReconDiff(expected){ if(!document.getElementById("drawerDiff")) return; const counted = parseAmount($("drawerCount").value); const diff = counted - (Number(expected)||0); $("drawerDiff").textContent = fmtSigned(diff); } /* Print helpers: strip duplicate ids in clones */ function stripIds(node){ try{ if(node && node.removeAttribute) node.removeAttribute("id"); if(node && node.querySelectorAll){ node.querySelectorAll("[id]").forEach(el=> el.removeAttribute("id")); } }catch(e){} } function readUiSettingsFromState(){ return normalizeUiSettings(state?.params || {}, UI_DEFAULTS); } function readUiSettingsFromUi(fallback){ const base = { ...(fallback || readUiSettingsFromState()) }; const readInt = (id, min, max, fb)=>{ const el = document.getElementById(id); if(!el) return fb; return Math.round(clampNum(el.value, min, max, fb)); }; base.uiBaseFontSize = readInt("uiBaseFontSize", 12, 18, base.uiBaseFontSize); base.uiButtonFontSize = readInt("uiButtonFontSize", 11, 18, base.uiButtonFontSize); base.uiButtonPadY = readInt("uiButtonPadY", 6, 16, base.uiButtonPadY); base.uiButtonPadX = readInt("uiButtonPadX", 8, 24, base.uiButtonPadX); base.uiButtonRadius = readInt("uiButtonRadius", 6, 20, base.uiButtonRadius); base.uiFieldFontSize = readInt("uiFieldFontSize", 12, 18, base.uiFieldFontSize); base.uiFieldPadY = readInt("uiFieldPadY", 6, 14, base.uiFieldPadY); base.uiFieldRadius = readInt("uiFieldRadius", 6, 18, base.uiFieldRadius); base.uiNavFontSize = readInt("uiNavFontSize", 11, 18, base.uiNavFontSize); base.uiNavItemMinHeight = readInt("uiNavItemMinHeight", 34, 60, base.uiNavItemMinHeight); base.uiNavWidth = readInt("uiNavWidth", 240, 420, base.uiNavWidth); base.uiTabFontSize = readInt("uiTabFontSize", 11, 18, base.uiTabFontSize); return base; } function applyUiSettings(settings){ const s = settings || readUiSettingsFromState(); const root = document.documentElement; if(!root || !root.style) return; root.style.setProperty("--ui-body-font-size", `${s.uiBaseFontSize}px`); root.style.setProperty("--btn-font-size", `${s.uiButtonFontSize}px`); root.style.setProperty("--btn-py", `${s.uiButtonPadY}px`); root.style.setProperty("--btn-px", `${s.uiButtonPadX}px`); root.style.setProperty("--btn-radius", `${s.uiButtonRadius}px`); root.style.setProperty("--field-font-size", `${s.uiFieldFontSize}px`); root.style.setProperty("--field-py", `${s.uiFieldPadY}px`); root.style.setProperty("--field-radius", `${s.uiFieldRadius}px`); root.style.setProperty("--tab-font-size", `${s.uiTabFontSize}px`); root.style.setProperty("--nav-link-font-size", `${s.uiNavFontSize}px`); root.style.setProperty("--nav-link-min-h", `${s.uiNavItemMinHeight}px`); root.style.setProperty("--focus-nav-w", `${s.uiNavWidth}px`); } function resetUiSettingsUiValues(){ const setIf = (id, val)=>{ const el = document.getElementById(id); if(el) el.value = String(val); }; setIf("uiBaseFontSize", UI_DEFAULTS.uiBaseFontSize); setIf("uiButtonFontSize", UI_DEFAULTS.uiButtonFontSize); setIf("uiButtonPadY", UI_DEFAULTS.uiButtonPadY); setIf("uiButtonPadX", UI_DEFAULTS.uiButtonPadX); setIf("uiButtonRadius", UI_DEFAULTS.uiButtonRadius); setIf("uiFieldFontSize", UI_DEFAULTS.uiFieldFontSize); setIf("uiFieldPadY", UI_DEFAULTS.uiFieldPadY); setIf("uiFieldRadius", UI_DEFAULTS.uiFieldRadius); setIf("uiNavFontSize", UI_DEFAULTS.uiNavFontSize); setIf("uiNavItemMinHeight", UI_DEFAULTS.uiNavItemMinHeight); setIf("uiNavWidth", UI_DEFAULTS.uiNavWidth); setIf("uiTabFontSize", UI_DEFAULTS.uiTabFontSize); } function saveUiSettingsOnly(opts={}){ if(!requireCapability("manageConfig", "Sauvegarder interface PRO")) return false; if(opts.requirePin === true && !requirePin("Sauvegarder interface PRO")){ if(opts.alert !== false) alert("Sauvegarde interface annulée (PIN)."); return false; } const ui = readUiSettingsFromUi(readUiSettingsFromState()); captureUndo(`Sauvegarde interface PRO (${state.params.day||"—"})`); state.params.uiBaseFontSize = ui.uiBaseFontSize; state.params.uiButtonFontSize = ui.uiButtonFontSize; state.params.uiButtonPadY = ui.uiButtonPadY; state.params.uiButtonPadX = ui.uiButtonPadX; state.params.uiButtonRadius = ui.uiButtonRadius; state.params.uiFieldFontSize = ui.uiFieldFontSize; state.params.uiFieldPadY = ui.uiFieldPadY; state.params.uiFieldRadius = ui.uiFieldRadius; state.params.uiNavFontSize = ui.uiNavFontSize; state.params.uiNavItemMinHeight = ui.uiNavItemMinHeight; state.params.uiNavWidth = ui.uiNavWidth; state.params.uiTabFontSize = ui.uiTabFontSize; if(!save()) return false; applyUiSettings(ui); touchSessionActivity({force:true}); renderSessionStatus(); addAudit("UI_PARAMS_SAVE", `btn=${ui.uiButtonFontSize}px nav=${ui.uiNavFontSize}px width=${ui.uiNavWidth}px`); if(opts.render !== false) renderAll(); if(opts.alert !== false) alert("Interface PRO sauvegardée."); return true; } function saveLogoSettingsOnly(opts={}){ if(!requireCapability("manageConfig", "Sauvegarder logo")) return false; if(opts.requirePin === true && !requirePin("Sauvegarder logo")){ if(opts.alert !== false) alert("Sauvegarde logo annulée (PIN)."); return false; } const logo = readLogoSettingsFromUi(readLogoSettingsFromState()); const nextLogoSrc = (logoDraftSrc !== null) ? sanitizeLogoSrc(logoDraftSrc) : sanitizeLogoSrc(state?.params?.logoSrc); captureUndo(`Sauvegarde logo (${state.params.day||"—"})`); state.params.logoSize = logo.logoSize; state.params.logoOffsetX = logo.logoOffsetX; state.params.logoOffsetY = logo.logoOffsetY; state.params.logoClarity = logo.logoClarity; state.params.logoOpacity = logo.logoOpacity; state.params.printLogoSize = logo.printLogoSize; state.params.printLogoOffsetX = logo.printLogoOffsetX; state.params.printLogoOffsetY = logo.printLogoOffsetY; state.params.printLogoOpacity = logo.printLogoOpacity; state.params.watermarkScale = logo.watermarkScale; state.params.watermarkOffsetX = logo.watermarkOffsetX; state.params.watermarkOffsetY = logo.watermarkOffsetY; state.params.watermarkOpacity = logo.watermarkOpacity; state.params.logoSrc = nextLogoSrc; if(!save()) return false; logoDraftSrc = null; logoDraftFileName = ""; if(document.getElementById("logoFileInput")) $("logoFileInput").value = ""; touchSessionActivity({force:true}); renderSessionStatus(); addAudit("LOGO_PARAMS_SAVE", `logo_custom=${state.params.logoSrc ? "oui" : "non"}; size=${logo.logoSize}px; print=${logo.printLogoSize}px`); if(opts.render !== false){ renderAll(); }else{ applyLogoFromUi(); } if(opts.alert !== false) alert("Logo sauvegardé."); return true; } function readLogoSettingsFromState(){ return normalizeLogoSettings(state?.params || {}, LOGO_DEFAULTS); } function readLogoSources({useDraft=true}={}){ const draft = useDraft ? sanitizeLogoSrc(logoDraftSrc) : ""; const saved = sanitizeLogoSrc(state?.params?.logoSrc); const custom = draft || saved; const topSrc = custom || DEFAULT_BRAND_LOGO_SRC || DEFAULT_PRINT_LOGO_SRC || "icon.png"; const printSrc = custom || DEFAULT_PRINT_LOGO_SRC || topSrc; return { custom, topSrc, printSrc }; } function readLogoSettingsFromUi(fallback){ const base = { ...(fallback || readLogoSettingsFromState()) }; const readInt = (id, min, max, fb)=>{ const el = document.getElementById(id); if(!el) return fb; return Math.round(clampNum(el.value, min, max, fb)); }; base.logoSize = readInt("logoSize", 28, 120, base.logoSize); base.logoOffsetX = readInt("logoOffsetX", -60, 60, base.logoOffsetX); base.logoOffsetY = readInt("logoOffsetY", -40, 40, base.logoOffsetY); base.logoClarity = readInt("logoClarity", 60, 200, base.logoClarity); base.logoOpacity = readInt("logoOpacity", 30, 100, base.logoOpacity); base.printLogoSize = readInt("printLogoSize", 24, 120, base.printLogoSize); base.printLogoOffsetX = readInt("printLogoOffsetX", -80, 80, base.printLogoOffsetX); base.printLogoOffsetY = readInt("printLogoOffsetY", -80, 80, base.printLogoOffsetY); base.printLogoOpacity = readInt("printLogoOpacity", 20, 100, base.printLogoOpacity); base.watermarkScale = readInt("watermarkScale", 40, 220, base.watermarkScale); base.watermarkOffsetX = readInt("watermarkOffsetX", -220, 220, base.watermarkOffsetX); base.watermarkOffsetY = readInt("watermarkOffsetY", -220, 220, base.watermarkOffsetY); base.watermarkOpacity = readInt("watermarkOpacity", 2, 60, base.watermarkOpacity); return base; } function logoFilterFromClarity(clarityPct){ const clarity = clampNum(clarityPct, 60, 200, 100) / 100; const sat = Math.min(2.4, Math.max(0.8, clarity + 0.2)); const bright = clarity >= 1 ? 1 + ((clarity - 1) * 0.08) : 1 - ((1 - clarity) * 0.08); return `contrast(${clarity.toFixed(2)}) saturate(${sat.toFixed(2)}) brightness(${bright.toFixed(2)})`; } function applyLogoVisuals(settings, sources){ const s = settings || readLogoSettingsFromState(); const src = sources || readLogoSources({useDraft:true}); const logoFilter = logoFilterFromClarity(s.logoClarity); const topLogo = document.querySelector(".brandbox img"); if(topLogo){ topLogo.src = src.topSrc; topLogo.style.height = `${s.logoSize}px`; topLogo.style.width = "auto"; topLogo.style.opacity = String(clampNum(s.logoOpacity, 30, 100, 100) / 100); topLogo.style.transform = `translate(${s.logoOffsetX}px, ${s.logoOffsetY}px)`; topLogo.style.filter = logoFilter; topLogo.onerror = ()=>{ topLogo.onerror = null; topLogo.src = DEFAULT_BRAND_LOGO_SRC || DEFAULT_PRINT_LOGO_SRC || "icon.png"; }; } document.querySelectorAll("#printBord .print-logo").forEach((img)=>{ img.src = src.printSrc; img.style.setProperty("height", `${s.printLogoSize}px`, "important"); img.style.setProperty("width", "auto", "important"); img.style.opacity = String(clampNum(s.printLogoOpacity, 20, 100, 100) / 100); img.style.transform = `translate(${s.printLogoOffsetX}px, ${s.printLogoOffsetY}px)`; img.style.filter = logoFilter; img.onerror = ()=>{ img.onerror = null; img.src = DEFAULT_PRINT_LOGO_SRC || DEFAULT_BRAND_LOGO_SRC || "icon.png"; }; }); const wmWidth = Math.round(420 * (clampNum(s.watermarkScale, 40, 220, 100) / 100)); document.querySelectorAll("#printBord .watermark").forEach((img)=>{ img.src = src.printSrc; img.style.width = `${wmWidth}px`; img.style.height = "auto"; img.style.opacity = String(clampNum(s.watermarkOpacity, 2, 60, 20) / 100); img.style.transform = `translate(calc(-50% + ${s.watermarkOffsetX}px), calc(-50% + ${s.watermarkOffsetY}px))`; img.style.filter = logoFilter; img.onerror = ()=>{ img.onerror = null; img.src = DEFAULT_PRINT_LOGO_SRC || DEFAULT_BRAND_LOGO_SRC || "icon.png"; }; }); const preview = document.getElementById("logoPreview"); if(preview){ preview.src = src.topSrc; preview.style.height = `${Math.max(34, Math.round(s.logoSize * 1.1))}px`; preview.style.opacity = String(clampNum(s.logoOpacity, 30, 100, 100) / 100); preview.style.transform = `translate(${s.logoOffsetX}px, ${s.logoOffsetY}px)`; preview.style.filter = logoFilter; } } function renderLogoHint(){ const hint = document.getElementById("logoHint"); if(!hint) return; const hasSavedCustom = !!sanitizeLogoSrc(state?.params?.logoSrc); const hasDraft = logoDraftSrc !== null; const action = hasDraft ? "Modifications logo en attente de sauvegarde." : "Réglages logo chargés."; if(hasDraft && logoDraftFileName){ hint.textContent = `${action} Fichier: ${logoDraftFileName}. Cliquez "Enregistrer" pour valider.`; return; } if(hasDraft && sanitizeLogoSrc(logoDraftSrc) === ""){ hint.textContent = `${action} Le logo personnalisé sera supprimé après enregistrement.`; return; } hint.textContent = hasSavedCustom ? `${action} Logo personnalisé actif.` : `${action} Logo par défaut actif.`; } function applyLogoFromUi(){ const settings = readLogoSettingsFromUi(readLogoSettingsFromState()); const sources = readLogoSources({useDraft:true}); applyLogoVisuals(settings, sources); renderLogoHint(); renderLogoBordPreview(); } function fillLogoBordPreviewNode(root, bankKey){ if(!root) return; const day = normalizeDate(state?.params?.day, nowDate()); const bank = bankKey === "CPA" ? "CPA" : "BEA"; ensureAutoDenoms(day, bank); const q = (id)=> root.querySelector(`#${id}`); const setText = (id, value)=>{ const el = q(id); if(el) el.textContent = String(value ?? ""); }; const amount = bordereauAmount(day, bank); const no = currentBordNo(day, bank); const color = bankColor(bank); const bar = q("p_bar"); if(bar) bar.style.background = color; setText("p_bank", bankName(bank)); setText("p_bank_code", bank); setText("p_rib", bankRib(bank)); setText("p_day", day); setText("p_no", no); setText("p_amount_label", bank==="BEA" ? "MONTANT A VERSER BEA (EMP):" : "MONTANT A VERSER CPA (LCA):"); setText("p_amount", fmt(amount)); setText("p_words", amountToWordsDZD(amount)); setText("p_company_info", getCompanyInfoText()); try{ const ex = getBordExtra(day, bank); const fondVal = (ex && ex.fond!=null) ? Number(ex.fond)||0 : Math.max(0, round2(parseAmount($("fond")?.value))); const renduVal = (ex && ex.rendu!=null) ? Number(ex.rendu)||0 : Math.max(0, round2(parseAmount($("renduAmount")?.value))); setText("p_fond", fmt(fondVal)); setText("p_rendu", fmt(renduVal)); }catch(e){} const den = ensureAutoDenoms(day, bank); const denRows = DENOMS.filter(d=> Number(den[d.key]||0) > 0).map((d)=>{ const qty = Number(den[d.key]||0); const sub = qty * d.value; const label = String(d.value); return `${d.type}${label}${qty}${fmt(sub)}`; }).join(""); const denBody = q("p_denoms"); if(denBody) denBody.innerHTML = denRows || `—`; setText("p_den_total", fmt(denomTotal(day, bank))); const prod = bankProduct(bank); let allTx = txOfDay(day).filter(t=> t.mode==="Espèces" && t.produit===prod); allTx = allTx.slice().sort((a,b)=> String(a.time||"").localeCompare(String(b.time||""))); const txBody = q("p_txs"); if(txBody){ const grouped = groupCashTxByClient(allTx); txBody.innerHTML = grouped.map((t, i)=>` ${i+1} ${t.clientId} ${escapeHtml(t.clientName)} ${fmt(t.amount)} `).join("") || `—`; const totalTx = grouped.reduce((s,t)=> s + (Number(t.amount)||0), 0); setText("p_txs_total", fmt(totalTx)); setText("p_txs_count", String(grouped.length)); } const s = readLogoSettingsFromUi(readLogoSettingsFromState()); const src = readLogoSources({useDraft:true}); const logoFilter = logoFilterFromClarity(s.logoClarity); root.querySelectorAll(".print-logo").forEach((img)=>{ img.src = src.printSrc; img.style.height = `${s.printLogoSize}px`; img.style.width = "auto"; img.style.opacity = String(clampNum(s.printLogoOpacity, 20, 100, 100) / 100); img.style.transform = `translate(${s.printLogoOffsetX}px, ${s.printLogoOffsetY}px)`; img.style.filter = logoFilter; }); const wmWidth = Math.round(420 * (clampNum(s.watermarkScale, 40, 220, 100) / 100)); root.querySelectorAll(".watermark").forEach((img)=>{ img.src = src.printSrc; img.style.width = `${wmWidth}px`; img.style.height = "auto"; img.style.opacity = String(clampNum(s.watermarkOpacity, 2, 60, 20) / 100); img.style.transform = `translate(calc(-50% + ${s.watermarkOffsetX}px), calc(-50% + ${s.watermarkOffsetY}px))`; img.style.filter = logoFilter; }); } function fitLogoBordPreviewScale(){ const mount = document.getElementById("logoBordPreviewMount"); const scaleWrap = mount ? mount.querySelector(".logo-bord-preview-scale") : null; const page = scaleWrap ? scaleWrap.querySelector(".page") : null; if(!mount || !scaleWrap || !page) return; scaleWrap.style.transform = "none"; let pageWidth = page.getBoundingClientRect().width; let pageHeight = page.getBoundingClientRect().height; if(!pageWidth || !pageHeight){ pageWidth = 190 * 3.7795; pageHeight = 277 * 3.7795; } const availableWidth = Math.max(220, mount.clientWidth - 8); const scale = Math.min(1, availableWidth / pageWidth); scaleWrap.style.width = `${Math.round(pageWidth)}px`; scaleWrap.style.height = `${Math.round(pageHeight)}px`; scaleWrap.style.transform = `scale(${scale})`; mount.style.height = `${Math.max(180, Math.ceil(pageHeight * scale) + 8)}px`; } function renderLogoBordPreview(){ const mount = document.getElementById("logoBordPreviewMount"); const source = document.getElementById("printBord"); if(!mount || !source) return; const bank = (document.getElementById("logoPreviewBank")?.value === "CPA") ? "CPA" : "BEA"; const clone = source.cloneNode(true); fillLogoBordPreviewNode(clone, bank); stripIds(clone); clone.classList.add("logo-bord-preview-page"); clone.style.display = ""; const scaleWrap = document.createElement("div"); scaleWrap.className = "logo-bord-preview-scale"; scaleWrap.appendChild(clone); mount.innerHTML = ""; mount.appendChild(scaleWrap); requestAnimationFrame(()=> fitLogoBordPreviewScale()); } function resetLogoSettingsUiValues(){ const setIf = (id, val)=>{ const el = document.getElementById(id); if(el) el.value = String(val); }; setIf("logoSize", LOGO_DEFAULTS.logoSize); setIf("logoOffsetX", LOGO_DEFAULTS.logoOffsetX); setIf("logoOffsetY", LOGO_DEFAULTS.logoOffsetY); setIf("logoClarity", LOGO_DEFAULTS.logoClarity); setIf("logoOpacity", LOGO_DEFAULTS.logoOpacity); setIf("printLogoSize", LOGO_DEFAULTS.printLogoSize); setIf("printLogoOffsetX", LOGO_DEFAULTS.printLogoOffsetX); setIf("printLogoOffsetY", LOGO_DEFAULTS.printLogoOffsetY); setIf("printLogoOpacity", LOGO_DEFAULTS.printLogoOpacity); setIf("watermarkScale", LOGO_DEFAULTS.watermarkScale); setIf("watermarkOffsetX", LOGO_DEFAULTS.watermarkOffsetX); setIf("watermarkOffsetY", LOGO_DEFAULTS.watermarkOffsetY); setIf("watermarkOpacity", LOGO_DEFAULTS.watermarkOpacity); } function fileToDataUrl(file){ return new Promise((resolve, reject)=>{ const reader = new FileReader(); reader.onload = ()=> resolve(String(reader.result || "")); reader.onerror = ()=> reject(new Error("read_failed")); reader.readAsDataURL(file); }); } async function handleLogoFileChange(input){ if(!requireCapability("manageConfig", "Charger un logo")) return; const file = input && input.files ? input.files[0] : null; if(!file) return; if(!String(file.type||"").toLowerCase().startsWith("image/")){ alert("Format non supporté. Utilisez une image PNG/JPG/WEBP/SVG."); input.value = ""; return; } if((Number(file.size)||0) > MAX_LOGO_FILE_SIZE_BYTES){ alert("Logo trop volumineux. Taille max recommandée: 2 Mo."); input.value = ""; return; } try{ const dataUrl = await fileToDataUrl(file); const safe = sanitizeLogoSrc(dataUrl); if(!safe){ alert("Impossible de charger ce logo."); input.value = ""; return; } logoDraftSrc = safe; logoDraftFileName = asSafeText(file.name || "logo", 140); applyLogoFromUi(); }catch(e){ alert("Erreur de lecture du fichier logo."); } } function normalizeLogoEditorState(){ logoEditorState.rotation = Math.round(clampNum(logoEditorState.rotation, -180, 180, LOGO_EDITOR_DEFAULTS.rotation)); logoEditorState.cropW = normalizePercentInt(logoEditorState.cropW, 5, 100, LOGO_EDITOR_DEFAULTS.cropW); logoEditorState.cropH = normalizePercentInt(logoEditorState.cropH, 5, 100, LOGO_EDITOR_DEFAULTS.cropH); logoEditorState.cropX = normalizePercentInt(logoEditorState.cropX, 0, 99, LOGO_EDITOR_DEFAULTS.cropX); logoEditorState.cropY = normalizePercentInt(logoEditorState.cropY, 0, 99, LOGO_EDITOR_DEFAULTS.cropY); if(logoEditorState.cropX + logoEditorState.cropW > 100){ logoEditorState.cropX = Math.max(0, 100 - logoEditorState.cropW); } if(logoEditorState.cropY + logoEditorState.cropH > 100){ logoEditorState.cropY = Math.max(0, 100 - logoEditorState.cropH); } } function resetLogoEditorStateValues(){ logoEditorState.rotation = LOGO_EDITOR_DEFAULTS.rotation; logoEditorState.cropX = LOGO_EDITOR_DEFAULTS.cropX; logoEditorState.cropY = LOGO_EDITOR_DEFAULTS.cropY; logoEditorState.cropW = LOGO_EDITOR_DEFAULTS.cropW; logoEditorState.cropH = LOGO_EDITOR_DEFAULTS.cropH; normalizeLogoEditorState(); } function setLogoEditorControlsFromState(){ const setIf = (id, value)=>{ const el = document.getElementById(id); if(el) el.value = String(value); }; setIf("logoEditorRotation", logoEditorState.rotation); setIf("logoEditorRotationNumber", logoEditorState.rotation); setIf("logoEditorCropX", logoEditorState.cropX); setIf("logoEditorCropY", logoEditorState.cropY); setIf("logoEditorCropW", logoEditorState.cropW); setIf("logoEditorCropH", logoEditorState.cropH); } function readLogoEditorControlsToState(){ const readInt = (id, min, max, fallback)=>{ const el = document.getElementById(id); if(!el) return fallback; return Math.round(clampNum(el.value, min, max, fallback)); }; logoEditorState.rotation = readInt("logoEditorRotation", -180, 180, logoEditorState.rotation); const rotNum = document.getElementById("logoEditorRotationNumber"); if(rotNum){ logoEditorState.rotation = Math.round(clampNum(rotNum.value, -180, 180, logoEditorState.rotation)); } logoEditorState.cropX = readInt("logoEditorCropX", 0, 99, logoEditorState.cropX); logoEditorState.cropY = readInt("logoEditorCropY", 0, 99, logoEditorState.cropY); logoEditorState.cropW = readInt("logoEditorCropW", 5, 100, logoEditorState.cropW); logoEditorState.cropH = readInt("logoEditorCropH", 5, 100, logoEditorState.cropH); normalizeLogoEditorState(); } function logoEditorSetHint(message){ const hint = document.getElementById("logoEditorHint"); if(hint) hint.textContent = String(message || ""); } function logoEditorSetDimensionHint(message){ const dim = document.getElementById("logoEditorDimHint"); if(dim) dim.textContent = String(message || ""); } function logoEditorLoadImage(src){ return new Promise((resolve, reject)=>{ const image = new Image(); image.decoding = "async"; image.onload = ()=> resolve(image); image.onerror = ()=> reject(new Error("image_load_failed")); if(/^https?:\/\//i.test(String(src || ""))){ image.crossOrigin = "anonymous"; } image.src = String(src || ""); }); } function buildLogoEditorSourceAsset(image){ const maxSide = 2200; const srcW = Math.max(1, Math.round(Number(image?.naturalWidth || image?.width || 0))); const srcH = Math.max(1, Math.round(Number(image?.naturalHeight || image?.height || 0))); if(srcW <= maxSide && srcH <= maxSide){ return { source: image, width: srcW, height: srcH, scaled: false }; } const scale = Math.min(1, maxSide / Math.max(srcW, srcH)); const w = Math.max(1, Math.round(srcW * scale)); const h = Math.max(1, Math.round(srcH * scale)); const canvas = document.createElement("canvas"); canvas.width = w; canvas.height = h; const ctx = canvas.getContext("2d"); if(ctx){ ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "high"; ctx.drawImage(image, 0, 0, srcW, srcH, 0, 0, w, h); } return { source: canvas, width: w, height: h, scaled: true }; } function buildLogoEditorRotatedCanvas(sourceAsset, rotationDeg){ const source = sourceAsset?.source || null; const srcW = Math.max(1, Math.round(Number(sourceAsset?.width || 1))); const srcH = Math.max(1, Math.round(Number(sourceAsset?.height || 1))); if(!source) return null; const rad = (Number(rotationDeg)||0) * Math.PI / 180; const cos = Math.abs(Math.cos(rad)); const sin = Math.abs(Math.sin(rad)); const outW = Math.max(1, Math.round((srcW * cos) + (srcH * sin))); const outH = Math.max(1, Math.round((srcW * sin) + (srcH * cos))); const canvas = document.createElement("canvas"); canvas.width = outW; canvas.height = outH; const ctx = canvas.getContext("2d"); if(!ctx) return canvas; ctx.imageSmoothingEnabled = true; ctx.imageSmoothingQuality = "high"; ctx.translate(outW / 2, outH / 2); ctx.rotate(rad); ctx.drawImage(source, -srcW / 2, -srcH / 2, srcW, srcH); return canvas; } function logoEditorCropToPixels(totalW, totalH){ const tw = Math.max(1, Math.round(Number(totalW)||1)); const th = Math.max(1, Math.round(Number(totalH)||1)); const cropW = Math.max(1, Math.round((logoEditorState.cropW / 100) * tw)); const cropH = Math.max(1, Math.round((logoEditorState.cropH / 100) * th)); let cropX = Math.max(0, Math.round((logoEditorState.cropX / 100) * tw)); let cropY = Math.max(0, Math.round((logoEditorState.cropY / 100) * th)); const clampedW = Math.min(tw, cropW); const clampedH = Math.min(th, cropH); if(cropX + clampedW > tw) cropX = Math.max(0, tw - clampedW); if(cropY + clampedH > th) cropY = Math.max(0, th - clampedH); return { x: cropX, y: cropY, w: clampedW, h: clampedH }; } function buildLogoEditorArtifacts(){ if(!logoEditorState.image) return null; normalizeLogoEditorState(); const sourceAsset = buildLogoEditorSourceAsset(logoEditorState.image); const rotatedCanvas = buildLogoEditorRotatedCanvas(sourceAsset, logoEditorState.rotation); if(!rotatedCanvas) return null; const cropPx = logoEditorCropToPixels(rotatedCanvas.width, rotatedCanvas.height); const maxExportSide = LOGO_EDITOR_MAX_EXPORT_DIM; const exportScale = Math.min(1, maxExportSide / Math.max(cropPx.w, cropPx.h)); const exportW = Math.max(1, Math.round(cropPx.w * exportScale)); const exportH = Math.max(1, Math.round(cropPx.h * exportScale)); const exportCanvas = document.createElement("canvas"); exportCanvas.width = exportW; exportCanvas.height = exportH; const ectx = exportCanvas.getContext("2d"); if(ectx){ ectx.imageSmoothingEnabled = true; ectx.imageSmoothingQuality = "high"; ectx.clearRect(0, 0, exportW, exportH); ectx.drawImage(rotatedCanvas, cropPx.x, cropPx.y, cropPx.w, cropPx.h, 0, 0, exportW, exportH); } return { sourceAsset, rotatedCanvas, cropPx, exportCanvas, exportW, exportH, rawW: cropPx.w, rawH: cropPx.h }; } function renderLogoEditorView(){ const stage = document.getElementById("logoEditorCanvas"); if(!stage) return; const stageCtx = stage.getContext("2d"); if(!stageCtx) return; stageCtx.clearRect(0, 0, stage.width, stage.height); const preview = document.getElementById("logoEditorPreviewCanvas"); const previewCtx = preview ? preview.getContext("2d") : null; if(previewCtx){ previewCtx.clearRect(0, 0, preview.width, preview.height); } if(!logoEditorState.image){ stageCtx.fillStyle = "rgba(13,55,86,.82)"; stageCtx.font = "700 15px Arial"; stageCtx.textAlign = "center"; stageCtx.fillText("Chargez un logo pour démarrer l'édition", stage.width / 2, stage.height / 2); logoEditorSetDimensionHint("0 x 0 px"); logoEditorState.lastRenderMeta = null; return; } const artifacts = buildLogoEditorArtifacts(); if(!artifacts) return; const rotW = artifacts.rotatedCanvas.width; const rotH = artifacts.rotatedCanvas.height; const scale = Math.min(stage.width / rotW, stage.height / rotH); const drawW = rotW * scale; const drawH = rotH * scale; const offsetX = (stage.width - drawW) / 2; const offsetY = (stage.height - drawH) / 2; stageCtx.imageSmoothingEnabled = true; stageCtx.imageSmoothingQuality = "high"; stageCtx.drawImage(artifacts.rotatedCanvas, offsetX, offsetY, drawW, drawH); const cropDrawX = offsetX + (artifacts.cropPx.x * scale); const cropDrawY = offsetY + (artifacts.cropPx.y * scale); const cropDrawW = artifacts.cropPx.w * scale; const cropDrawH = artifacts.cropPx.h * scale; stageCtx.save(); stageCtx.fillStyle = "rgba(6,25,38,.40)"; stageCtx.fillRect(offsetX, offsetY, drawW, Math.max(0, cropDrawY - offsetY)); stageCtx.fillRect(offsetX, cropDrawY, Math.max(0, cropDrawX - offsetX), cropDrawH); stageCtx.fillRect(cropDrawX + cropDrawW, cropDrawY, Math.max(0, (offsetX + drawW) - (cropDrawX + cropDrawW)), cropDrawH); stageCtx.fillRect(offsetX, cropDrawY + cropDrawH, drawW, Math.max(0, (offsetY + drawH) - (cropDrawY + cropDrawH))); stageCtx.restore(); stageCtx.save(); stageCtx.strokeStyle = "#0f5f96"; stageCtx.lineWidth = 2; stageCtx.setLineDash([8, 5]); stageCtx.strokeRect(cropDrawX, cropDrawY, cropDrawW, cropDrawH); stageCtx.setLineDash([]); stageCtx.fillStyle = "#0f5f96"; const handle = 7; stageCtx.fillRect(cropDrawX - handle, cropDrawY - handle, handle * 2, handle * 2); stageCtx.fillRect(cropDrawX + cropDrawW - handle, cropDrawY - handle, handle * 2, handle * 2); stageCtx.fillRect(cropDrawX - handle, cropDrawY + cropDrawH - handle, handle * 2, handle * 2); stageCtx.fillRect(cropDrawX + cropDrawW - handle, cropDrawY + cropDrawH - handle, handle * 2, handle * 2); stageCtx.restore(); if(previewCtx && preview){ previewCtx.clearRect(0, 0, preview.width, preview.height); const padding = 8; const fit = Math.min((preview.width - (padding*2)) / artifacts.exportW, (preview.height - (padding*2)) / artifacts.exportH); const drawPW = artifacts.exportW * fit; const drawPH = artifacts.exportH * fit; const drawPX = (preview.width - drawPW) / 2; const drawPY = (preview.height - drawPH) / 2; previewCtx.imageSmoothingEnabled = true; previewCtx.imageSmoothingQuality = "high"; previewCtx.drawImage(artifacts.exportCanvas, drawPX, drawPY, drawPW, drawPH); } logoEditorSetDimensionHint(`${artifacts.exportW} x ${artifacts.exportH} px | brut ${artifacts.rawW} x ${artifacts.rawH} px | rot ${logoEditorState.rotation}°`); logoEditorState.lastRenderMeta = { rotatedWidth: rotW, rotatedHeight: rotH, scale, offsetX, offsetY, cropDrawX, cropDrawY, cropDrawW, cropDrawH }; } function logoEditorPointerToStage(event){ const canvas = document.getElementById("logoEditorCanvas"); if(!canvas) return null; const rect = canvas.getBoundingClientRect(); if(!rect.width || !rect.height) return null; const x = (event.clientX - rect.left) * (canvas.width / rect.width); const y = (event.clientY - rect.top) * (canvas.height / rect.height); return { x, y, canvas }; } function onLogoEditorPointerDown(event){ if(!logoEditorState.open) return; if(!logoEditorState.lastRenderMeta) return; if(event.button !== undefined && event.button !== 0) return; const point = logoEditorPointerToStage(event); if(!point) return; const m = logoEditorState.lastRenderMeta; const tolerance = 6; const inside = point.x >= (m.cropDrawX - tolerance) && point.x <= (m.cropDrawX + m.cropDrawW + tolerance) && point.y >= (m.cropDrawY - tolerance) && point.y <= (m.cropDrawY + m.cropDrawH + tolerance); if(!inside) return; logoEditorState.dragActive = true; logoEditorState.dragPointerId = event.pointerId ?? null; logoEditorState.dragStartX = point.x; logoEditorState.dragStartY = point.y; logoEditorState.dragCropX = logoEditorState.cropX; logoEditorState.dragCropY = logoEditorState.cropY; try{ if(point.canvas && typeof point.canvas.setPointerCapture === "function" && event.pointerId != null){ point.canvas.setPointerCapture(event.pointerId); } }catch(e){} event.preventDefault(); } function onLogoEditorPointerMove(event){ if(!logoEditorState.dragActive) return; if(!logoEditorState.lastRenderMeta) return; const point = logoEditorPointerToStage(event); if(!point) return; const m = logoEditorState.lastRenderMeta; const dx = point.x - logoEditorState.dragStartX; const dy = point.y - logoEditorState.dragStartY; const dxPct = ((dx / m.scale) / m.rotatedWidth) * 100; const dyPct = ((dy / m.scale) / m.rotatedHeight) * 100; const maxX = Math.max(0, 100 - logoEditorState.cropW); const maxY = Math.max(0, 100 - logoEditorState.cropH); logoEditorState.cropX = Math.round(clampNum(logoEditorState.dragCropX + dxPct, 0, maxX, logoEditorState.cropX)); logoEditorState.cropY = Math.round(clampNum(logoEditorState.dragCropY + dyPct, 0, maxY, logoEditorState.cropY)); normalizeLogoEditorState(); setLogoEditorControlsFromState(); renderLogoEditorView(); event.preventDefault(); } function onLogoEditorPointerUp(event){ if(!logoEditorState.dragActive) return; const canvas = document.getElementById("logoEditorCanvas"); try{ if(canvas && typeof canvas.releasePointerCapture === "function" && event.pointerId != null){ canvas.releasePointerCapture(event.pointerId); } }catch(e){} logoEditorState.dragActive = false; logoEditorState.dragPointerId = null; } function closeLogoEditorModal(){ const backdrop = document.getElementById("logoEditorBackdrop"); if(backdrop){ backdrop.style.display = "none"; backdrop.setAttribute("aria-hidden", "true"); } logoEditorState.open = false; logoEditorState.dragActive = false; logoEditorState.dragPointerId = null; logoEditorState.image = null; logoEditorState.source = ""; logoEditorState.lastRenderMeta = null; } async function openLogoEditorModal(){ if(!requireCapability("manageConfig", "Éditer le logo")) return; const backdrop = document.getElementById("logoEditorBackdrop"); if(!backdrop) return; const src = sanitizeLogoSrc(readLogoSources({useDraft:true}).topSrc); if(!src){ alert("Aucun logo disponible à éditer."); return; } resetLogoEditorStateValues(); setLogoEditorControlsFromState(); logoEditorState.open = true; logoEditorState.source = src; logoEditorState.lastRenderMeta = null; const token = `${Date.now()}_${Math.random()}`; logoEditorState.loadToken = token; backdrop.style.display = "flex"; backdrop.setAttribute("aria-hidden", "false"); logoEditorSetHint("Chargement du logo..."); renderLogoEditorView(); try{ const image = await logoEditorLoadImage(src); if(!logoEditorState.open || logoEditorState.loadToken !== token) return; logoEditorState.image = image; logoEditorSetHint("Astuce: déplacez le cadre pour cadrer précisément, puis appliquez."); setLogoEditorControlsFromState(); renderLogoEditorView(); const focusEl = document.getElementById("logoEditorRotation"); if(focusEl) focusEl.focus(); }catch(e){ if(!logoEditorState.open || logoEditorState.loadToken !== token) return; logoEditorSetHint("Impossible de charger ce logo dans l'éditeur."); renderLogoEditorView(); alert("Impossible de charger le logo pour l'édition (format/cross-origin)."); } } function logoEditorApplyPresetFullFrame(){ if(!requireCapability("manageConfig", "Recadrage logo")) return; logoEditorState.cropX = 0; logoEditorState.cropY = 0; logoEditorState.cropW = 100; logoEditorState.cropH = 100; normalizeLogoEditorState(); setLogoEditorControlsFromState(); renderLogoEditorView(); } function logoEditorRotateBy(delta){ if(!requireCapability("manageConfig", "Rotation logo")) return; logoEditorState.rotation = Math.round(clampNum((Number(logoEditorState.rotation)||0) + Number(delta||0), -180, 180, 0)); normalizeLogoEditorState(); setLogoEditorControlsFromState(); renderLogoEditorView(); } function logoEditorResetAll(){ if(!requireCapability("manageConfig", "Réinitialiser logo studio")) return; resetLogoEditorStateValues(); setLogoEditorControlsFromState(); renderLogoEditorView(); } function applyLogoEditorResult(){ if(!requireCapability("manageConfig", "Appliquer logo édité")) return; if(!logoEditorState.image) return alert("Chargez un logo avant d'appliquer."); const artifacts = buildLogoEditorArtifacts(); if(!artifacts || !artifacts.exportCanvas) return alert("Échec du rendu logo."); let dataUrl = ""; try{ dataUrl = artifacts.exportCanvas.toDataURL("image/png"); }catch(e){ alert("Impossible d'exporter ce logo. Vérifiez la source (CORS)."); return; } const safe = sanitizeLogoSrc(dataUrl); if(!safe){ alert("Le logo généré est invalide."); return; } logoDraftSrc = safe; logoDraftFileName = asSafeText(`logo_edite_${artifacts.exportW}x${artifacts.exportH}.png`, 140); if(document.getElementById("logoFileInput")) $("logoFileInput").value = ""; applyLogoFromUi(); addAudit("LOGO_EDIT_PREPARE", `draft=${artifacts.exportW}x${artifacts.exportH}; rot=${logoEditorState.rotation}`); logoEditorSetHint("Logo édité prêt. Cliquez sur Enregistrer dans Paramètres pour valider."); closeLogoEditorModal(); } function syncLogoEditorFromInputsAndRender(){ readLogoEditorControlsToState(); setLogoEditorControlsFromState(); renderLogoEditorView(); } /* Render */ let activeBank="BEA"; function renderParams(){ const uiSettings = readUiSettingsFromState(); const logoSettings = readLogoSettingsFromState(); $("day").value = state.params.day || nowDate(); $("fond").value = round2(state.params.fond||0).toFixed(2); $("ribBEA").value = state.params.ribBEA || ""; $("ribCPA").value = state.params.ribCPA || ""; $("adresse").value = state.params.adresse || ""; $("rc").value = state.params.rc || ""; $("nif").value = state.params.nif || ""; $("pin").value = state.params.pinAdmin || state.params.pin || ""; if(document.getElementById("pinCashier")) $("pinCashier").value = state.params.pinCashier || ""; if(document.getElementById("pinAudit")) $("pinAudit").value = state.params.pinAudit || ""; if(document.getElementById("sessionTimeoutMin")){ $("sessionTimeoutMin").value = String(normalizeSessionTimeoutMinutes(state.params.sessionTimeoutMin, 15)); } if(document.getElementById("uiBaseFontSize")) $("uiBaseFontSize").value = String(uiSettings.uiBaseFontSize); if(document.getElementById("uiButtonFontSize")) $("uiButtonFontSize").value = String(uiSettings.uiButtonFontSize); if(document.getElementById("uiButtonPadY")) $("uiButtonPadY").value = String(uiSettings.uiButtonPadY); if(document.getElementById("uiButtonPadX")) $("uiButtonPadX").value = String(uiSettings.uiButtonPadX); if(document.getElementById("uiButtonRadius")) $("uiButtonRadius").value = String(uiSettings.uiButtonRadius); if(document.getElementById("uiFieldFontSize")) $("uiFieldFontSize").value = String(uiSettings.uiFieldFontSize); if(document.getElementById("uiFieldPadY")) $("uiFieldPadY").value = String(uiSettings.uiFieldPadY); if(document.getElementById("uiFieldRadius")) $("uiFieldRadius").value = String(uiSettings.uiFieldRadius); if(document.getElementById("uiNavFontSize")) $("uiNavFontSize").value = String(uiSettings.uiNavFontSize); if(document.getElementById("uiNavItemMinHeight")) $("uiNavItemMinHeight").value = String(uiSettings.uiNavItemMinHeight); if(document.getElementById("uiNavWidth")) $("uiNavWidth").value = String(uiSettings.uiNavWidth); if(document.getElementById("uiTabFontSize")) $("uiTabFontSize").value = String(uiSettings.uiTabFontSize); if(document.getElementById("logoSize")) $("logoSize").value = String(logoSettings.logoSize); if(document.getElementById("logoOffsetX")) $("logoOffsetX").value = String(logoSettings.logoOffsetX); if(document.getElementById("logoOffsetY")) $("logoOffsetY").value = String(logoSettings.logoOffsetY); if(document.getElementById("logoClarity")) $("logoClarity").value = String(logoSettings.logoClarity); if(document.getElementById("logoOpacity")) $("logoOpacity").value = String(logoSettings.logoOpacity); if(document.getElementById("printLogoSize")) $("printLogoSize").value = String(logoSettings.printLogoSize); if(document.getElementById("printLogoOffsetX")) $("printLogoOffsetX").value = String(logoSettings.printLogoOffsetX); if(document.getElementById("printLogoOffsetY")) $("printLogoOffsetY").value = String(logoSettings.printLogoOffsetY); if(document.getElementById("printLogoOpacity")) $("printLogoOpacity").value = String(logoSettings.printLogoOpacity); if(document.getElementById("watermarkScale")) $("watermarkScale").value = String(logoSettings.watermarkScale); if(document.getElementById("watermarkOffsetX")) $("watermarkOffsetX").value = String(logoSettings.watermarkOffsetX); if(document.getElementById("watermarkOffsetY")) $("watermarkOffsetY").value = String(logoSettings.watermarkOffsetY); if(document.getElementById("watermarkOpacity")) $("watermarkOpacity").value = String(logoSettings.watermarkOpacity); if(document.getElementById("logoPreviewBank")){ const sel = $("logoPreviewBank"); const v = String(sel.value || "").trim().toUpperCase(); if(v !== "BEA" && v !== "CPA"){ sel.value = activeBank === "CPA" ? "CPA" : "BEA"; } } $("txDate").value = state.params.day || nowDate(); $("txTime").value = nowTime(); if(document.getElementById("decDate")) $("decDate").value = state.params.day || nowDate(); if(document.getElementById("decTime")) $("decTime").value = nowTime(); const topDay = document.getElementById("topDay"); if(topDay) topDay.textContent = state.params.day || nowDate(); $("dayStatus").innerHTML = isClosed(state.params.day) ? 'Jour clôturé' : 'Jour ouvert'; applyUiSettings(uiSettings); applyLogoFromUi(); } function readPageSize(selectId, fallback=50){ const raw = Number(document.getElementById(selectId)?.value || fallback); return PAGE_SIZE_OPTIONS.has(raw) ? raw : fallback; } function setPager(prefix, page, totalPages){ const cur = document.getElementById(`${prefix}PageCurrent`); const tot = document.getElementById(`${prefix}PageTotal`); const prev = document.getElementById(`${prefix}PrevPageBtn`); const next = document.getElementById(`${prefix}NextPageBtn`); if(cur) cur.textContent = String(page); if(tot) tot.textContent = String(totalPages); if(prev) prev.disabled = page <= 1; if(next) next.disabled = page >= totalPages; } function sortMult(dir){ return dir === "asc" ? 1 : -1; } function cmpText(a, b){ return String(a??"").localeCompare(String(b??""), "fr", {numeric:true, sensitivity:"base"}); } function cmpNum(a, b){ return (Number(a)||0) - (Number(b)||0); } function cmpDateTime(aDate, aTime, bDate, bTime){ const aa = `${String(aDate||"")} ${String(aTime||"")}`; const bb = `${String(bDate||"")} ${String(bTime||"")}`; return aa.localeCompare(bb); } function getSortOrders(stateObj, fallbackKey){ const base = Array.isArray(stateObj?.orders) ? stateObj.orders : []; const clean = base .filter(o => o && typeof o === "object") .map(o => ({ key: String(o.key || "").trim(), dir: String(o.dir || "").trim() === "asc" ? "asc" : "desc" })) .filter(o => !!o.key); if(clean.length <= 0){ clean.push({ key: fallbackKey, dir: SORT_DEFAULT_DIR[fallbackKey] || "asc" }); } return clean.slice(0, MAX_MULTI_SORT_KEYS); } function cmpClientByKey(key, a, b){ if(key === "name") return cmpText(a.name, b.name); return cmpNum(a.id, b.id); } function cmpTxByKey(key, a, b){ if(key === "date") return cmpDateTime(a.date, a.time, b.date, b.time); if(key === "time") return cmpText(a.time, b.time) || cmpText(a.date, b.date); if(key === "clientId") return cmpNum(a.clientId, b.clientId); if(key === "clientName") return cmpText(a.clientName, b.clientName); if(key === "produit") return cmpText(a.produit, b.produit); if(key === "receipt") return cmpText(a.receipt, b.receipt); if(key === "mode") return cmpText(a.mode, b.mode); if(key === "amount") return cmpNum(a.amount, b.amount); return cmpDateTime(a.date, a.time, b.date, b.time); } function cmpDecByKey(key, a, b){ if(key === "date") return cmpDateTime(a.date, a.time, b.date, b.time); if(key === "time") return cmpText(a.time, b.time) || cmpText(a.date, b.date); if(key === "label") return cmpText(a.label, b.label); if(key === "mode") return cmpText(a.mode, b.mode); if(key === "amount") return cmpNum(a.amount, b.amount); return cmpDateTime(a.date, a.time, b.date, b.time); } function sortClientsRows(rows){ const orders = getSortOrders(clientsSort, "id"); clientsSort.orders = orders; const out = rows.slice(); out.sort((a,b)=>{ for(const o of orders){ const c = cmpClientByKey(o.key, a, b); if(c !== 0) return c * sortMult(o.dir); } return cmpNum(a.id, b.id) || cmpText(a.name, b.name); }); return out; } function sortTxRows(rows){ const orders = getSortOrders(txSort, "date"); txSort.orders = orders; const out = rows.slice(); out.sort((a,b)=>{ for(const o of orders){ const c = cmpTxByKey(o.key, a, b); if(c !== 0) return c * sortMult(o.dir); } return cmpText(a.id, b.id); }); return out; } function sortDecRows(rows){ const orders = getSortOrders(decSort, "date"); decSort.orders = orders; const out = rows.slice(); out.sort((a,b)=>{ for(const o of orders){ const c = cmpDecByKey(o.key, a, b); if(c !== 0) return c * sortMult(o.dir); } return cmpText(a.id, b.id); }); return out; } function getSortState(table){ if(table === "clients") return clientsSort; if(table === "tx") return txSort; if(table === "dec") return decSort; return null; } function renderSortButtons(){ document.querySelectorAll(".sort-btn[data-sort-table][data-sort-key]").forEach((btn)=>{ const table = btn.getAttribute("data-sort-table"); const key = btn.getAttribute("data-sort-key"); const st = getSortState(table); const fallbackKey = table === "clients" ? "id" : "date"; const orders = getSortOrders(st, fallbackKey); const idx = orders.findIndex(o=> o.key === key); if(!st || idx < 0){ btn.classList.remove("active"); btn.setAttribute("data-dir", ""); btn.removeAttribute("data-rank"); btn.setAttribute("aria-sort", "none"); return; } const current = orders[idx]; btn.classList.add("active"); btn.setAttribute("data-dir", current.dir); btn.setAttribute("data-rank", String(idx + 1)); btn.setAttribute("aria-sort", current.dir === "asc" ? "ascending" : "descending"); }); } function toggleTableSort(table, key, opts={}){ const st = getSortState(table); if(!st) return; const orders = getSortOrders(st, key); const idx = orders.findIndex(o=> o.key === key); const multi = opts && opts.multi === true; if(multi){ if(idx < 0){ orders.push({ key, dir: SORT_DEFAULT_DIR[key] || "asc" }); }else{ const curr = orders[idx]; if(curr.dir === "asc"){ curr.dir = "desc"; }else{ if(orders.length > 1){ orders.splice(idx, 1); }else{ curr.dir = "asc"; } } } }else{ if(idx === 0){ orders[0].dir = orders[0].dir === "asc" ? "desc" : "asc"; }else if(idx > 0){ const curr = orders[idx]; orders.splice(idx, 1); orders.unshift(curr); }else{ orders.unshift({ key, dir: SORT_DEFAULT_DIR[key] || "asc" }); } } st.orders = orders.slice(0, MAX_MULTI_SORT_KEYS); if(table === "clients"){ clientsPage = 1; renderClients(); }else if(table === "tx"){ txPage = 1; renderTx(); }else if(table === "dec"){ decPage = 1; renderDec(); } renderSortButtons(); } function matchTxPreset(t, preset){ if(!preset || preset === "all") return true; if(preset === "cash") return t.mode === "Espèces"; if(preset === "card") return t.mode === "Carte"; if(preset === "cheque") return t.mode === "Chèque"; if(preset === "high") return (Number(t.amount)||0) >= 10000; if(preset === "noreceipt") return !String(t.receipt||"").trim(); return true; } function matchDecPreset(d, preset){ if(!preset || preset === "all") return true; if(preset === "cash") return d.mode === "Espèces"; if(preset === "cheque") return d.mode === "Chèque"; if(preset === "wire") return d.mode === "Virement"; if(preset === "high") return (Number(d.amount)||0) >= 10000; return true; } function renderFilterPresetButtons(){ document.querySelectorAll('[data-filter-group="txPreset"][data-filter-value]').forEach((btn)=>{ const val = String(btn.getAttribute("data-filter-value")||"all"); btn.classList.toggle("active", val === txPresetFilter); }); document.querySelectorAll('[data-filter-group="decPreset"][data-filter-value]').forEach((btn)=>{ const val = String(btn.getAttribute("data-filter-value")||"all"); btn.classList.toggle("active", val === decPresetFilter); }); } function renderClients(){ const qLocal = normalizeSearchTerm($("clientSearch").value); const qGlobal = getGlobalSearchTerm(); const match = (c, q)=> !q || String(c.id).includes(q) || String(c.name||"").toLowerCase().includes(q); const filtered = sortClientsRows( state.clients.filter(c=> match(c, qLocal) && match(c, qGlobal)) ); const pageSize = readPageSize("clientsPageSize", 50); const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); if(clientsPage > totalPages) clientsPage = totalPages; if(clientsPage < 1) clientsPage = 1; const start = (clientsPage - 1) * pageSize; const list = filtered.slice(start, start + pageSize); const rows=list .map(c=>` ${c.id} ${escapeHtml(c.name)} `).join(""); $("clientsTbody").innerHTML = rows || `Aucun client`; setPager("clients", clientsPage, totalPages); refreshClientFormMode(); } function renderTx(){ const day=state.params.day; const qLocal = normalizeSearchTerm($("txSearch").value); const qGlobal = getGlobalSearchTerm(); const pf=$("txProdFilter").value || ""; const match = (t, q)=>{ if(!q) return true; const blob = [ t.clientId, t.clientName, t.receipt, t.produit, t.mode, t.date, t.time, t.amount ].map(v=> String(v||"").toLowerCase()).join(" "); return blob.includes(q); }; const filtered=sortTxRows( txOfDay(day) .filter(t=> !pf || t.produit===pf) .filter(t=> matchTxPreset(t, txPresetFilter)) .filter(t=> match(t, qLocal) && match(t, qGlobal)) ); const pageSize = readPageSize("txPageSize", 50); const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); if(txPage > totalPages) txPage = totalPages; if(txPage < 1) txPage = 1; const start = (txPage - 1) * pageSize; const list = filtered.slice(start, start + pageSize); const rows=list.map((t,i)=>` ${start + i + 1} ${t.date} ${t.time} ${t.clientId} ${escapeHtml(t.clientName)} ${t.produit} ${escapeHtml(t.receipt||"")} ${t.mode} ${fmt(t.amount)} `).join(""); $("txTbody").innerHTML = rows || `Aucune opération`; setPager("tx", txPage, totalPages); } function renderDec(){ const day = state.params.day; const qLocal = normalizeSearchTerm($("decSearch").value); const qGlobal = getGlobalSearchTerm(); const match = (d, q)=>{ if(!q) return true; const blob = [ d.label, d.mode, d.date, d.time, d.amount ].map(v=> String(v||"").toLowerCase()).join(" "); return blob.includes(q); }; const filtered = sortDecRows( decOfDay(day) .filter(d=> matchDecPreset(d, decPresetFilter)) .filter(d => match(d, qLocal) && match(d, qGlobal)) ); const pageSize = readPageSize("decPageSize", 50); const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); if(decPage > totalPages) decPage = totalPages; if(decPage < 1) decPage = 1; const start = (decPage - 1) * pageSize; const list = filtered.slice(start, start + pageSize); const rows = list.map((d,i)=>` ${start + i + 1} ${d.date} ${d.time} ${escapeHtml(d.label)} ${d.mode} ${fmt(d.amount)} `).join(""); $("decTbody").innerHTML = rows || `Aucun décaissement`; setPager("dec", decPage, totalPages); } function renderSummary(){ const day = state.params.day; const s=computeDeposits(day); $("sumCash").textContent = fmt(s.cashTotal); $("sumCard").textContent = fmt(s.card); $("sumCheque").textContent = fmt(s.cheque); $("sumAll").textContent = fmt(s.all); $("sumFond").textContent = fmt(s.fond); $("sumDecCash").textContent = fmt(s.decCash||0); $("sumDecAll").textContent = fmt(s.decAll||0); $("depBEA").textContent = fmt(s.depBEA); $("depCPA").textContent = fmt(s.depCPA); // Daily monitoring (drawer) const drawerBefore = (Number(s.fond)||0) + (Number(s.cashTotal)||0) - (Number(s.decCash)||0); const depositAll = (Number(s.depBEA)||0) + (Number(s.depCPA)||0); const drawerAfter = drawerBefore - depositAll; // should equal fond if(document.getElementById("sumDrawerBefore")) $("sumDrawerBefore").textContent = fmt(drawerBefore); if(document.getElementById("sumDepositAll")) $("sumDepositAll").textContent = fmt(depositAll); if(document.getElementById("sumDrawerAfter")) $("sumDrawerAfter").textContent = fmt(drawerAfter); renderRecon(day, drawerAfter); } function computeTopClientsForDay(day, limit=6){ const bucket = new Map(); for(const tx of txOfDay(day)){ const clientId = asPositiveInt(tx.clientId); if(!clientId) continue; const amount = Number(tx.amount)||0; const key = String(clientId); const existing = bucket.get(key) || { clientId, clientName: asSafeText(tx.clientName || clientNameById(clientId) || `Client ${clientId}`, 180), count: 0, amount: 0 }; existing.count += 1; existing.amount += amount; if(!existing.clientName) existing.clientName = asSafeText(tx.clientName || `Client ${clientId}`, 180); bucket.set(key, existing); } return Array.from(bucket.values()) .sort((a,b)=>{ if(round2(b.amount) !== round2(a.amount)) return round2(b.amount) - round2(a.amount); if(b.count !== a.count) return b.count - a.count; return a.clientId - b.clientId; }) .slice(0, Math.max(1, Number(limit)||6)); } function countPotentialDuplicateClusters(rows){ const byKey = new Map(); for(const tx of (Array.isArray(rows) ? rows : [])){ const clientId = asPositiveInt(tx.clientId); const amount = round2(Number(tx.amount)||0); const minutes = parseHHMMToMinutes(tx.time); if(!clientId || amount <= 0 || minutes < 0) continue; const mode = String(tx.mode || ""); const key = `${clientId}|${amount}|${mode}`; if(!byKey.has(key)) byKey.set(key, []); byKey.get(key).push(minutes); } let duplicates = 0; for(const minutesList of byKey.values()){ const arr = minutesList.slice().sort((a,b)=>a-b); let matched = false; for(let i=1; i cashIn){ anomalies.push({ severity: "high", title: "Décaissement espèces supérieur aux encaissements espèces", details: `Sorties espèces ${fmt(decCash)} DZD > entrées espèces ${fmt(cashIn)} DZD.` }); } if(drawerAfter < -0.01){ anomalies.push({ severity: "high", title: "Solde de caisse théorique négatif", details: `Solde attendu après versement: ${fmt(drawerAfter)} DZD.` }); } const duplicateClusters = countPotentialDuplicateClusters(txRows); if(duplicateClusters > 0){ anomalies.push({ severity: duplicateClusters >= 3 ? "high" : "medium", title: "Opérations potentiellement dupliquées", details: `${duplicateClusters} groupe(s) client/montant proche(s) dans une fenêtre de ${TX_DUP_TIME_WINDOW_MIN} min.` }); } const missingReceipt = txRows.filter(t=> !String(t.receipt||"").trim()).length; if(txRows.length >= 5){ const ratio = missingReceipt / txRows.length; if(ratio >= 0.35){ anomalies.push({ severity: "medium", title: "Taux élevé de reçus manquants", details: `${missingReceipt}/${txRows.length} opérations sans N° reçu (${(ratio*100).toFixed(0)}%).` }); } } const maxCashTx = txRows .filter(t=> String(t.mode||"") === "Espèces") .reduce((m,t)=> Math.max(m, Number(t.amount)||0), 0); if(maxCashTx >= TX_WARN_AMOUNT_DZD){ anomalies.push({ severity: "low", title: "Encaissement cash unitaire élevé", details: `Montant maximum observé: ${fmt(maxCashTx)} DZD.` }); } const recon = (state.recon && state.recon[day]) ? state.recon[day] : null; if(recon && recon.drawerCount !== ""){ const gap = (Number(recon.drawerCount)||0) - drawerAfter; const absGap = Math.abs(gap); if(absGap >= 500){ anomalies.push({ severity: absGap >= 2000 ? "high" : "medium", title: "Écart de comptage caisse", details: `Écart constaté: ${fmtSigned(gap)} DZD.` }); } }else if(txRows.length > 0 || decRows.length > 0){ anomalies.push({ severity: "low", title: "Comptage de caisse non enregistré", details: "Enregistrez le comptage final pour verrouiller le contrôle journalier." }); } if(txRows.length === 0 && decRows.length > 0){ anomalies.push({ severity: "medium", title: "Décaissements sans encaissements le même jour", details: `${decRows.length} décaissement(s) détecté(s) avec 0 encaissement.` }); } if(anomalies.length === 0){ anomalies.push({ severity: "low", title: "Aucune anomalie critique détectée", details: "Les contrôles opérationnels du jour sont conformes." }); } return anomalies.slice(0, 8); } function renderExecutiveDashboard(){ if(!document.getElementById("dashboard")) return; const day = normalizeDate(state.params.day, nowDate()); const txRows = txOfDay(day); const s = computeDeposits(day); const topClients = computeTopClientsForDay(day, 6); const anomalies = computeDailyAnomalies(day); const uniqueClients = new Set(txRows.map(t=> asPositiveInt(t.clientId)).filter(v=>v>0)).size; const depositAll = (Number(s.depBEA)||0) + (Number(s.depCPA)||0); const avgTicket = txRows.length ? (Number(s.all)||0) / txRows.length : 0; const netCash = (Number(s.cashTotal)||0) - (Number(s.decCash)||0); const coverage = (Number(s.cashTotal)||0) > 0 ? (depositAll / (Number(s.cashTotal)||0)) * 100 : 0; const recon = (state.recon && state.recon[day]) ? state.recon[day] : null; const drawerBefore = (Number(s.fond)||0) + (Number(s.cashTotal)||0) - (Number(s.decCash)||0); const drawerAfter = drawerBefore - depositAll; const drawerGap = (recon && recon.drawerCount !== "") ? ((Number(recon.drawerCount)||0) - drawerAfter) : null; const highCount = anomalies.filter(a=>a.severity==="high").length; const mediumCount = anomalies.filter(a=>a.severity==="medium").length; if(document.getElementById("dashKpiOps")) $("dashKpiOps").textContent = String(txRows.length); if(document.getElementById("dashKpiOpsMeta")) $("dashKpiOpsMeta").innerHTML = `${uniqueClients} client(s) actif(s)`; if(document.getElementById("dashKpiRevenue")) $("dashKpiRevenue").textContent = fmt(s.all||0); if(document.getElementById("dashKpiRevenueMeta")) $("dashKpiRevenueMeta").innerHTML = `Cash ${fmt(s.cashTotal||0)} | Carte ${fmt(s.card||0)} | Chèque ${fmt(s.cheque||0)}`; if(document.getElementById("dashKpiCashflow")) $("dashKpiCashflow").textContent = fmt(netCash); if(document.getElementById("dashKpiCashflowMeta")) $("dashKpiCashflowMeta").innerHTML = `Décaissement espèces: ${fmt(s.decCash||0)} DZD`; if(document.getElementById("dashKpiRisk")) $("dashKpiRisk").textContent = String(anomalies.length); if(document.getElementById("dashKpiRiskMeta")) $("dashKpiRiskMeta").textContent = highCount > 0 ? `${highCount} alerte(s) haute priorité` : (mediumCount > 0 ? `${mediumCount} alerte(s) moyenne priorité` : "Niveau de risque faible"); if(document.getElementById("dashAvgTicket")) $("dashAvgTicket").textContent = fmt(avgTicket); if(document.getElementById("dashDepositCoverage")) $("dashDepositCoverage").textContent = `${Math.max(0, coverage).toFixed(1)}%`; if(document.getElementById("dashDrawerGap")){ $("dashDrawerGap").textContent = drawerGap === null ? "—" : fmtSigned(drawerGap); } if(document.getElementById("dashTopClientsTbody")){ $("dashTopClientsTbody").innerHTML = topClients.map((row, i)=>` ${i+1} ${escapeHtml(String(row.clientName||`Client ${row.clientId}`))} #${row.clientId} ${row.count} ${fmt(row.amount)} `).join("") || `Aucune opération client pour ce jour.`; } if(document.getElementById("dashAnomalyList")){ $("dashAnomalyList").innerHTML = anomalies.map((a)=>`
    ${escapeHtml(a.title || "")} ${escapeHtml(a.details || "")}
    `).join(""); } } function navigateToSectionHash(hash){ const h = String(hash||"").startsWith("#") ? String(hash) : `#${String(hash||"")}`; const link = document.querySelector(`#nav a.nav-link[href="${h}"]`); if(link){ focusSectionByHash(h, link); }else{ const sec = document.querySelector(h); if(sec) sec.scrollIntoView({behavior:"smooth", block:"start"}); } try{ history.replaceState(null, "", h); }catch(e){} } function getCommandPaletteCommands(){ const navCommands = Array.from(document.querySelectorAll('#nav a.nav-link[href^="#"]')).map((link)=>{ const hash = String(link.getAttribute("href")||"").trim(); const rawLabel = String(link.textContent||"").replace(/\s+/g, " ").trim(); const label = rawLabel.replace(/^\d+\s*/, "").trim() || hash.replace(/^#/, ""); return { id: `go_${hash.replace("#","")}`, title: `Aller à ${label}`, hint: hash, keywords: `${label} section ${hash}`, enabled: ()=> true, run: ()=> navigateToSectionHash(hash) }; }); const actionCommands = [ { id: "focus_search", title: "Focus recherche globale", hint: "Client / reçu / montant", keywords: "recherche global search", enabled: ()=> true, run: ()=>{ const input = document.getElementById("globalSearch"); if(input){ input.focus(); input.select(); } } }, { id: "logo_studio", title: "Logo Studio (Crop + Rotation)", hint: "Paramètres logo", keywords: "logo crop rotation studio", enabled: ()=> hasRoleCapability("manageConfig", currentRole), run: ()=> openLogoEditorModal() }, { id: "add_client", title: "Ajouter / modifier client", hint: "Fenêtre client", keywords: "client add edit", enabled: ()=> hasRoleCapability("manageData", currentRole), run: ()=> openClientModal() }, { id: "add_tx", title: "Ajouter encaissement", hint: "Fenêtre encaissement", keywords: "encaissement operation tx", enabled: ()=> hasRoleCapability("manageData", currentRole), run: ()=> openTxModal() }, { id: "add_dec", title: "Ajouter décaissement", hint: "Fenêtre décaissement", keywords: "decaissement sortie cash", enabled: ()=> hasRoleCapability("manageData", currentRole), run: ()=> openDecModal() }, { id: "switch_role", title: "Changer de rôle", hint: `Rôle actuel: ${roleLabel(currentRole)}`, keywords: "role pin access", enabled: ()=> hasRoleCapability("switchRole", currentRole), run: ()=> switchRoleInteractive() }, { id: "lock_session", title: sessionLocked ? "Déverrouiller la session" : "Verrouiller la session", hint: sessionLocked ? "PIN du rôle actif" : "Sécurité session", keywords: "lock unlock session", enabled: ()=> true, run: ()=>{ if(sessionLocked){ unlockSessionInteractive("Déverrouillage via palette"); }else{ lockSession("palette"); } } }, { id: "undo_last", title: "Annuler dernière action", hint: undoEntry ? undoEntry.label : "Aucune action disponible", keywords: "undo annuler", enabled: ()=> !!undoEntry && hasRoleCapability("manageData", currentRole), run: ()=> applyUndo() }, { id: "toggle_theme", title: "Basculer thème clair/sombre", hint: "Theme", keywords: "theme dark light", enabled: ()=> true, run: ()=> toggleTheme() }, { id: "cycle_skin", title: "Changer style PRO", hint: "Classique / Executive / Heritage / Aqua", keywords: "style skin pro", enabled: ()=> true, run: ()=> cycleSkin() }, { id: "toggle_density", title: "Basculer vue compacte", hint: "Densité des tableaux", keywords: "compact density table", enabled: ()=> true, run: ()=> toggleDensity() }, { id: "print_report", title: "Imprimer rapport caisse", hint: "PDF journalier", keywords: "print report pdf", enabled: ()=> hasRoleCapability("printOps", currentRole), run: ()=>{ if(!requireCapability("printOps", "Imprimer le rapport caisse")) return; document.getElementById("printReportBtn")?.click(); } }, { id: "print_bord", title: "Imprimer bordereau actif", hint: "Banque sélectionnée", keywords: "print bordereau bank", enabled: ()=> hasRoleCapability("printOps", currentRole), run: ()=>{ if(!requireCapability("printOps", "Imprimer le bordereau actif")) return; document.getElementById("printBordBtn")?.click(); } }, { id: "print_pack", title: "Imprimer pack du jour", hint: "Rapport + BEA/CPA", keywords: "print pack day", enabled: ()=> hasRoleCapability("printOps", currentRole), run: ()=>{ if(!requireCapability("printOps", "Imprimer le pack du jour")) return; document.getElementById("printPackBtn")?.click(); } }, { id: "backup_now", title: "Créer auto-backup maintenant", hint: "Sauvegarde locale", keywords: "backup json auto", enabled: ()=> hasRoleCapability("export", currentRole), run: ()=>{ if(!requireCapability("export", "Créer un auto-backup")) return; document.getElementById("createAutoBackupBtn")?.click(); } } ]; return [...navCommands, ...actionCommands]; } function updateCommandPaletteActive(opts={}){ const list = document.getElementById("cmdPaletteList"); if(!list) return; const buttons = list.querySelectorAll(".cmd-item[data-cmd-index]"); buttons.forEach((btn)=>{ const idx = Number(btn.getAttribute("data-cmd-index") || -1); const isActive = idx === cmdPaletteActiveIndex; btn.classList.toggle("active", isActive); if(isActive && opts.scroll){ try{ btn.scrollIntoView({block:"nearest"}); }catch(e){} } }); const hint = document.getElementById("cmdPaletteHint"); if(hint){ if(!cmdPaletteItems.length){ hint.textContent = "Aucune commande trouvée. Essayez un autre mot-clé."; }else{ const cmd = cmdPaletteItems[cmdPaletteActiveIndex]; if(!cmd){ hint.textContent = "Entrée: exécuter | ↑/↓: naviguer | Esc: fermer"; }else if(cmd.enabled){ hint.textContent = `${cmd.hint || "Commande"} | Entrée: exécuter | ↑/↓: naviguer | Esc: fermer`; }else{ hint.textContent = `${cmd.hint || "Commande"} | Indisponible pour le rôle ${roleLabel(currentRole)}`; } } } } function renderCommandPaletteResults(){ const list = document.getElementById("cmdPaletteList"); const input = document.getElementById("cmdPaletteInput"); if(!list || !input) return; const term = normalizeSearchTerm(input.value); const all = getCommandPaletteCommands().map((cmd)=>({ ...cmd, enabled: typeof cmd.enabled === "function" ? !!cmd.enabled() : true })); cmdPaletteItems = all.filter((cmd)=>{ if(!term) return true; const blob = `${cmd.title||""} ${cmd.hint||""} ${cmd.keywords||""}`.toLowerCase(); return blob.includes(term); }); if(cmdPaletteActiveIndex >= cmdPaletteItems.length){ cmdPaletteActiveIndex = Math.max(0, cmdPaletteItems.length - 1); } if(cmdPaletteItems.length && (!cmdPaletteItems[cmdPaletteActiveIndex] || !cmdPaletteItems[cmdPaletteActiveIndex].enabled)){ const firstEnabled = cmdPaletteItems.findIndex(c=> c.enabled); cmdPaletteActiveIndex = firstEnabled >= 0 ? firstEnabled : 0; } if(!cmdPaletteItems.length){ list.innerHTML = `
    Aucun résultat pour cette recherche.
    `; updateCommandPaletteActive(); return; } list.innerHTML = cmdPaletteItems.map((cmd, idx)=>` `).join(""); list.querySelectorAll(".cmd-item[data-cmd-index]").forEach((btn)=>{ btn.addEventListener("mouseenter", ()=>{ cmdPaletteActiveIndex = Number(btn.getAttribute("data-cmd-index")||0); updateCommandPaletteActive(); }); btn.addEventListener("click", ()=>{ const idx = Number(btn.getAttribute("data-cmd-index")||0); const selected = cmdPaletteItems[idx]; if(!selected || !selected.enabled) return; closeCommandPalette({restoreFocus:false}); try{ selected.run(); addAudit("CMD_EXECUTE", selected.id || selected.title || "unknown"); }catch(err){ alert("Commande impossible à exécuter."); } }); }); updateCommandPaletteActive(); } function moveCommandPaletteSelection(delta){ if(!cmdPaletteItems.length) return; const dir = delta >= 0 ? 1 : -1; let idx = cmdPaletteActiveIndex; let found = false; for(let i=0; i{ input.focus(); input.select(); }, 20); } function closeCommandPalette(opts={}){ const backdrop = document.getElementById("commandPaletteBackdrop"); if(!backdrop) return; cmdPaletteOpen = false; backdrop.style.display = "none"; backdrop.setAttribute("aria-hidden", "true"); cmdPaletteItems = []; cmdPaletteActiveIndex = 0; const restoreFocus = opts.restoreFocus !== false; const prevFocus = cmdPaletteLastFocus; cmdPaletteLastFocus = null; if(restoreFocus && prevFocus && typeof prevFocus.focus === "function"){ setTimeout(()=>{ try{ prevFocus.focus(); }catch(e){} }, 0); } } function bankColor(bankKey){ return bankKey==="BEA" ? "#0a3d62" : "#0a7a2f"; } function bankName(bankKey){ return bankKey==="BEA" ? "BEA (EMP)" : "CPA (LCA)"; } function bankRib(bankKey){ return bankKey==="BEA" ? (state.params.ribBEA||"") : (state.params.ribCPA||""); } function bankProduct(bankKey){ return bankKey==="BEA" ? "EMP" : "LCA"; } function bordereauCashTotal(day, bankKey){ const prod = bankProduct(bankKey); return txOfDay(day).reduce((sum, tx)=>{ const isBankCash = tx.mode==="Espèces" && tx.produit===prod; if(!isBankCash) return sum; return sum + (Number(tx.amount)||0); }, 0); } function bordereauAmount(day, bankKey){ const s = computeDeposits(day); return (bankKey==="BEA") ? (Number(s.depBEA)||0) : (Number(s.depCPA)||0); } function renderBordereau(){ const day=state.params.day; $("bDay").textContent = day; const amount = bordereauAmount(day, activeBank); $("bAmountLabel").textContent = (activeBank==="BEA") ? "MONTANT A VERSER BEA (EMP)" : "MONTANT A VERSER CPA (LCA)"; $("bAmount").textContent = fmt(amount); $("bNo").textContent = currentBordNo(day, activeBank); $("tabBEA").classList.toggle("active", activeBank==="BEA"); $("tabCPA").classList.toggle("active", activeBank==="CPA"); $("bankBar").style.background = bankColor(activeBank); // Fond / Rendu (extra fields for printing) try{ const ex = getBordExtra(day, activeBank); const curFond = Math.max(0, round2(parseAmount($("fond")?.value))); const curRendu = Math.max(0, round2(parseAmount($("renduAmount")?.value))); const fondVal = (ex && ex.fond!=null) ? Number(ex.fond)||0 : curFond; const renduVal = (ex && ex.rendu!=null) ? Number(ex.rendu)||0 : curRendu; if(document.getElementById("bFond")) $("bFond").textContent = fmt(fondVal); if(document.getElementById("bRendu")) $("bRendu").textContent = fmt(renduVal); }catch(e){} const den=ensureAutoDenoms(day, activeBank); const canEditDenoms = hasCapability("manageData"); $("denomsTbody").innerHTML = DENOMS.map(d=>{ const qty=Number(den[d.key]||0); const sub=qty*d.value; const label = String(d.value); return ` ${d.type} ${label} ${fmt(sub)} `; }).join(""); $("denomsTbody").querySelectorAll("input[data-denkey]").forEach(inp=>{ inp.addEventListener("input", ()=>{ if(!hasCapability("manageData")) return; den._manual = true; const k=inp.getAttribute("data-denkey"); den[k]=Math.max(0, Number(inp.value)||0); save(); renderBordTotals(); }); }); renderBordTotals(); } function renderBordTotals(){ const day=state.params.day; const amount = bordereauAmount(day, activeBank); const total = denomTotal(day, activeBank); $("denTotal").textContent = fmt(total); const diff = round2(total - amount); $("denDiff").textContent = fmt(diff); if(diff===0){ $("denStatus").textContent = "Conforme"; $("denStatus").className = "ok"; } else { $("denStatus").textContent = "Écart détecté"; $("denStatus").className = "bad"; } } function renderAll(){ renderParams(); renderClients(); renderTx(); renderDec(); renderSortButtons(); renderFilterPresetButtons(); renderExecutiveDashboard(); renderSummary(); renderBordereau(); renderAccessAudit(); renderAutoBackupInfo(); renderUndoState(); renderDensityToggle(); if(cmdPaletteOpen) renderCommandPaletteResults(); queueFormDraftSave(260); } let focusedSectionId = null; function getSectionFromHash(hash){ if(!hash || !hash.startsWith("#")) return null; const node = document.querySelector(hash); if(!node) return null; return node.closest("section.card"); } function setCurrentNavLink(activeLink){ document.querySelectorAll("#nav a.nav-link").forEach(a=> a.classList.remove("is-current")); if(activeLink) activeLink.classList.add("is-current"); } function focusSectionByHash(hash, activeLink=null){ const grid = document.querySelector(".grid"); const section = getSectionFromHash(hash); if(!grid || !section || section.id==="nav") return; focusedSectionId = section.id; grid.classList.add("section-focus"); document.querySelectorAll(".grid section.card").forEach(sec=> sec.classList.remove("is-active")); section.classList.add("is-active"); const nav = document.getElementById("nav"); if(nav) nav.classList.add("is-active"); if(activeLink){ setCurrentNavLink(activeLink); } else { const autoLink = document.querySelector(`#nav a.nav-link[href="${hash}"]`); setCurrentNavLink(autoLink); } section.scrollIntoView({behavior:"smooth", block:"start"}); // Ensure sticky fields are applied when switching sections setTimeout(()=>{ try{ restoreStickyInputs(); }catch(e){} }, 0); } /* Table actions */ window.uiDelClient = (id)=>{ if(!requireCapability("manageData", "Supprimer un client")) return; if(!requirePin("Supprimer un client")) return; if(confirm("Supprimer le client ?")){ deleteClient(Number(id)); renderAll(); } }; window.uiEditClient = (id)=>{ openClientModal({ id: Number(id) }); }; window.uiDelDec = (id)=>{ if(!requireCapability("manageData", "Supprimer le décaissement")) return; if(!requirePin('Supprimer le décaissement')) return; if(confirm('Supprimer le décaissement ?')){ deleteDec(id); renderAll(); } }; window.uiEditDec = (id)=>{ if(!requireCapability("manageData", "Modifier le décaissement")) return; openDecModal({ id: String(id) }); }; window.uiDelTx = (id)=>{ if(!requireCapability("manageData", "Supprimer une opération")) return; if(!requirePin("Supprimer une opération")) return; if(confirm("Supprimer l’opération ?")){ deleteTx(id); renderAll(); } }; window.uiEditTx = (id)=>{ if(!requireCapability("manageData", "Modifier une opération")) return; openTxModal({ id: String(id) }); }; /* Events */ const renderClientsDebounced = debounce(()=> renderClients(), SEARCH_DEBOUNCE_MS); const renderTxDebounced = debounce(()=> renderTx(), SEARCH_DEBOUNCE_MS); const renderDecDebounced = debounce(()=> renderDec(), SEARCH_DEBOUNCE_MS); const renderSearchViewsDebounced = debounce(()=>{ renderClients(); renderTx(); renderDec(); }, SEARCH_DEBOUNCE_MS); if(document.getElementById("toggleTheme")){ $("toggleTheme").addEventListener("click", toggleTheme); } if(document.getElementById("cycleSkinBtn")){ $("cycleSkinBtn").addEventListener("click", cycleSkin); } if(document.getElementById("topCompactToggle")){ $("topCompactToggle").addEventListener("click", toggleDensity); } if(document.getElementById("undoBtn")){ $("undoBtn").addEventListener("click", applyUndo); } if(document.getElementById("globalSearch")){ $("globalSearch").addEventListener("input", ()=>{ clientsPage = 1; txPage = 1; decPage = 1; renderSearchViewsDebounced(); }); } if(document.getElementById("clearGlobalSearchBtn")){ $("clearGlobalSearchBtn").addEventListener("click", ()=>{ if(document.getElementById("globalSearch")) $("globalSearch").value = ""; txPresetFilter = "all"; decPresetFilter = "all"; clientsPage = 1; txPage = 1; decPage = 1; renderClients(); renderTx(); renderDec(); renderFilterPresetButtons(); }); } document.querySelectorAll(".sort-btn[data-sort-table][data-sort-key]").forEach((btn)=>{ btn.addEventListener("click", (e)=>{ const table = btn.getAttribute("data-sort-table"); const key = btn.getAttribute("data-sort-key"); if(!table || !key) return; const multi = !!(e.shiftKey || e.ctrlKey || e.metaKey); toggleTableSort(table, key, { multi }); }); }); document.querySelectorAll(".filter-pill[data-filter-group][data-filter-value]").forEach((btn)=>{ btn.addEventListener("click", ()=>{ const group = String(btn.getAttribute("data-filter-group")||""); const value = String(btn.getAttribute("data-filter-value")||"all"); if(group === "txPreset"){ txPresetFilter = value; txPage = 1; renderTx(); renderFilterPresetButtons(); return; } if(group === "decPreset"){ decPresetFilter = value; decPage = 1; renderDec(); renderFilterPresetButtons(); } }); }); if(document.getElementById("topPrintReport")){ $("topPrintReport").addEventListener("click", ()=> $("printReportBtn").click()); } if(document.getElementById("topPrintBord")){ $("topPrintBord").addEventListener("click", ()=> $("printBordBtn").click()); } if(document.getElementById("topPrintPack")){ $("topPrintPack").addEventListener("click", ()=> $("printPackBtn").click()); } if(document.getElementById("navForceSaveBtn")){ $("navForceSaveBtn").addEventListener("click", async ()=>{ await runManualFullSave({alert:true}); }); } if(document.getElementById("topSwitchRole")){ $("topSwitchRole").addEventListener("click", ()=> switchRoleInteractive()); } if(document.getElementById("topLockSession")){ $("topLockSession").addEventListener("click", ()=>{ if(sessionLocked){ unlockSessionInteractive("Déverrouillage manuel"); }else{ lockSession("manuel"); } }); } if(document.getElementById("openCmdPaletteBtn")){ $("openCmdPaletteBtn").addEventListener("click", ()=> openCommandPalette()); } if(document.getElementById("commandPaletteCloseBtn")){ $("commandPaletteCloseBtn").addEventListener("click", ()=> closeCommandPalette()); } if(document.getElementById("commandPaletteBackdrop")){ $("commandPaletteBackdrop").addEventListener("click", (e)=>{ if(e.target === $("commandPaletteBackdrop")) closeCommandPalette(); }); } if(document.getElementById("cmdPaletteInput")){ $("cmdPaletteInput").addEventListener("input", ()=>{ cmdPaletteActiveIndex = 0; renderCommandPaletteResults(); }); } if(document.getElementById("logoFileInput")){ $("logoFileInput").addEventListener("change", async ()=>{ await handleLogoFileChange($("logoFileInput")); }); } if(document.getElementById("saveUiSettingsBtn")){ $("saveUiSettingsBtn").addEventListener("click", ()=>{ saveUiSettingsOnly({ requirePin:false }); }); } if(document.getElementById("saveLogoSettingsBtn")){ $("saveLogoSettingsBtn").addEventListener("click", ()=>{ saveLogoSettingsOnly({ requirePin:false }); }); } if(document.getElementById("resetUiSettingsBtn")){ $("resetUiSettingsBtn").addEventListener("click", ()=>{ if(!requireCapability("manageConfig", "Réinitialiser interface")) return; resetUiSettingsUiValues(); applyUiSettings(readUiSettingsFromUi(readUiSettingsFromState())); }); } for(const id of [ "uiBaseFontSize","uiButtonFontSize","uiButtonPadY","uiButtonPadX","uiButtonRadius", "uiFieldFontSize","uiFieldPadY","uiFieldRadius", "uiNavFontSize","uiNavItemMinHeight","uiNavWidth","uiTabFontSize" ]){ const el = document.getElementById(id); if(!el) continue; el.addEventListener("input", ()=> applyUiSettings(readUiSettingsFromUi(readUiSettingsFromState()))); el.addEventListener("change", ()=> applyUiSettings(readUiSettingsFromUi(readUiSettingsFromState()))); } if(document.getElementById("logoPreviewBank")){ $("logoPreviewBank").addEventListener("change", ()=> renderLogoBordPreview()); } window.addEventListener("resize", ()=> fitLogoBordPreviewScale()); if(document.getElementById("openLogoEditorBtn")){ $("openLogoEditorBtn").addEventListener("click", ()=> openLogoEditorModal()); } if(document.getElementById("logoEditorCloseBtn")){ $("logoEditorCloseBtn").addEventListener("click", closeLogoEditorModal); } if(document.getElementById("logoEditorCancelBtn")){ $("logoEditorCancelBtn").addEventListener("click", closeLogoEditorModal); } if(document.getElementById("logoEditorBackdrop")){ $("logoEditorBackdrop").addEventListener("click", (e)=>{ if(e.target === $("logoEditorBackdrop")) closeLogoEditorModal(); }); } if(document.getElementById("logoEditorApplyBtn")){ $("logoEditorApplyBtn").addEventListener("click", ()=> applyLogoEditorResult()); } if(document.getElementById("logoRotateLeftBtn")){ $("logoRotateLeftBtn").addEventListener("click", ()=> logoEditorRotateBy(-90)); } if(document.getElementById("logoRotateRightBtn")){ $("logoRotateRightBtn").addEventListener("click", ()=> logoEditorRotateBy(90)); } if(document.getElementById("logoEditorFullFrameBtn")){ $("logoEditorFullFrameBtn").addEventListener("click", ()=> logoEditorApplyPresetFullFrame()); } if(document.getElementById("logoEditorResetBtn")){ $("logoEditorResetBtn").addEventListener("click", ()=> logoEditorResetAll()); } if(document.getElementById("logoEditorRotation") && document.getElementById("logoEditorRotationNumber")){ $("logoEditorRotation").addEventListener("input", ()=>{ if(document.getElementById("logoEditorRotationNumber")) $("logoEditorRotationNumber").value = $("logoEditorRotation").value; syncLogoEditorFromInputsAndRender(); }); $("logoEditorRotation").addEventListener("change", ()=>{ if(document.getElementById("logoEditorRotationNumber")) $("logoEditorRotationNumber").value = $("logoEditorRotation").value; syncLogoEditorFromInputsAndRender(); }); $("logoEditorRotationNumber").addEventListener("input", ()=>{ if(document.getElementById("logoEditorRotation")) $("logoEditorRotation").value = $("logoEditorRotationNumber").value; syncLogoEditorFromInputsAndRender(); }); $("logoEditorRotationNumber").addEventListener("change", ()=>{ if(document.getElementById("logoEditorRotation")) $("logoEditorRotation").value = $("logoEditorRotationNumber").value; syncLogoEditorFromInputsAndRender(); }); } for(const id of ["logoEditorCropX", "logoEditorCropY", "logoEditorCropW", "logoEditorCropH"]){ const el = document.getElementById(id); if(!el) continue; el.addEventListener("input", ()=> syncLogoEditorFromInputsAndRender()); el.addEventListener("change", ()=> syncLogoEditorFromInputsAndRender()); } if(document.getElementById("logoEditorCanvas")){ $("logoEditorCanvas").addEventListener("pointerdown", onLogoEditorPointerDown); $("logoEditorCanvas").addEventListener("pointermove", onLogoEditorPointerMove); $("logoEditorCanvas").addEventListener("pointerup", onLogoEditorPointerUp); $("logoEditorCanvas").addEventListener("pointercancel", onLogoEditorPointerUp); $("logoEditorCanvas").addEventListener("pointerleave", onLogoEditorPointerUp); } if(document.getElementById("removeCustomLogoBtn")){ $("removeCustomLogoBtn").addEventListener("click", ()=>{ if(!requireCapability("manageConfig", "Supprimer logo personnalisé")) return; logoDraftSrc = ""; logoDraftFileName = ""; if(document.getElementById("logoFileInput")) $("logoFileInput").value = ""; applyLogoFromUi(); }); } if(document.getElementById("resetLogoSettingsBtn")){ $("resetLogoSettingsBtn").addEventListener("click", ()=>{ if(!requireCapability("manageConfig", "Réinitialiser les réglages logo")) return; resetLogoSettingsUiValues(); applyLogoFromUi(); }); } for(const id of [ "logoSize","logoOffsetX","logoOffsetY","logoClarity","logoOpacity", "printLogoSize","printLogoOffsetX","printLogoOffsetY","printLogoOpacity", "watermarkScale","watermarkOffsetX","watermarkOffsetY","watermarkOpacity" ]){ const el = document.getElementById(id); if(!el) continue; el.addEventListener("input", ()=> applyLogoFromUi()); el.addEventListener("change", ()=> applyLogoFromUi()); } if(document.getElementById("switchRoleBtn")){ $("switchRoleBtn").addEventListener("click", ()=> switchRoleInteractive()); } for(const id of ["auditFilter","auditRoleFilter","auditDateFrom","auditDateTo"]){ const el = document.getElementById(id); if(!el) continue; el.addEventListener("input", onAuditFiltersChanged); el.addEventListener("change", onAuditFiltersChanged); } if(document.getElementById("auditPageSize")){ $("auditPageSize").addEventListener("change", onAuditFiltersChanged); } if(document.getElementById("auditResetFiltersBtn")){ $("auditResetFiltersBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Réinitialiser les filtres d'audit")) return; resetAuditFilters(); }); } if(document.getElementById("auditPresetTodayBtn")){ $("auditPresetTodayBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Filtrer l'audit: aujourd'hui")) return; setAuditDatePreset(1); }); } if(document.getElementById("auditPreset7dBtn")){ $("auditPreset7dBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Filtrer l'audit: 7 jours")) return; setAuditDatePreset(7); }); } if(document.getElementById("auditPreset30dBtn")){ $("auditPreset30dBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Filtrer l'audit: 30 jours")) return; setAuditDatePreset(30); }); } if(document.getElementById("auditPrevPageBtn")){ $("auditPrevPageBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Page précédente audit")) return; auditPage = Math.max(1, auditPage - 1); renderAccessAudit(); }); } if(document.getElementById("auditNextPageBtn")){ $("auditNextPageBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Page suivante audit")) return; const totalPages = Math.max(1, Math.ceil(getFilteredAuditRows().length / readAuditPageSize())); auditPage = Math.min(totalPages, auditPage + 1); renderAccessAudit(); }); } if(document.getElementById("exportAuditBtn")){ $("exportAuditBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Exporter le journal d'audit")) return; if(!requireCapability("exportAudit", "Exporter le journal d'audit")) return; const filtered = getFilteredAuditRows(); const filters = getAuditFilters(); const rows = [["ts","day","role","action","details"]]; for(const a of filtered){ rows.push([a.ts||"", getAuditDateKey(a), a.role||"", a.action||"", a.details||""]); } const csv = rows.map(r=> r.map(v=>{ const s = String(v??""); return /[",\n]/.test(s) ? `"${s.replaceAll('"','""')}"` : s; }).join(",")).join("\n"); const blob = new Blob([csv], {type:"text/csv;charset=utf-8"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `audit_${state.params.day||nowDate()}_filtre.csv`; a.click(); URL.revokeObjectURL(a.href); addAudit("AUDIT_EXPORT", `lignesFiltrees=${filtered.length}; role=${filters.role||"tous"}; de=${filters.from||"-"}; a=${filters.to||"-"}`); }); } if(document.getElementById("printAuditBtn")){ $("printAuditBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Imprimer le journal d'audit")) return; if(!requireCapability("printAudit", "Imprimer le journal d'audit")) return; const filtered = getFilteredAuditRows(); const PRINT_LIMIT = 2000; const rows = filtered.slice(0, PRINT_LIMIT); const isTrimmed = filtered.length > rows.length; const filters = getAuditFilters(); const node = document.createElement("div"); node.id = "printAudit"; const rolePart = filters.role ? roleLabel(filters.role) : "Tous"; const fromPart = filters.from || "—"; const toPart = filters.to || "—"; const textPart = filters.qRaw ? escapeHtml(filters.qRaw) : "—"; node.innerHTML = `

    JOURNAL D'AUDIT

    Rôle: ${escapeHtml(rolePart)} | De: ${escapeHtml(fromPart)} | À: ${escapeHtml(toPart)} | Texte: ${textPart}
    Généré le ${formatAuditTs(new Date().toISOString())}
    ${rows.map(a=>` `).join("") || ``}
    Total filtré: ${filtered.length}. Lignes imprimées: ${rows.length}${isTrimmed ? " (limite technique atteinte)." : ""}.
    `; openPrintWindow("Journal audit", node); addAudit("AUDIT_PRINT", `lignesImprimees=${rows.length}; totalFiltre=${filtered.length}; tronque=${isTrimmed ? "oui" : "non"}`); }); } if(document.getElementById("printComplianceBtn")){ $("printComplianceBtn").addEventListener("click", ()=>{ if(!requireCapability("viewAudit", "Imprimer le rapport de conformité")) return; if(!requireCapability("printAudit", "Imprimer le rapport de conformité")) return; const filtered = getFilteredAuditRows(); const filters = getAuditFilters(); const stats = computeAuditStats(filtered); const alerts = computeAuditAnomalies(filtered); const health = computeAuditHealth(filtered, stats, alerts); const topActions = getTopAuditActions(filtered, 10); const node = document.createElement("div"); node.id = "printCompliance"; node.innerHTML = `

    RAPPORT DE CONFORMITÉ

    Périmètre: rôle=${escapeHtml(filters.role ? roleLabel(filters.role) : "Tous")} | de=${escapeHtml(filters.from || "—")} | à=${escapeHtml(filters.to || "—")} | texte=${escapeHtml(filters.qRaw || "—")}
    Généré le ${formatAuditTs(new Date().toISOString())}

    Top actions

    ${topActions.map(a=>` `).join("") || ``}

    Alertes détectées

    ${alerts.map(a=>` `).join("") || ``}

    Recommandations prioritaires

    `; openPrintWindow("Rapport conformité", node); addAudit("COMPLIANCE_PRINT", `lignes=${filtered.length}; alertes=${alerts.length}; health=${health.score}`); }); } if(document.getElementById("clearAuditBtn")){ $("clearAuditBtn").addEventListener("click", ()=>{ if(!requireCapability("clearAudit", "Vider le journal d'audit")) return; if(!requirePin("Vider le journal d'audit", "admin")) return; if(!confirm("Vider le journal d'audit ?")) return; captureUndo("Vider journal d'audit"); state.audit = []; if(!save()) return; addAudit("AUDIT_CLEAR", "journal réinitialisé"); renderAccessAudit(); }); } document.querySelectorAll('#nav a.nav-link[href^="#"]').forEach(link=>{ link.addEventListener("click", (e)=>{ e.preventDefault(); const hash = link.getAttribute("href"); focusSectionByHash(hash, link); try{ history.replaceState(null, "", hash); }catch(err){} }); }); /* Navigation : garder l’élément actif synchronisé (scroll/clic/hash) */ function syncNavHighlight(){ const links = Array.from(document.querySelectorAll('#nav a.nav-link[href^="#"]')); const offset = 120; // hauteur approximative de la topbar let bestLink = null; let bestScore = Infinity; links.forEach(a=>{ const href = a.getAttribute('href') || ''; if(!href.startsWith('#')) return; const id = href.slice(1); const sec = document.getElementById(id); if(!sec) return; const rect = sec.getBoundingClientRect(); if(rect.height <= 0) return; const visible = rect.bottom > offset && rect.top < window.innerHeight * 0.75; if(!visible) return; const score = Math.abs(rect.top - offset); if(score < bestScore){ bestScore = score; bestLink = a; } }); if(bestLink) setCurrentNavLink(bestLink); } let _navTick = false; window.addEventListener('scroll', ()=>{ if(_navTick) return; _navTick = true; requestAnimationFrame(()=>{ syncNavHighlight(); _navTick = false; }); }, {passive:true}); window.addEventListener('hashchange', ()=>{ const hash = window.location.hash; const a = document.querySelector(`#nav a.nav-link[href="${hash}"]`); if(a) setCurrentNavLink(a); else syncNavHighlight(); }); document.addEventListener('click', (e)=>{ const sec = e.target.closest('section.card'); if(sec && sec.id && sec.id !== 'nav'){ const a = document.querySelector(`#nav a.nav-link[href="#${sec.id}"]`); if(a) setCurrentNavLink(a); } }); function applyDayChange(rawDay){ const nextDay = normalizeDate(rawDay, ""); if(!nextDay) return false; const oldDay = state.params.day; if(oldDay !== nextDay){ captureUndo(`Changement de jour ${oldDay||"—"} -> ${nextDay}`); } state.params.day = nextDay; if(!save()) return false; if(document.getElementById("day")) $("day").value = nextDay; if(document.getElementById("txDate")) $("txDate").value = nextDay; if(document.getElementById("decDate")) $("decDate").value = nextDay; renderAll(); try{ const el = document.getElementById("topDay"); if(el) el.textContent = nextDay; }catch(e){} if(oldDay !== nextDay){ addAudit("DAY_CHANGE", `${oldDay||"—"} -> ${nextDay}`); } return true; } const DATE_INPUT_ERROR = "Date invalide. Exemples acceptés: 2026-02-20, 20/02/2026."; $("saveParamsBtn").addEventListener("click", ()=>{ if(!requireCapability("manageConfig", "Enregistrer les paramètres")) return; if(!requirePin("Enregistrer les paramètres")) return; const nextDay = normalizeDate($("day").value, ""); if(!nextDay) return alert(DATE_INPUT_ERROR); captureUndo(`Modification paramètres (jour ${state.params.day||"—"})`); state.params.day = nextDay; state.params.fond = Math.max(0, round2(parseAmount($("fond").value))); state.params.ribBEA = asSafeText($("ribBEA").value, 80); state.params.ribCPA = asSafeText($("ribCPA").value, 80); state.params.adresse = asSafeText($("adresse").value, 240); state.params.rc = asSafeText($("rc").value, 100); state.params.nif = asSafeText($("nif").value, 100); state.params.sessionTimeoutMin = normalizeSessionTimeoutMinutes($("sessionTimeoutMin")?.value, state.params.sessionTimeoutMin || 15); const uiSettingsFromUi = readUiSettingsFromUi(readUiSettingsFromState()); state.params.uiBaseFontSize = uiSettingsFromUi.uiBaseFontSize; state.params.uiButtonFontSize = uiSettingsFromUi.uiButtonFontSize; state.params.uiButtonPadY = uiSettingsFromUi.uiButtonPadY; state.params.uiButtonPadX = uiSettingsFromUi.uiButtonPadX; state.params.uiButtonRadius = uiSettingsFromUi.uiButtonRadius; state.params.uiFieldFontSize = uiSettingsFromUi.uiFieldFontSize; state.params.uiFieldPadY = uiSettingsFromUi.uiFieldPadY; state.params.uiFieldRadius = uiSettingsFromUi.uiFieldRadius; state.params.uiNavFontSize = uiSettingsFromUi.uiNavFontSize; state.params.uiNavItemMinHeight = uiSettingsFromUi.uiNavItemMinHeight; state.params.uiNavWidth = uiSettingsFromUi.uiNavWidth; state.params.uiTabFontSize = uiSettingsFromUi.uiTabFontSize; const logoSettingsFromUi = readLogoSettingsFromUi(readLogoSettingsFromState()); state.params.logoSize = logoSettingsFromUi.logoSize; state.params.logoOffsetX = logoSettingsFromUi.logoOffsetX; state.params.logoOffsetY = logoSettingsFromUi.logoOffsetY; state.params.logoClarity = logoSettingsFromUi.logoClarity; state.params.logoOpacity = logoSettingsFromUi.logoOpacity; state.params.printLogoSize = logoSettingsFromUi.printLogoSize; state.params.printLogoOffsetX = logoSettingsFromUi.printLogoOffsetX; state.params.printLogoOffsetY = logoSettingsFromUi.printLogoOffsetY; state.params.printLogoOpacity = logoSettingsFromUi.printLogoOpacity; state.params.watermarkScale = logoSettingsFromUi.watermarkScale; state.params.watermarkOffsetX = logoSettingsFromUi.watermarkOffsetX; state.params.watermarkOffsetY = logoSettingsFromUi.watermarkOffsetY; state.params.watermarkOpacity = logoSettingsFromUi.watermarkOpacity; if(logoDraftSrc !== null){ state.params.logoSrc = sanitizeLogoSrc(logoDraftSrc); }else{ state.params.logoSrc = sanitizeLogoSrc(state.params.logoSrc); } if(currentRole === "admin"){ state.params.pin = asSafeText($("pin").value, 32); // legacy compatibility state.params.pinAdmin = asSafeText($("pin").value, 32); state.params.pinCashier = asSafeText($("pinCashier")?.value, 32); state.params.pinAudit = asSafeText($("pinAudit")?.value, 32); }else{ // Non-admin users can save operational settings but must not overwrite role PINs. state.params.pin = asSafeText(state.params.pin, 32); state.params.pinAdmin = asSafeText(state.params.pinAdmin || state.params.pin, 32); state.params.pinCashier = asSafeText(state.params.pinCashier, 32); state.params.pinAudit = asSafeText(state.params.pinAudit, 32); } if(!save()) return; logoDraftSrc = null; logoDraftFileName = ""; if(document.getElementById("logoFileInput")) $("logoFileInput").value = ""; touchSessionActivity({force:true}); renderSessionStatus(); if(document.getElementById("txDate")) $("txDate").value = state.params.day; if(document.getElementById("decDate")) $("decDate").value = state.params.day; renderAll(); try{ const el = document.getElementById("topDay"); if(el) el.textContent = state.params.day; }catch(e){} addAudit("PARAMS_SAVE", `jour=${state.params.day}; logo_custom=${state.params.logoSrc ? "oui" : "non"}; btn=${state.params.uiButtonFontSize}px; nav=${state.params.uiNavFontSize}px`); }); $("day").addEventListener("change", ()=>{ if(applyDayChange($("day").value)) return; $("day").value = state.params.day || nowDate(); alert(DATE_INPUT_ERROR); }); $("newDayBtn").addEventListener("click", ()=>{ if(!requireCapability("manageDay", "Nouveau jour")) return; const d = prompt("Saisissez une date (YYYY-MM-DD ou JJ/MM/AAAA)", nowDate()); if(d === null) return; if(!applyDayChange(d)) alert(DATE_INPUT_ERROR); }); $("closeDayBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("manageDay", "Clôturer le jour")) return; if(!requirePin("Clôturer le jour")) return; const s = computeDeposits(day); const drawerBefore = (Number(s.fond)||0) + (Number(s.cashTotal)||0) - (Number(s.decCash)||0); const depositAll = (Number(s.depBEA)||0) + (Number(s.depCPA)||0); const expectedAfter = drawerBefore - depositAll; const issues=[]; // Bordereau denomination checks (only if there is a deposit amount) for(const bank of ["BEA","CPA"]){ const amt = bordereauAmount(day, bank); if(amt>0){ const diff = denomTotal(day, bank) - amt; if(diff!==0) issues.push(`Attention - ${bankName(bank)} : écart des coupures = ${fmtSigned(diff)} DZD`); } } // Drawer comptage check const r = (state.recon && state.recon[day]) ? state.recon[day] : null; if(!r || r.drawerCount==="" || r.drawerCount===null || typeof r.drawerCount==="undefined"){ issues.push("Attention - le comptage de caisse (après versement) n’a pas été enregistré."); } else { const diff = (Number(r.drawerCount)||0) - expectedAfter; if(diff!==0) issues.push(`Attention - écart du comptage de caisse = ${fmtSigned(diff)} DZD`); } let msg = "Clôturer le jour empêche d’ajouter des opérations à cette date. Confirmer ?"; if(issues.length){ msg = issues.join("\n") + "\n\n" + msg; } if(confirm(msg)){ captureUndo(`Clôture du jour ${day}`); state.closedDays[day]=true; if(!save()) return; createAutoBackup("day-close", {force:true}); addAudit("DAY_CLOSE", `jour=${day}`); renderAll(); } }); $("openDayBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("manageDay", "Ouvrir le jour")) return; if(!requirePin("Ouvrir le jour")) return; if(confirm("Ouvrir le jour permet d’ajouter/modifier les opérations pour cette date. Confirmer ?")){ captureUndo(`Réouverture du jour ${day}`); delete state.closedDays[day]; if(!save()) return; addAudit("DAY_OPEN", `jour=${day}`); renderAll(); } }); if(document.getElementById("drawerCount")){ $("drawerCount").addEventListener("input", ()=>{ const day = state.params.day; const s = computeDeposits(day); const drawerBefore = (Number(s.fond)||0) + (Number(s.cashTotal)||0) - (Number(s.decCash)||0); const depositAll = (Number(s.depBEA)||0) + (Number(s.depCPA)||0); const expectedAfter = drawerBefore - depositAll; updateReconDiff(expectedAfter); }); } if(document.getElementById("saveDrawerCountBtn")){ $("saveDrawerCountBtn").addEventListener("click", ()=>{ if(!requireCapability("manageData", "Enregistrer le comptage de caisse")) return; if(!requirePin("Enregistrer le comptage de caisse")) return; const day = state.params.day; const s = computeDeposits(day); const drawerBefore = (Number(s.fond)||0) + (Number(s.cashTotal)||0) - (Number(s.decCash)||0); const depositAll = (Number(s.depBEA)||0) + (Number(s.depCPA)||0); const expectedAfter = drawerBefore - depositAll; const r = getRecon(day); captureUndo(`Comptage caisse ${day}`); r.drawerCount = ($("drawerCount").value ?? "").toString(); r.note = ($("drawerNote").value||"").trim(); r.savedAt = day + " " + nowTime(); if(!save()) return; addAudit("DRAWER_COUNT_SAVE", `jour=${day} montant=${fmt(parseAmount(r.drawerCount))}`); updateReconDiff(expectedAfter); alert("Comptage de caisse enregistré."); }); } $("exportJsonBtn").addEventListener("click", ()=>{ if(!requireCapability("export", "Export JSON")) return; const payload = { app: "caissier-ems-saida", version: APP_VERSION, exportedAt: new Date().toISOString(), state }; const blob=new Blob([JSON.stringify(payload,null,2)], {type:"application/json"}); const a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=`backup_caissier_${state.params.day||nowDate()}.json`; a.click(); URL.revokeObjectURL(a.href); addAudit("EXPORT_JSON", `jour=${state.params.day||nowDate()}`); }); if(document.getElementById("createAutoBackupBtn")){ $("createAutoBackupBtn").addEventListener("click", ()=>{ if(!requireCapability("export", "Créer un auto-backup")) return; const ok = createAutoBackup("manual", {force:true}); if(ok){ addAudit("AUTO_BACKUP_CREATE", `jour=${state.params.day||nowDate()}`); alert("Auto-backup local créé."); }else{ alert("Échec de création de l'auto-backup."); } renderAutoBackupInfo(); }); } if(document.getElementById("restoreAutoBackupBtn")){ $("restoreAutoBackupBtn").addEventListener("click", restoreAutoBackupInteractive); } $("importFile").addEventListener("change", async (e)=>{ const file=e.target.files?.[0]; if(!file) return; if(!requireCapability("manageImports", "Importer une sauvegarde JSON")){ e.target.value=""; return; } if(!requirePin("Importer une sauvegarde JSON")){ e.target.value=""; return; } try{ const text = await file.text(); const parsed = JSON.parse(text); const candidate = (parsed && typeof parsed === "object" && parsed.state && typeof parsed.state === "object") ? parsed.state : parsed; const previous = state; captureUndo(`Import JSON (${asSafeText(file.name, 80)})`); state = normalizeState(candidate); if(!save()){ state = previous; return; } addAudit("IMPORT_JSON", `fichier=${asSafeText(file.name, 120)}`); renderAll(); alert("Restauration réussie"); }catch(err){ alert("Échec de la restauration : fichier JSON invalide ou incompatible."); }finally{ e.target.value=""; } }); // --- Import Clients from Excel (CSV or Excel 2003 XML) --- function detectDelim(sample){ const delims = [";","\t",","]; let best = ",", bestScore = -1; for(const d of delims){ const c = (sample.split(d).length - 1); if(c > bestScore){ bestScore = c; best = d; } } return best; } function parseCSVLine(line, delim){ const out = []; let cur = ""; let inQ = false; for(let i=0;iString(s??"").trim()); } function parseDelimitedText(text){ const raw = String(text||"").replace(/\r/g,""); const lines = raw.split("\n").map(l=>l.trim()).filter(l=>l!==""); if(!lines.length) return []; const sample = lines.slice(0,5).join("\n"); const delim = detectDelim(sample); return lines.map(l=>parseCSVLine(l, delim)); } function parseExcel2003Xml(xmlText){ const doc = new DOMParser().parseFromString(xmlText, "text/xml"); // SpreadsheetML uses these tag names; keep it flexible const rows = Array.from(doc.getElementsByTagName("Row")); const out = []; for(const r of rows){ const cells = Array.from(r.getElementsByTagName("Cell")); const vals = []; for(const c of cells){ const d = c.getElementsByTagName("Data")[0]; vals.push(d ? d.textContent : ""); } if(vals.length) out.push(vals.map(v=>String(v??"").trim())); } return out; } function importClientsRows(rows){ let added=0, updated=0, skipped=0; captureUndo("Import clients (Excel/CSV)"); const map = new Map((state.clients||[]).map(c=>[Number(c.id), {id:Number(c.id), name:String(c.name||"")}])); // detect header row (first cell not numeric) let start = 0; if(rows.length){ const maybeId = String(rows[0][0]??"").trim(); if(!maybeId || !Number.isFinite(Number(maybeId))) start = 1; } for(let i=start;ia.id-b.id); if(!save()) return; addAudit("IMPORT_CLIENTS", `ajoutés=${added} mis_a_jour=${updated} ignorés=${skipped}`); renderAll(); alert(`Import des clients effectué ✅\nAjoutés: ${added}\nMis à jour: ${updated}\nIgnorés: ${skipped}\nTotal: ${state.clients.length}`); } $("importClientsFile").addEventListener("change", async (e)=>{ const file = e.target.files?.[0]; if(!file) return; if(!requireCapability("manageImports", "Import Excel (Clients)")) { e.target.value=""; return; } if(!requirePin("Import Excel (Clients)")) { e.target.value=""; return; } try{ const text = await file.text(); const name = (file.name||"").toLowerCase(); let rows = []; if(name.endsWith(".xml") || String(file.type||"").includes("xml")){ rows = parseExcel2003Xml(text); }else{ // CSV/TXT from Excel (Save As CSV UTF-8) rows = parseDelimitedText(text); } if(!rows.length) return alert("Fichier vide ou illisible. Utilisez CSV (UTF-8) ou Excel 2003 XML."); importClientsRows(rows); }catch(err){ alert("Échec de l’import. Vérifiez le fichier (CSV UTF-8 ou Excel 2003 XML)."); }finally{ e.target.value=""; } }); $("exportCsvBtn").addEventListener("click", ()=>{ if(!requireCapability("export", "Export CSV (jour)")) return; const day=state.params.day; const rows=[["date","time","clientId","clientName","produit","receipt","mode","amount","obs"]]; for(const t of txOfDay(day).slice().reverse()){ rows.push([t.date,t.time,t.clientId,t.clientName,t.produit,t.receipt,t.mode,t.amount,t.obs]); } const csv=rows.map(r=>r.map(v=>{ const s=String(v??""); return /[",\n]/.test(s) ? `"${s.replaceAll('"','""')}"` : s; }).join(",")).join("\n"); const blob=new Blob([csv], {type:"text/csv;charset=utf-8"}); const a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=`encaissements_${day}.csv`; a.click(); URL.revokeObjectURL(a.href); addAudit("EXPORT_CSV", `jour=${day}`); }); // --- Export Excel (Jour) as SpreadsheetML (.xls) --- function xmlEsc(s){ return String(s??"") .replaceAll("&","&") .replaceAll("<","<") .replaceAll(">",">") .replaceAll('"',""") .replaceAll("'","'"); } function ssCell(v){ if(v===null || v===undefined) v=""; const isNum = (typeof v === "number") && Number.isFinite(v); const type = isNum ? "Number" : "String"; const val = isNum ? String(round2(v)) : xmlEsc(String(v)); return `${val}`; } function ssRow(arr){ return `${(arr||[]).map(ssCell).join("")}`; } function ssSheet(name, rows){ const safeName = xmlEsc(name).slice(0, 31); // Excel sheet name limit return `${(rows||[]).map(ssRow).join("")}
    `; } function exportExcelDay(){ const day = state.params.day; const s = computeDeposits(day); const recon = getRecon(day); // Daily monitoring const drawerBefore = (Number(s.fond)||0) + (Number(s.cashTotal)||0) - (Number(s.decCash)||0); const depositAll = (Number(s.depBEA)||0) + (Number(s.depCPA)||0); const drawerAfter = drawerBefore - depositAll; const counted = parseAmount(recon.drawerCount); const diff = counted - (Number(drawerAfter)||0); // Sheets const resumeRows = [ ["Date", day], ["Statut", isClosed(day) ? "Clôturé" : "Ouvert"], ["Fond de caisse", Number(s.fond)||0], ["Encaissements Espèces", Number(s.cashTotal)||0], ["Décaissements Espèces", Number(s.decCash)||0], ["Encaissements Carte", Number(s.card)||0], ["Encaissements Chèque", Number(s.cheque)||0], ["Dépôt BEA (EMP)", Number(s.depBEA)||0], ["Dépôt CPA (LCA)", Number(s.depCPA)||0], ["Total Dépôts", depositAll], ["Tiroir Avant Dépôts", drawerBefore], ["Tiroir Après Dépôts", drawerAfter], ["Comptage Tiroir (Saisi)", counted], ["Écart (Comptage - Attendu)", diff], ["Note", recon.note||""], ["Sauvegardé le", recon.savedAt||""], ]; const txs = txOfDay(day).slice().sort((a,b)=> String(a.time||"").localeCompare(String(b.time||""))); const txRows = [["date","time","clientId","clientName","produit","receipt","mode","amount","obs"]]; for(const t of txs){ txRows.push([t.date, t.time, t.clientId, t.clientName, t.produit, t.receipt, t.mode, Number(t.amount)||0, t.obs]); } const decs = decOfDay(day).slice().sort((a,b)=> String(a.time||"").localeCompare(String(b.time||""))); const decRows = [["date","time","label","mode","amount","obs"]]; for(const d of decs){ decRows.push([d.date, d.time, d.label, d.mode, Number(d.amount)||0, d.obs]); } const cashBEA = txs.filter(t=> t.mode==="Espèces" && t.produit==="EMP"); const cashCPA = txs.filter(t=> t.mode==="Espèces" && t.produit==="LCA"); const gBEA = groupCashTxByClient(cashBEA); const gCPA = groupCashTxByClient(cashCPA); const beaRows = [["time","clientId","clientName","receipts","amount"]]; for(const t of gBEA){ beaRows.push([t.time, t.clientId, t.clientName, t.receipt, Number(t.amount)||0]); } const cpaRows = [["time","clientId","clientName","receipts","amount"]]; for(const t of gCPA){ cpaRows.push([t.time, t.clientId, t.clientName, t.receipt, Number(t.amount)||0]); } function bordRows(bankKey, bankNameLabel){ const den = getDen(day, bankKey); const rows = [ ["Banque", bankNameLabel], ["Bordereau No", currentBordNo(day, bankKey)], ["Date", day], [""], ["Type","Valeur","Qté","Sous-total"] ]; for(const d of DENOMS){ const qty = Number(den[d.key]||0); const sub = qty * d.value; rows.push([d.type, d.value, qty, sub]); } const depAmt = (bankKey==="BEA") ? (Number(s.depBEA)||0) : (Number(s.depCPA)||0); rows.push(["", "", "TOTAL", depAmt]); return rows; } const workbook = ` ${ssSheet("Résumé", resumeRows)} ${ssSheet("Encaissements", txRows)} ${ssSheet("Décaissements", decRows)} ${ssSheet("Espèces BEA", beaRows)} ${ssSheet("Espèces CPA", cpaRows)} ${ssSheet("Bordereau BEA", bordRows("BEA","BEA"))} ${ssSheet("Bordereau CPA", bordRows("CPA","CPA"))} `; const blob = new Blob([workbook], {type:"application/vnd.ms-excel;charset=utf-8"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); a.download = `caisse_${day}.xls`; a.click(); URL.revokeObjectURL(a.href); } $("exportXlsBtn").addEventListener("click", ()=>{ if(!requireCapability("export", "Export Excel (jour)")) return; if(!requirePin("Export Excel")) return; exportExcelDay(); addAudit("EXPORT_XLS_DAY", `jour=${state.params.day}`); }); $("exportClientsXlsBtn").addEventListener("click", ()=>{ if(!requireCapability("export", "Export Excel (clients)")) return; if(!requirePin("Export Excel (Clients)")) return; const clients = (state.clients||[]).slice().sort((a,b)=> Number(a.id||0)-Number(b.id||0)); const rows = [["id","name"]]; for(const c of clients){ rows.push([Number(c.id)||0, String(c.name||"")]); } const workbook = ` ${ssSheet("Clients", rows)} `; const blob = new Blob([workbook], {type:"application/vnd.ms-excel;charset=utf-8"}); const a = document.createElement("a"); a.href = URL.createObjectURL(blob); const day = state.params.day || ""; a.download = `clients_${day || "export"}.xls`; a.click(); URL.revokeObjectURL(a.href); addAudit("EXPORT_XLS_CLIENTS", `total=${(state.clients||[]).length}`); }); $("exportPdfBtn").addEventListener("click", ()=>{ // Export PDF = imprimer le pack puis « Save as PDF » depuis la fenêtre d'impression if(!requireCapability("export", "Export PDF")) return; if(!requirePin("Export PDF")) return; alert("Remarque : dans la fenêtre d’impression, choisissez « Save as PDF » ou « Microsoft Print to PDF », puis sauvegardez le fichier."); addAudit("EXPORT_PDF_REQUEST", `jour=${state.params.day}`); $("printPackBtn").click(); }); $("saveClientBtn").addEventListener("click", ()=>{ if(!requireCapability("manageData", "Enregistrer un client")) return; if(!requirePin("Enregistrer un client")) return; const id = asPositiveInt($("clientId").value); const name = asSafeText($("clientName").value, 180); if(!id || !name) return alert("Saisissez le N° et le nom du client."); const existedBefore = !!clientById(id); if(!upsertClient(id,name)) return alert("Impossible d’enregistrer ce client."); clientsPage = 1; editClientId = id; $("clientId").value = String(id); $("clientName").value = clientNameById(id); renderAll(); refreshClientFormMode(); queueCriticalFullSave(); if(!existedBefore){ // Keep the new client in edit mode so the user can modify it right after save. const nameEl = document.getElementById("clientName"); if(nameEl) nameEl.focus(); } }); $("clientSearch").addEventListener("input", ()=>{ clientsPage = 1; renderClientsDebounced(); }); if(document.getElementById("clientsPrevPageBtn")){ $("clientsPrevPageBtn").addEventListener("click", ()=>{ clientsPage = Math.max(1, clientsPage - 1); renderClients(); }); } if(document.getElementById("clientsNextPageBtn")){ $("clientsNextPageBtn").addEventListener("click", ()=>{ clientsPage += 1; renderClients(); }); } if(document.getElementById("clientsPageSize")){ $("clientsPageSize").addEventListener("change", ()=>{ clientsPage = 1; renderClients(); }); } if(document.getElementById("openClientModalBtn")){ $("openClientModalBtn").addEventListener("click", ()=> openClientModal()); } if(document.getElementById("clientModalCloseBtn")){ $("clientModalCloseBtn").addEventListener("click", closeClientModal); } if(document.getElementById("clientModalCancelBtn")){ $("clientModalCancelBtn").addEventListener("click", closeClientModal); } if(document.getElementById("clientModalSaveBtn")){ $("clientModalSaveBtn").addEventListener("click", saveClientFromModal); } if(document.getElementById("clientModalName")){ $("clientModalName").addEventListener("keydown", (e)=>{ if(e.key === "Enter"){ e.preventDefault(); saveClientFromModal(); } }); } if(document.getElementById("clientModalBackdrop")){ $("clientModalBackdrop").addEventListener("click", (e)=>{ if(e.target === $("clientModalBackdrop")) closeClientModal(); }); } if(document.getElementById("openTxModalBtn")){ $("openTxModalBtn").addEventListener("click", ()=> openTxModal()); } if(document.getElementById("txModalCloseBtn")){ $("txModalCloseBtn").addEventListener("click", closeTxModal); } if(document.getElementById("txModalCancelBtn")){ $("txModalCancelBtn").addEventListener("click", closeTxModal); } if(document.getElementById("txModalSaveBtn")){ $("txModalSaveBtn").addEventListener("click", saveTxFromModal); } if(document.getElementById("txModalBackdrop")){ $("txModalBackdrop").addEventListener("click", (e)=>{ if(e.target === $("txModalBackdrop")) closeTxModal(); }); } if(document.getElementById("txModalClientId")){ $("txModalClientId").addEventListener("input", syncTxModalClientName); $("txModalClientId").addEventListener("change", syncTxModalClientName); } if(document.getElementById("txModalObs")){ $("txModalObs").addEventListener("keydown", (e)=>{ if(e.key === "Enter" && !e.shiftKey){ e.preventDefault(); saveTxFromModal(); } }); } if(document.getElementById("openDecModalBtn")){ $("openDecModalBtn").addEventListener("click", ()=> openDecModal()); } if(document.getElementById("decModalCloseBtn")){ $("decModalCloseBtn").addEventListener("click", closeDecModal); } if(document.getElementById("decModalCancelBtn")){ $("decModalCancelBtn").addEventListener("click", closeDecModal); } if(document.getElementById("decModalSaveBtn")){ $("decModalSaveBtn").addEventListener("click", saveDecFromModal); } if(document.getElementById("decModalBackdrop")){ $("decModalBackdrop").addEventListener("click", (e)=>{ if(e.target === $("decModalBackdrop")) closeDecModal(); }); } if(document.getElementById("decModalObs")){ $("decModalObs").addEventListener("keydown", (e)=>{ if(e.key === "Enter" && !e.shiftKey){ e.preventDefault(); saveDecFromModal(); } }); } document.addEventListener("keydown", (e)=>{ const key = String(e.key||""); const lower = key.toLowerCase(); const ctrl = !!(e.ctrlKey || e.metaKey); const clientOpen = isModalVisible("clientModalBackdrop"); const txOpen = isModalVisible("txModalBackdrop"); const decOpen = isModalVisible("decModalBackdrop"); const logoOpen = isModalVisible("logoEditorBackdrop"); const dataModalOpen = clientOpen || txOpen || decOpen || logoOpen; if(ctrl && lower === "k"){ e.preventDefault(); if(cmdPaletteOpen){ closeCommandPalette(); return; } if(dataModalOpen) return; openCommandPalette(); return; } if(!cmdPaletteOpen) return; if(key === "Escape"){ e.preventDefault(); closeCommandPalette(); return; } if(key === "ArrowDown"){ e.preventDefault(); moveCommandPaletteSelection(1); return; } if(key === "ArrowUp"){ e.preventDefault(); moveCommandPaletteSelection(-1); return; } if(key === "Enter"){ e.preventDefault(); executeCommandPaletteIndex(cmdPaletteActiveIndex); } }); document.addEventListener("keydown", (e)=>{ const clientOpen = isModalVisible("clientModalBackdrop"); const txOpen = isModalVisible("txModalBackdrop"); const decOpen = isModalVisible("decModalBackdrop"); const logoOpen = isModalVisible("logoEditorBackdrop"); const anyOpen = clientOpen || txOpen || decOpen || logoOpen; if(!anyOpen) return; const ctrl = !!(e.ctrlKey || e.metaKey); if(ctrl && (e.key === "s" || e.key === "S" || e.key === "Enter")){ e.preventDefault(); if(logoOpen) return applyLogoEditorResult(); if(txOpen) return saveTxFromModal(); if(decOpen) return saveDecFromModal(); if(clientOpen) return saveClientFromModal(); return; } if(ctrl && (e.key === "l" || e.key === "L")){ e.preventDefault(); if(logoOpen) return focusPrimaryModalField("logo"); if(txOpen) return focusPrimaryModalField("tx"); if(decOpen) return focusPrimaryModalField("dec"); if(clientOpen) return focusPrimaryModalField("client"); return; } if(e.key !== "Escape") return; if(logoOpen) closeLogoEditorModal(); if(clientOpen) closeClientModal(); if(txOpen) closeTxModal(); if(decOpen) closeDecModal(); }); if(document.getElementById("clientId")){ $("clientId").addEventListener("input", syncClientFormFromId); $("clientId").addEventListener("change", syncClientFormFromId); $("clientId").addEventListener("blur", syncClientFormFromId); } if(document.getElementById("clientName")){ $("clientName").addEventListener("input", refreshClientFormMode); } if(document.getElementById("cancelClientEditBtn")){ $("cancelClientEditBtn").addEventListener("click", ()=>{ if(!requireCapability("manageData", "Annuler la modification client")) return; resetClientForm(); queueFormDraftSave(60); }); } function updateAutoClientName(){ $("txClientName").value = clientNameById($("txClientId").value); } $("txClientId").addEventListener("input", updateAutoClientName); $("txClientId").addEventListener("change", updateAutoClientName); $("txClientId").addEventListener("blur", updateAutoClientName); // === Cash calculator for Encaissement / Décaissement === const formCashCalcState = { tx:{}, dec:{}, fond:{}, rendu:{} }; function formCashAmountInputId(kind){ if(kind==="tx") return "txAmount"; if(kind==="dec") return "decAmount"; if(kind==="fond") return "fond"; if(kind==="rendu") return "renduAmount"; return ""; } function formCashModeInputId(kind){ if(kind==="tx") return "txMode"; if(kind==="dec") return "decMode"; // Fond & rendu are always cash; no mode selector. return ""; } function formCashLabel(d){ if(d.value===200 && d.type==="Billet") return "200P"; if(d.value===200 && d.type==="Pièce") return "200M"; return String(d.value); } function formCashTotal(kind){ const data = formCashCalcState[kind] || {}; return DENOMS.reduce((sum,d)=> sum + (Number(data[d.key]||0) * d.value), 0); } function updateFormCashTotalUI(kind){ const totalEl = $(`${kind}CashTotal`); if(totalEl) totalEl.textContent = fmt(formCashTotal(kind)); } function setFormAmountFromCash(kind, forceClear=false){ const amountEl = $(formCashAmountInputId(kind)); if(!amountEl) return; const total = formCashTotal(kind); if(total>0) amountEl.value = round2(total).toFixed(2); else if(forceClear) amountEl.value = ""; } function renderFormCashGrid(kind){ const grid = $(`${kind}CashGrid`); if(!grid) return; const data = formCashCalcState[kind] || {}; grid.innerHTML = DENOMS.map(d=>{ const qty = Number(data[d.key]||0); return ` `; }).join(""); updateFormCashTotalUI(kind); } function syncFormCashCalcVisibility(kind){ const box = $(`${kind}CashCalc`); if(!box) return; const modeId = formCashModeInputId(kind); if(!modeId){ // Fond de caisse & rendu: always cash box.style.display = ""; return; } const modeEl = $(modeId); if(!modeEl) { box.style.display=""; return; } box.style.display = modeEl.value==="Espèces" ? "" : "none"; } function resetFormCashCalc(kind, clearAmount=true){ formCashCalcState[kind] = {}; renderFormCashGrid(kind); if(clearAmount) setFormAmountFromCash(kind, true); queueFormDraftSave(80); } function onFormModeChange(kind){ const modeEl = $(formCashModeInputId(kind)); if(!modeEl) return; if(modeEl.value!=="Espèces"){ formCashCalcState[kind] = {}; renderFormCashGrid(kind); } syncFormCashCalcVisibility(kind); queueFormDraftSave(80); } function initFormCashCalc(){ renderFormCashGrid("tx"); renderFormCashGrid("dec"); renderFormCashGrid("fond"); renderFormCashGrid("rendu"); syncFormCashCalcVisibility("tx"); syncFormCashCalcVisibility("dec"); syncFormCashCalcVisibility("fond"); syncFormCashCalcVisibility("rendu"); document.addEventListener("input", (e)=>{ const target = e.target; if(!(target instanceof HTMLInputElement)) return; const kind = target.getAttribute("data-cash-kind"); const key = target.getAttribute("data-denkey"); if(!kind || !key || !formCashCalcState[kind]) return; formCashCalcState[kind][key] = Math.max(0, Number(target.value)||0); updateFormCashTotalUI(kind); setFormAmountFromCash(kind, true); }); if($("txCashReset")) $("txCashReset").addEventListener("click", ()=> resetFormCashCalc("tx", true)); if($("decCashReset")) $("decCashReset").addEventListener("click", ()=> resetFormCashCalc("dec", true)); if($("fondCashReset")) $("fondCashReset").addEventListener("click", ()=> resetFormCashCalc("fond", true)); if($("renduCashReset")) $("renduCashReset").addEventListener("click", ()=> resetFormCashCalc("rendu", true)); if($("txMode")) $("txMode").addEventListener("change", ()=> onFormModeChange("tx")); if($("decMode")) $("decMode").addEventListener("change", ()=> onFormModeChange("dec")); } initFormCashCalc(); // === End cash calculator === // Auto-save "Rendu" per day (used in Rapport & Bordereau) if(document.getElementById("renduAmount")){ $("renduAmount").addEventListener("input", ()=>{ if(!hasCapability("manageData")) return; const day = state.params.day || nowDate(); const r = getRecon(day); r.rendu = ($("renduAmount").value ?? "").toString(); // do not touch drawerCount/note here save(); // Refresh bordereau header pills if visible try{ if(document.getElementById("bRendu")) $("bRendu").textContent = fmt(Math.max(0, round2(parseAmount($("renduAmount").value)))); }catch(e){} }); } $("txSearch").addEventListener("input", renderTxDebounced); $("txSearch").addEventListener("input", ()=>{ txPage = 1; }); $("txProdFilter").addEventListener("change", ()=>{ txPage = 1; renderTx(); }); if(document.getElementById("txPrevPageBtn")){ $("txPrevPageBtn").addEventListener("click", ()=>{ txPage = Math.max(1, txPage - 1); renderTx(); }); } if(document.getElementById("txNextPageBtn")){ $("txNextPageBtn").addEventListener("click", ()=>{ txPage += 1; renderTx(); }); } if(document.getElementById("txPageSize")){ $("txPageSize").addEventListener("change", ()=>{ txPage = 1; renderTx(); }); } $("cancelEditBtn").addEventListener("click", ()=>{ editId=null; $("saveTxBtn").textContent="Ajouter"; $("cancelEditBtn").style.display="none"; $("txClientId").value=""; $("txClientName").value=""; $("txReceipt").value=""; $("txAmount").value=""; $("txObs").value=""; $("txTime").value=nowTime(); resetFormCashCalc("tx", true); syncFormCashCalcVisibility("tx"); queueFormDraftSave(60); }); $("saveTxBtn").addEventListener("click", ()=>{ if(!requireCapability("manageData", "Enregistrer une opération")) return; const day = normalizeDate(state.params.day, nowDate()); if(isClosed(day)) return alert("Ce jour est clôturé. Changez la date ou ouvrez un nouveau jour."); const clientId = asPositiveInt($("txClientId").value); const name = asSafeText(clientNameById(clientId), 180); const amount = parseAmount($("txAmount").value); if(!clientId) return alert("Saisissez le N° client."); if(!name) return alert("Client introuvable dans la liste."); if(amount<=0) return alert("Saisissez un montant valide."); const produit = VALID_PRODUCTS.has(String($("txProduit").value||"")) ? $("txProduit").value : "EMP"; const mode = VALID_TX_MODES.has(String($("txMode").value||"")) ? $("txMode").value : "Espèces"; const tx={ day, date:normalizeDate($("txDate").value, day), time:normalizeTime($("txTime").value, nowTime()), clientId, clientName:name, receipt:asSafeText($("txReceipt").value, 80), produit, mode, amount:round2(amount), obs:asSafeText($("txObs").value, 240) }; if(!validateTxBeforeSave(tx, { isEdit: !!editId, editTxId: editId })){ return; } // Save denominations from cash calculator (Espèces) so they can auto-fill Bordereau if(tx.mode==="Espèces"){ const den = cleanDenoms(formCashCalcState.tx); if(Object.keys(den).length) tx.cashDenoms = den; } if(editId){ if(!requirePin("Modifier l’opération")) return; if(!updateTx(editId, tx)) return alert("Échec de la modification."); editId=null; $("saveTxBtn").textContent="Ajouter"; $("cancelEditBtn").style.display="none"; } else { tx.id = makeId("tx"); if(!addTx(tx)) return alert("Échec de l'enregistrement."); } $("txClientId").value=""; $("txClientName").value=""; $("txReceipt").value=""; $("txAmount").value=""; $("txObs").value=""; $("txTime").value=nowTime(); resetFormCashCalc("tx", true); syncFormCashCalcVisibility("tx"); txPage = 1; renderAll(); queueCriticalFullSave(); }); $("clearDayTxBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("manageData", "Réinitialiser les opérations du jour")) return; if(!requirePin("Réinitialiser les opérations du jour")) return; if(confirm("Réinitialiser les opérations du jour ?")){ clearDayTx(day); renderAll(); } }); $("tabBEA").addEventListener("click", ()=>{ activeBank="BEA"; renderBordereau(); }); $("tabCPA").addEventListener("click", ()=>{ activeBank="CPA"; renderBordereau(); }); $("resetDenBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("manageData", "Réinitialiser les coupures")) return; if(!requirePin("Réinitialiser les coupures")) return; // Reset to empty and keep manual mode (so auto does not refill immediately) captureUndo(`Réinitialisation coupures ${activeBank} ${day}`); state.denoms[activeBank][day] = {_manual:true}; if(!save()) return; addAudit("DENOMS_RESET", `jour=${day} banque=${activeBank}`); renderBordereau(); }); $("autoDenBtn").addEventListener("click", ()=>{ const day = state.params.day; if(!requireCapability("manageData", "Auto-remplir les coupures")) return; const den = getDen(day, activeBank); if(confirm("Remplir automatiquement les coupures depuis les opérations du jour / la calculatrice ? Les valeurs actuelles seront remplacées.")){ captureUndo(`Auto coupures ${activeBank} ${day}`); den._manual = false; if(!save()) return; addAudit("DENOMS_AUTO", `jour=${day} banque=${activeBank}`); renderBordereau(); } }); if($("copyFondRenduBtn")){ $("copyFondRenduBtn").addEventListener("click", ()=>{ if(!requireCapability("manageData", "Récupérer Fond")) return; const day = state.params.day; captureUndo(`Copie fond bordereau ${activeBank} ${day}`); const ex = getBordExtra(day, activeBank); ex.fond = Math.max(0, round2(parseAmount($("fond")?.value))); // Rendu non utilisé sur le bordereau // ex.rendu = Math.max(0, round2(parseAmount($("renduAmount")?.value))); if(!save()) return; addAudit("BORD_FOND_COPY", `jour=${day} banque=${activeBank} fond=${fmt(ex.fond)}`); renderBordereau(); alert("Fond récupéré pour le bordereau ("+bankName(activeBank)+")."); }); } $("newBordBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("manageData", "Nouveau bordereau")) return; if(!requirePin("Nouveau bordereau")) return; captureUndo(`Nouveau numéro bordereau ${activeBank} ${day}`); const no=nextBordNo(day, activeBank); $("bNo").textContent=no; addAudit("BORD_NEW_NUMBER", `jour=${day} banque=${activeBank} no=${no}`); alert("N° Bordereau: "+no); }); function setCompanyInfo(elId){ const parts=[]; if(state.params.adresse) parts.push(state.params.adresse); if(state.params.rc) parts.push("RC: "+state.params.rc); if(state.params.nif) parts.push("NIF: "+state.params.nif); const el = document.getElementById(elId); if(el) el.textContent = parts.join(" | "); } function getCompanyInfoText(){ const parts=[]; if(state.params.adresse) parts.push(state.params.adresse); if(state.params.rc) parts.push("RC: "+state.params.rc); if(state.params.nif) parts.push("NIF: "+state.params.nif); return parts.join(" | "); } function setBordereauLogo(){ applyLogoFromUi(); } $("printBordBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("printOps", "Imprimer le bordereau")) return; if(!requirePin("Imprimer le bordereau")) return; // Make sure denoms are up-to-date (auto-fill) ensureAutoDenoms(day, activeBank); const amount = bordereauAmount(day, activeBank); const no = $("bNo").textContent || currentBordNo(day, activeBank); const color = bankColor(activeBank); $("printReport").style.display="none"; $("printBord").style.display=""; try{ const bar=document.getElementById("p_bar"); if(bar) bar.style.background=color; }catch(e){} $("p_bank").textContent=bankName(activeBank); $("p_bank_code").textContent=activeBank; $("p_rib").textContent=bankRib(activeBank); $("p_day").textContent=day; $("p_no").textContent=no; $("p_amount_label").textContent = (activeBank==="BEA") ? "MONTANT A VERSER BEA (EMP):" : "MONTANT A VERSER CPA (LCA):"; $("p_amount").textContent=fmt(amount); $("p_words").textContent=amountToWordsDZD(amount); setCompanyInfo("p_company_info"); // Fond / Rendu in printed bordereau (stored per bank/day via button, otherwise read current inputs) try{ const ex = getBordExtra(day, activeBank); const fondVal = (ex && ex.fond!=null) ? Number(ex.fond)||0 : Math.max(0, round2(parseAmount($("fond")?.value))); const renduVal = (ex && ex.rendu!=null) ? Number(ex.rendu)||0 : Math.max(0, round2(parseAmount($("renduAmount")?.value))); if(document.getElementById("p_fond")) $("p_fond").textContent = fmt(fondVal); if(document.getElementById("p_rendu")) $("p_rendu").textContent = fmt(renduVal); }catch(e){} const den=ensureAutoDenoms(day, activeBank); const rows=DENOMS.filter(d=>Number(den[d.key]||0)>0).map(d=>{ const qty=Number(den[d.key]||0); const sub=qty*d.value; const label = String(d.value); return `${d.type}${label}${qty}${fmt(sub)}`; }).join(""); const total=denomTotal(day, activeBank); $("p_denoms").innerHTML = rows || `—`; $("p_den_total").textContent=fmt(total); // === Add encaissements list (Espèces only for this bank) === const prod = bankProduct(activeBank); let allTx = txOfDay(day).filter(t => t.mode==="Espèces" && t.produit===prod); // sort by time allTx = allTx.slice().sort((a,b)=> String(a.time||"").localeCompare(String(b.time||""))); if(document.getElementById("p_txs")){ const tbody = document.getElementById("p_txs"); const grouped = groupCashTxByClient(allTx); tbody.innerHTML = grouped.map((t,i)=>` ${i+1} ${t.clientId} ${escapeHtml(t.clientName)} ${fmt(t.amount)} `).join("") || `—`; const totalTx = grouped.reduce((s,t)=> s + (Number(t.amount)||0), 0); const elTot = document.getElementById("p_txs_total"); const elCnt = document.getElementById("p_txs_count"); if(elTot) elTot.textContent = fmt(totalTx); if(elCnt) elCnt.textContent = String(grouped.length); } setBordereauLogo(); openPrintWindow("Bordereau", $("printBord")); addAudit("PRINT_BORDEREAU", `jour=${day} banque=${activeBank}`); }); /* Pack print: Report + Bordereaux (BEA/CPA) in one print job */ $("printPackBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("printOps", "Imprimer le pack")) return; if(!requirePin("Imprimer le pack")) return; const pack = document.getElementById("printPack"); if(!pack) return alert("printPack introuvable."); // clean pack.innerHTML=""; pack.style.display=""; const oldActive = activeBank; // ---- Prepare Report (use existing logic by triggering fill then clone) ---- // Reuse report filler by programmatically clicking but without printing: // We will fill report fields inline (copied from printReportBtn handler logic). const list=txOfDay(day); const s=computeDeposits(day); $("printBord").style.display="none"; $("printReport").style.display=""; setCompanyInfo("r_company_info"); $("r_day").textContent=day; $("r_status").textContent=isClosed(day) ? "Clôturé" : "Ouvert"; $("r_cash").textContent=fmt(s.cashTotal); $("r_card").textContent=fmt(s.card); $("r_cheque").textContent=fmt(s.cheque); $("r_fond").textContent=fmt(s.fond); // Rendu (calculatrice) try{ const r = (state.recon && state.recon[day]) ? state.recon[day] : null; const renduVal = r && r.rendu!=="" ? Math.max(0, round2(parseAmount(r.rendu))) : Math.max(0, round2(parseAmount($("renduAmount")?.value))); if(document.getElementById("r_rendu")) $("r_rendu").textContent = fmt(renduVal); }catch(e){} // Rendu (calculatrice) try{ const r = (state.recon && state.recon[day]) ? state.recon[day] : null; const renduVal = r && r.rendu!=="" ? Math.max(0, round2(parseAmount(r.rendu))) : Math.max(0, round2(parseAmount($("renduAmount")?.value))); if(document.getElementById("r_rendu")) $("r_rendu").textContent = fmt(renduVal); }catch(e){} $("r_bea").textContent=fmt(s.depBEA); $("r_cpa").textContent=fmt(s.depCPA); $("r_dec_cash").textContent = fmt(s.decCash||0); $("r_dec_all").textContent = fmt(s.decAll||0); const soldeCash = (s.cashTotal||0) - (s.decCash||0) - (s.depBEA||0) - (s.depCPA||0); $("r_solde_cash").textContent = fmt(soldeCash); // Recon in report (if fields exist) try{ const r = (state.recon && state.recon[day]) ? state.recon[day] : null; if(document.getElementById("r_counted")) $("r_counted").textContent = r && r.drawerCount!=="" ? fmt(Number(r.drawerCount)||0) : "—"; if(document.getElementById("r_diff")) $("r_diff").textContent = r && r.drawerCount!=="" ? fmtSigned((Number(r.drawerCount)||0) - (Number(soldeCash)||0)) : "—"; if(document.getElementById("r_note")) $("r_note").textContent = (r && r.note) ? r.note : "—"; }catch(e){} $("r_txs").innerHTML = list.slice().reverse().map((t,i)=>` ${i+1} ${t.time} ${t.clientId} ${escapeHtml(t.clientName)} ${t.produit} ${escapeHtml(t.receipt||"")} ${t.mode} ${fmt(t.amount)} `).join("") || `—`; // Décaissements const decs = decOfDay(day).slice().sort((a,b)=> String(a.time||"").localeCompare(String(b.time||""))); if(document.getElementById("r_decs")){ const totalDecAll = decs.reduce((s,x)=> s + (Number(x.amount)||0), 0); const elTotDec = document.getElementById("r_decs_total"); if(elTotDec) elTotDec.textContent = fmt(totalDecAll); document.getElementById("r_decs").innerHTML = decs.map((d,i)=>` ${i+1} ${d.time} ${escapeHtml(d.label)} ${d.mode} ${fmt(d.amount)} `).join("") || `—`; } const reportClone = $("printReport").cloneNode(true); stripIds(reportClone); reportClone.style.display=""; reportClone.style.pageBreakAfter="always"; pack.appendChild(reportClone); // ---- Prepare Bordereaux BEA & CPA (only if amount > 0) ---- for(const bank of ["BEA","CPA"]){ activeBank = bank; const amount = bordereauAmount(day, activeBank); if(amount<=0) continue; const no = currentBordNo(day, activeBank); const color = bankColor(activeBank); $("printReport").style.display="none"; $("printBord").style.display=""; try{ const bar=document.getElementById("p_bar"); if(bar) bar.style.background=color; }catch(e){} $("p_bank").textContent=bankName(activeBank); $("p_rib").textContent=bankRib(activeBank); $("p_day").textContent=day; $("p_no").textContent=no; $("p_amount_label").textContent = (activeBank==="BEA") ? "MONTANT A VERSER BEA (EMP):" : "MONTANT A VERSER CPA (LCA):"; $("p_amount").textContent=fmt(amount); $("p_words").textContent=amountToWordsDZD(amount); setCompanyInfo("p_company_info"); const den=ensureAutoDenoms(day, activeBank); const rows=DENOMS.filter(d=>Number(den[d.key]||0)>0).map(d=>{ const qty=Number(den[d.key]||0); const sub=qty*d.value; const label = String(d.value); return `${d.type}${label}${qty}${fmt(sub)}`; }).join(""); const total=denomTotal(day, activeBank); $("p_denoms").innerHTML = rows || `—`; $("p_den_total").textContent=fmt(total); // Encaissements list (Espèces only for this bank) const prod = bankProduct(activeBank); let allTx = txOfDay(day).filter(t => t.mode==="Espèces" && t.produit===prod); allTx = allTx.slice().sort((a,b)=> String(a.time||"").localeCompare(String(b.time||""))); if(document.getElementById("p_txs")){ const tbody = document.getElementById("p_txs"); const grouped = groupCashTxByClient(allTx); tbody.innerHTML = grouped.map((t,i)=>` ${i+1} ${t.time} ${t.clientId} ${escapeHtml(t.clientName)} ${escapeHtml(t.receipt||"")} ${fmt(t.amount)} `).join("") || `—`; const totalTx = grouped.reduce((s,t)=> s + (Number(t.amount)||0), 0); const elTot = document.getElementById("p_txs_total"); const elCnt = document.getElementById("p_txs_count"); if(elTot) elTot.textContent = fmt(totalTx); if(elCnt) elCnt.textContent = String(grouped.length); } setBordereauLogo(); const bordClone = $("printBord").cloneNode(true); stripIds(bordClone); bordClone.style.display=""; bordClone.style.pageBreakAfter="always"; pack.appendChild(bordClone); } // Restore UI + hide originals activeBank = oldActive; $("printBord").style.display="none"; $("printReport").style.display="none"; openPrintWindow("Pack journalier", pack); addAudit("PRINT_PACK", `jour=${day}`); // Cleanup pack (après ouverture de la fenêtre d'impression) setTimeout(()=>{ pack.style.display="none"; pack.innerHTML=""; }, 800); }); $("printReportBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("printOps", "Imprimer le rapport")) return; if(!requirePin("Imprimer le rapport")) return; const list=txOfDay(day); const s=computeDeposits(day); $("printBord").style.display="none"; $("printReport").style.display=""; setCompanyInfo("r_company_info"); $("r_day").textContent=day; $("r_status").textContent=isClosed(day) ? "Clôturé" : "Ouvert"; $("r_cash").textContent=fmt(s.cashTotal); $("r_card").textContent=fmt(s.card); $("r_cheque").textContent=fmt(s.cheque); $("r_fond").textContent=fmt(s.fond); $("r_bea").textContent=fmt(s.depBEA); $("r_cpa").textContent=fmt(s.depCPA); // Décaissements summary $("r_dec_cash").textContent = fmt(s.decCash||0); $("r_dec_all").textContent = fmt(s.decAll||0); const soldeCash = (s.cashTotal||0) - (s.decCash||0) - (s.depBEA||0) - (s.depCPA||0); $("r_solde_cash").textContent = fmt(soldeCash); // Comptage caisse try{ const r = (state.recon && state.recon[day]) ? state.recon[day] : null; if(document.getElementById("r_counted")) $("r_counted").textContent = r && r.drawerCount!=="" ? fmt(Number(r.drawerCount)||0) : "—"; if(document.getElementById("r_diff")) $("r_diff").textContent = r && r.drawerCount!=="" ? fmtSigned((Number(r.drawerCount)||0) - (Number(soldeCash)||0)) : "—"; if(document.getElementById("r_note")) $("r_note").textContent = (r && r.note) ? r.note : "—"; }catch(e){} $("r_txs").innerHTML = list.slice().reverse().map((t,i)=>` ${i+1} ${t.time} ${t.clientId} ${escapeHtml(t.clientName)} ${t.produit} ${escapeHtml(t.receipt||"")} ${t.mode} ${fmt(t.amount)} `).join("") || `—`; // === Add encaissements list (Espèces only for this bank) === let allTx = txOfDay(day).filter(t => t.mode==="Espèces" && t.produit===bankProduct(activeBank) ); allTx = allTx.slice().sort((a,b)=> String(a.time||"").localeCompare(String(b.time||""))); if(document.getElementById("p_txs")){ const grouped = groupCashTxByClient(allTx); document.getElementById("p_txs").innerHTML = grouped.map((t,i)=>` ${i+1} ${t.time} ${t.clientId} ${escapeHtml(t.clientName)} ${escapeHtml(t.receipt||"")} ${fmt(t.amount)} `).join("") || `—`; } // Décaissements const decs = decOfDay(day).slice().sort((a,b)=> String(a.time||"").localeCompare(String(b.time||""))); if(document.getElementById("r_decs")){ const totalDecAll = decs.reduce((s,x)=> s + (Number(x.amount)||0), 0); const elTotDec = document.getElementById("r_decs_total"); if(elTotDec) elTotDec.textContent = fmt(totalDecAll); document.getElementById("r_decs").innerHTML = decs.map((d,i)=>` ${i+1} ${d.time} ${escapeHtml(d.label)} ${d.mode} ${fmt(d.amount)} `).join("") || `—`; } openPrintWindow("Rapport caisse", $("printReport")); addAudit("PRINT_REPORT", `jour=${day}`); }); // === DECAISSEMENT EVENTS === $("decSearch").addEventListener("input", ()=>{ decPage = 1; renderDecDebounced(); }); if(document.getElementById("decPrevPageBtn")){ $("decPrevPageBtn").addEventListener("click", ()=>{ decPage = Math.max(1, decPage - 1); renderDec(); }); } if(document.getElementById("decNextPageBtn")){ $("decNextPageBtn").addEventListener("click", ()=>{ decPage += 1; renderDec(); }); } if(document.getElementById("decPageSize")){ $("decPageSize").addEventListener("change", ()=>{ decPage = 1; renderDec(); }); } $("cancelDecEditBtn").addEventListener("click", ()=>{ editDecId=null; $("saveDecBtn").textContent="Ajouter"; $("cancelDecEditBtn").style.display="none"; $("decLabel").value=""; $("decAmount").value=""; $("decObs").value=""; $("decTime").value=nowTime(); resetFormCashCalc("dec", true); syncFormCashCalcVisibility("dec"); queueFormDraftSave(60); }); $("saveDecBtn").addEventListener("click", ()=>{ if(!requireCapability("manageData", "Enregistrer un décaissement")) return; const day = normalizeDate(state.params.day, nowDate()); if(isClosed(day)) return alert("Ce jour est clôturé. Changez la date ou ouvrez un nouveau jour."); const label = asSafeText($("decLabel").value, 220); const amount = parseAmount($("decAmount").value); if(!label) return alert("Saisissez le libellé / motif."); if(amount<=0) return alert("Saisissez un montant valide."); const mode = VALID_DEC_MODES.has(String($("decMode").value||"")) ? $("decMode").value : "Espèces"; const dec={ day, date:normalizeDate($("decDate").value, day), time:normalizeTime($("decTime").value, nowTime()), label, mode, amount:round2(amount), obs:asSafeText($("decObs").value, 240) }; // Save denominations from cash calculator (Espèces) if(dec.mode==="Espèces"){ const den = cleanDenoms(formCashCalcState.dec); if(Object.keys(den).length) dec.cashDenoms = den; } if(editDecId){ if(!requirePin("Modifier le décaissement")) return; if(!updateDec(editDecId, dec)) return alert("Échec de la modification."); editDecId=null; $("saveDecBtn").textContent="Ajouter"; $("cancelDecEditBtn").style.display="none"; } else { dec.id = makeId("dec"); if(!addDec(dec)) return alert("Échec de l'enregistrement."); } $("decLabel").value=""; $("decAmount").value=""; $("decObs").value=""; $("decTime").value=nowTime(); resetFormCashCalc("dec", true); syncFormCashCalcVisibility("dec"); decPage = 1; renderAll(); queueCriticalFullSave(); }); $("clearDayDecBtn").addEventListener("click", ()=>{ const day=state.params.day; if(!requireCapability("manageData", "Réinitialiser les décaissements du jour")) return; if(!requirePin("Réinitialiser les décaissements du jour")) return; if(confirm("Réinitialiser les décaissements du jour ?")){ clearDayDec(day); renderAll(); } }); window.addEventListener("beforeunload", ()=>{ persistFormDraftNow(); try{ save({skipIndexedDb:true, skipAutoBackup:true}); }catch(e){} createAutoBackup("beforeunload", {force:true}); }); document.addEventListener("visibilitychange", ()=>{ if(document.visibilityState === "hidden"){ persistFormDraftNow(); try{ save({skipIndexedDb:true, skipAutoBackup:true}); }catch(e){} createAutoBackup("visibility-hidden", {force:true}); } }); /* INIT */ (async function init(){ state.params.day = normalizeDate(state.params.day, nowDate()); loadRoleSession(); loadTheme(); loadSkin(); loadDensity(); await hydrateFromIndexedDb(); if(stateDataScore(state) > 0){ save({skipIndexedDb:true, skipAutoBackup:true}); } bindFormDraftTracking(); bindStickyInputTracking(); renderAll(); // Restore AFTER render (avoid being overwritten by rendering) setTimeout(()=>{ try{ restoreFormDraftPayload(); restoreStickyInputs(); }catch(e){} }, 0); queueFormDraftSave(200); bindSessionActivityMonitor(); renderSessionStatus(); initPwa(); const initialHash = window.location.hash || "#settings"; const link = document.querySelector(`#nav a.nav-link[href="${initialHash}"]`) || document.querySelector('#nav a.nav-link[href="#settings"]'); if(link) focusSectionByHash(initialHash, link); syncNavHighlight(); })();