${logo?`logo`:`
${esc(co.name)}
`}
${esc(co.name)} (${esc(co.legalForm)})
${esc(co.address)}, ${esc(co.city)}, ${esc(co.country)}
ICE : ${esc(co.ice)} · RC : ${esc(co.rc)} · IF : ${esc(co.if)}
${esc(co.phone)} · ${esc(co.email)}${co.website?' · '+esc(co.website):''}
${esc(docTitle)}
N° ${esc(docRef)}  ·  Date : ${fmtDate(docDate)}${extraMeta?'  ·  '+extraMeta:''}
${clientHtml} ${bodyTableHtml} ${totalsHtml||''} ${footNote?`
${footNote}
`:''}
Pour le client
Bon pour accord — signature et cachet
Pour ${esc(co.name)}
Signature et cachet
${esc(co.name)} ${esc(co.legalForm)} — ${esc(co.address)}, ${esc(co.city)} — ICE ${esc(co.ice)} — RC ${esc(co.rc)} — IF ${esc(co.if)} — Patente ${esc(co.patente)}
Banque : ${esc(co.bank)} — RIB : ${esc(co.rib)}
Document généré automatiquement par SUSTNEX Project & Portfolio Management.
`; } function downloadHTMLDoc(filename, htmlString){ const blob = new Blob([htmlString], {type:'text/html;charset=utf-8;'}); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = filename; a.click(); } function previewDocument(filename, fullHtml){ const root = document.getElementById('modalRoot'); root.innerHTML = ``; document.getElementById('docPreviewFrame').srcdoc = fullHtml; } function previewFactureDoc(filename, fullHtml, invId, templateId, showHeader){ const root = document.getElementById('modalRoot'); root.innerHTML = ``; let currentFile = filename; document.getElementById('docPreviewFrame').srcdoc = fullHtml; document.getElementById('factDlBtn').onclick = ()=> downloadHTMLDoc(currentFile, document.getElementById('docPreviewFrame').srcdoc); const rerenderFacturePreview = ()=>{ const tid = document.getElementById('factTplSelect').value; const showHeader = document.getElementById('factHeaderChk').checked; const ctx = prepFactureCtx(invId, {showHeader}); const html = getFactureTemplate(tid).fn(ctx); document.getElementById('docPreviewFrame').srcdoc = html; currentFile = `Facture_${ctx.inv.number}_${tid}${showHeader?'':'_sans-entete'}.html`; }; document.getElementById('factTplSelect').onchange = rerenderFacturePreview; document.getElementById('factHeaderChk').onchange = rerenderFacturePreview; } function generateDevis(oppId){ const o = byId(db.opportunities, oppId); const cl = byId(db.clients, o.clientId); const ref = docNumber('devis'); const date = todayISO(); const validUntil = new Date(Date.now()+CFG.company.devisValidityDays*86400000).toISOString().slice(0,10); const montantHT = toMAD(o.amountEstimated, o.currency); const tva = 20; const montantTVA = montantHT*tva/100; const montantTTC = montantHT+montantTVA; const body = `
DésignationDomaineMontant HT
${esc(o.name)}${o.description?'
'+esc(o.description)+'':''}
${esc(o.domain||'—')}${moneyDoc(montantHT)}
`; const totals = `
Total HT${moneyDoc(montantHT)}
TVA (${tva}%)${moneyDoc(montantTVA)}
Total TTC${moneyDoc(montantTTC)}
`; const note = `Devis valable jusqu'au ${fmtDate(validUntil)}. Conditions de paiement : ${esc(CFG.company.defaultPaymentTerms)}.${o.days?' Durée estimée : '+o.days+' jour(s) consultant.':''}`; const html = documentShell('DEVIS', ref, date, 'Validité : '+fmtDate(validUntil), clientBlock(cl), body, totals, note); previewDocument(`${ref}.html`, html); } function moneyDoc(n, ccy, showCcy){ const formatted = new Intl.NumberFormat('fr-FR',{minimumFractionDigits:2,maximumFractionDigits:2}).format(Number(n)||0); return showCcy===false ? formatted : formatted+' '+(ccy||'MAD'); } function buildPaymentQrSvg(co, montantTTC, inv){ try { if (typeof qrcode !== 'function') return ''; if (qrcode.stringToBytesFuncs && qrcode.stringToBytesFuncs['UTF-8']) { qrcode.stringToBytes = qrcode.stringToBytesFuncs['UTF-8']; } var payload = 'Beneficiaire: ' + (co.name||'') + '\n' + 'Banque: ' + (co.bank||'') + '\n' + 'RIB: ' + (co.rib||'') + '\n' + 'Montant: ' + moneyDoc(montantTTC, inv.currency) + '\n' + 'Reference: Facture ' + (inv.number||''); var qr = qrcode(0, 'M'); qr.addData(payload); qr.make(); return qr.createSvgTag({cellSize:3, margin:4}); } catch(e) { return ''; } } const FACTURE_FONT = `'Century Gothic','Poppins','Avenir Next','Futura','Trebuchet MS',Arial,sans-serif`; const SUSTNEX_BLUE = '#1a56c7'; const SUSTNEX_BLUE_DARK = '#123f94'; const SUSTNEX_BLUE_TINT = '#eef4fd'; const FACTURE_COBLOCK_BLUE = '#46619c'; // bleu sourd utilisé sur le modèle Classique (nom + adresse société) function legalFooterLine1(co){ return `${esc(co.legalName||co.name)} ${esc(co.legalForm)}${co.capital?' au Capital Social de '+esc(co.capital):''}${co.rc?' | R.C : '+esc(co.rc):''}${co.if?' | I.F : '+esc(co.if):''}`; } function legalFooterLine2(co){ return `Taxe professionnelle : ${esc(co.patente)} | ICE : ${esc(co.ice)}${co.cnss?(' | CNSS : '+esc(co.cnss)):''}`; } function withHeader(ctx, html){ return (ctx && ctx.showHeader===false) ? '' : html; } function prepFactureCtx(invId, opts){ const inv = byId(db.invoices, invId); const p = byId(db.projects, inv.projectId); const cl = byId(db.clients, p?.clientId); const co = CFG.company; const montantHT = inv.amountHT; const tva = inv.tva!=null?inv.tva:20; const montantTVA = montantHT*tva/100; const montantTTC = montantHT+montantTVA; const honoraires = p ? toMAD(p.contractAmount, p.currency||'MAD') : montantHT; const projectInvoicesList = p ? projectInvoices(p.id).slice().sort((a,b)=>new Date(a.date)-new Date(b.date)) : [inv]; const acompteIndex = Math.max(projectInvoicesList.findIndex(i=>i.id===inv.id)+1, 1); const pctFacture = honoraires ? Math.round(montantHT/honoraires*1000)/10 : 100; const label = projectInvoicesList.length<=1 ? 'Solde de mission' : `Acompte n°${acompteIndex}`; const objet = p ? `${p.name}${cl?' de '+cl.name:''}` : (inv.number||''); const coNameParts = co.name.split(' '); const showHeader = (opts && typeof opts.showHeader === 'boolean') ? opts.showHeader : !!(window.MT_MEMBERSHIP && window.MT_MEMBERSHIP.orgId === '00000000-0000-0000-0000-000000000001'); const lines = (db.invoiceLines||[]).filter(l=>l.invoiceId===inv.id).slice().sort((a,b)=>(a.position||0)-(b.position||0)); return { inv, p, cl, co, logo: logoDataUri(), logoWhite: DOC_LOGO_WHITE_URI, montantHT, tva, montantTVA, montantTTC, honoraires, pctFacture, label, showHeader, desc: `${label}
${pctFacture}% du montant
des honoraires`, objet, coNameBold: coNameParts[0], coNameRest: coNameParts.slice(1).join(' '), lines, projectNameDisplay: inv.projectName || '', refAchat: inv.refAchat || '' }; } // Lignes du tableau de prix des factures SUSTNex (sustnex_classic / sustnex_classic_v3) : si l'utilisateur // a saisi des lignes explicites (voir openInvoiceLinesEditor), on les affiche une par une ; sinon on // conserve EXACTEMENT le comportement historique (une seule ligne calculée à partir du montant HT global). function factureLignesRows(ctx){ const {inv, p, montantHT} = ctx; if(ctx.lines && ctx.lines.length){ return ctx.lines.map(l=>`${esc(l.description)}${moneyDoc(l.amount, inv.currency, false)}1${moneyDoc(l.amount, inv.currency)}`).join(''); } const ligneDesc = p ? esc(p.name) : esc(inv.number||''); return `${ligneDesc}${moneyDoc(montantHT, inv.currency, false)}1${moneyDoc(montantHT, inv.currency)}`; } /* ---------- TEMPLATE 1 — Classique (identique au modèle réel SUSTNex) ---------- */ function tplClassique(ctx){ const {inv,p,cl,co,logo,montantHT,tva,montantTVA,montantTTC,honoraires,pctFacture,desc,objet,coNameBold,coNameRest} = ctx; const ligneDesc = p ? esc(p.name) : esc(inv.number||''); const title = p ? esc(p.name) : esc(inv.number||''); const FACTURE_TITLE_SRC = 'data:image/png;base64,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'; return `Facture ${esc(inv.number)}
${withHeader(ctx, logo?``:`
`)} ${withHeader(ctx, `
${esc(coNameBold)}${coNameRest?' '+esc(coNameRest):''}
${esc(co.address)} - ${esc(co.city)}, ${esc(co.country)}  |  ${esc(co.phone)}  |  ${esc(co.email)}
`)}
Facture N°
${esc(inv.number)}
${ctx.projectNameDisplay?`
Projet :
${esc(ctx.projectNameDisplay)}
`:''}${ctx.refAchat?`
Réf. achat :
${esc(ctx.refAchat)}
`:''}
FACTURE
Facturé à :
${cl?esc(cl.name):""}
${cl?`${esc(cl.address||'')}${cl.address?'
':''}${esc(cl.city||'')}
${cl.ice?'ICE : '+esc(cl.ice):''}`:'Client non renseigné'}
Réglement
Par virement bancaire
Banque : ${esc(co.bank)}
RIB : ${esc(co.rib)}
${buildPaymentQrSvg(co, montantTTC, inv)}
Date :
${fmtDateDoc(inv.date)}
${factureLignesRows(ctx)}
DescriptionPrix unitaireQuantitéTotal
Total HT${moneyDoc(montantHT, inv.currency)}
TVA ${tva}%${moneyDoc(montantTVA, inv.currency)}
Total TTC${moneyDoc(montantTTC, inv.currency)}
www.sustnex.com
`; } function tplClassiqueV3(ctx){ const V3_ICON_WHITE_SRC = "data:image/png;base64,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"; const {inv,p,cl,co,logo,montantHT,tva,montantTVA,montantTTC,honoraires,pctFacture,desc,objet,coNameBold,coNameRest} = ctx; const ligneDesc = p ? esc(p.name) : esc(inv.number||''); const title = p ? esc(p.name) : esc(inv.number||''); const FACTURE_TITLE_SRC = 'data:image/png;base64,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'; return `Facture ${esc(inv.number)}
${withHeader(ctx, (logo?``:`
`) + `
${esc(co.legalName)} ${esc(co.legalForm)}
Siège social
${esc(co.address)}
${esc(co.city)}, ${esc(co.country)}
Téléphone : ${esc(co.phone)}
Email : ${esc(co.email)}
www.sustnex.com
`)}
Facture N°
${esc(inv.number)}
${ctx.projectNameDisplay?`
Projet :
${esc(ctx.projectNameDisplay)}
`:''}${ctx.refAchat?`
Réf. achat :
${esc(ctx.refAchat)}
`:''}
FACTURE
Facturé à :
${cl?esc(cl.name):""}
${cl?`${esc(cl.address||'')}${cl.address?'
':''}${esc(cl.city||'')}
${cl.ice?'ICE : '+esc(cl.ice):''}`:'Client non renseigné'}
Réglement
Par virement bancaire
Banque : ${esc(co.bank)}
RIB : ${esc(co.rib)}
${buildPaymentQrSvg(co, montantTTC, inv)}
Date :
${fmtDateDoc(inv.date)}
${factureLignesRows(ctx)}
DescriptionPrix unitaireQuantitéTotal
Total HT${moneyDoc(montantHT, inv.currency)}
TVA ${tva}%${moneyDoc(montantTVA, inv.currency)}
Total TTC${moneyDoc(montantTTC, inv.currency)}
${withHeader(ctx, `
SUSTNEX
${esc(co.legalName)} ${esc(co.legalForm)}, société à responsabilité limitée d'associé unique de droit marocain — Conseil et études en stratégie, management, organisation, finance et systèmes d'information ; formation et ressources humaines.
${co.cnss?``:''}
Capital${esc(co.capital)}
RC${esc(co.rc)}
ICE${esc(co.ice)}
IF${esc(co.if)}
CNSS${esc(co.cnss)}
Taxe professionnelle${esc(co.patente)}
`)}
`; } // ---------- Conversion d'un montant en lettres (français, MAD) — utilisé par le modèle GID ---------- function nombreEnLettresFr(nombre){ nombre = Math.floor(Math.abs(Number(nombre)||0)); if(nombre === 0) return 'zéro'; const UNITES = ['zéro','un','deux','trois','quatre','cinq','six','sept','huit','neuf','dix','onze','douze','treize','quatorze','quinze','seize','dix-sept','dix-huit','dix-neuf']; const DIZAINES = ['','','vingt','trente','quarante','cinquante','soixante','soixante-dix','quatre-vingt','quatre-vingt-dix']; function dizaineUnite(n){ if(n < 20) return UNITES[n]; const d = Math.floor(n/10), u = n % 10; if(d === 7 || d === 9){ if(u === 0) return DIZAINES[d]; if(d === 7) return u === 1 ? 'soixante-et-onze' : ('soixante-' + UNITES[10+u]); return 'quatre-vingt-' + UNITES[10+u]; } if(d === 8) return u === 0 ? 'quatre-vingts' : ('quatre-vingt-' + UNITES[u]); const base = DIZAINES[d]; if(u === 0) return base; if(u === 1) return base + '-et-un'; return base + '-' + UNITES[u]; } function centaine(n){ if(n === 0) return ''; let res = ''; const c = Math.floor(n/100), reste = n % 100; if(c > 0){ res += (c === 1) ? 'cent' : (UNITES[c] + ' cent'); if(reste === 0 && c > 1) res += 's'; if(reste > 0) res += ' '; } if(reste > 0) res += dizaineUnite(reste); return res; } function sansPlurielFinal(s){ if(s.endsWith('vingts')) return s.slice(0,-1); if(s.endsWith('cents')) return s.slice(0,-1); return s; } const groupes = []; let reste = nombre; while(reste > 0){ groupes.unshift(reste % 1000); reste = Math.floor(reste/1000); } const nbGroupes = groupes.length; const mots = []; for(let i = 0; i < nbGroupes; i++){ const val = groupes[i]; if(val === 0) continue; const position = nbGroupes - 1 - i; // 0=unités, 1=mille, 2=million, 3=milliard let groupeMots = centaine(val); if(position >= 1) groupeMots = sansPlurielFinal(groupeMots); if(position === 1){ mots.push(val === 1 ? 'mille' : (groupeMots + ' mille')); } else if(position === 2){ mots.push(groupeMots + (val === 1 ? ' million' : ' millions')); } else if(position === 3){ mots.push(groupeMots + (val === 1 ? ' milliard' : ' milliards')); } else { mots.push(groupeMots); } } return mots.join(' ').replace(/\s+/g,' ').trim(); } function montantEnLettresMAD(montant){ const total = Math.round((Number(montant)||0) * 100); const dh = Math.floor(total/100), ct = total % 100; let mots = nombreEnLettresFr(dh); mots = mots.charAt(0).toUpperCase() + mots.slice(1); let phrase = mots + ' dirham' + (dh !== 1 ? 's' : ''); if(ct > 0){ phrase += ' et ' + nombreEnLettresFr(ct) + ' centime' + (ct !== 1 ? 's' : ''); } return phrase; } // Lignes du tableau de prix du modèle GID (6 colonnes : N°/Désignation/Unité/Quantité/Prix unitaire HT/Montant HT). function gidLignesRows(ctx){ const {inv, p, montantHT} = ctx; if(ctx.lines && ctx.lines.length){ return ctx.lines.map((l,i)=>`${i+1}${esc(l.description)}${esc(l.unit||'Forfait')}1${moneyDoc(l.amount, inv.currency, false)}${moneyDoc(l.amount, inv.currency)}`).join(''); } const ligneDesc = p ? esc(p.name) : esc(inv.number||''); return `1${ligneDesc}Forfait1${moneyDoc(montantHT, inv.currency, false)}${moneyDoc(montantHT, inv.currency)}`; } /* ---------- TEMPLATE GID — Organismes publics (conforme GID) ---------- */ function tplGidPublic(ctx){ const {inv,cl,co,logo,montantHT,tva,montantTVA,montantTTC} = ctx; const V3_ICON_WHITE_SRC = "data:image/png;base64,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"; return `Facture ${esc(inv.number)}
${withHeader(ctx, (logo?``:`
`) + `
${esc(co.legalName)} ${esc(co.legalForm)}
Siège social
${esc(co.address)}
${esc(co.city)}, ${esc(co.country)}
Téléphone : ${esc(co.phone)}
Email : ${esc(co.email)}
www.sustnex.com
`)}
${cl?esc(cl.name):'Client non renseigné'}
FACTURE
Facture n° :
${esc(inv.number)}
Date :
${fmtDateDoc(inv.date)}
${ctx.refAchat?`
Référence :
${esc(ctx.refAchat)}
`:''} ${ctx.projectNameDisplay?`
Objet :
${esc(ctx.projectNameDisplay)}
`:''}
${gidLignesRows(ctx)}
DésignationUnitéQuantitéPrix unitaire HTMontant HT
Total Hors taxes${moneyDoc(montantHT, inv.currency)}
TVA ${tva}%${moneyDoc(montantTVA, inv.currency)}
Total Toutes Taxes Comprises${moneyDoc(montantTTC, inv.currency)}
Arrêté la présente facture à la somme de ${montantEnLettresMAD(montantTTC)}, toutes taxes comprises.
Règlement :
Par virement bancaire
Banque : ${esc(co.bank)}
RIB : ${esc(co.rib)}
${withHeader(ctx, `
SUSTNEX
${esc(co.legalName)} ${esc(co.legalForm)}, société à responsabilité limitée d'associé unique de droit marocain — Conseil et études en stratégie, management, organisation, finance et systèmes d'information ; formation et ressources humaines.
${co.cnss?``:''}
Capital${esc(co.capital)}
RC${esc(co.rc)}
ICE${esc(co.ice)}
IF${esc(co.if)}
CNSS${esc(co.cnss)}
Taxe professionnelle${esc(co.patente)}
`)}
`; } /* ---------- TEMPLATE 2 — Signature Bleu (texte/bordures bleues, sans fond couleur) ---------- */ function tplTeal(ctx){ const {inv,p,cl,co,logo,montantHT,tva,montantTVA,montantTTC,honoraires,pctFacture,desc,objet} = ctx; return `Facture ${esc(inv.number)}
${withHeader(ctx, logo?`logo`:`
${esc(co.name)}
`)}
FACTURE
Facture N°
${esc(inv.number)}
${withHeader(ctx, `
${esc(co.name)}
${esc(co.address)} - ${esc(co.city)}, ${esc(co.country)}
${esc(co.phone)} · ${esc(co.email)}
`)}
Date
${fmtDateDoc(inv.date)}
Objet
${esc(objet)}
Facturé à
${cl?`${esc(cl.name)}
${esc(cl.address||'')}${cl.address?'
':''}${esc(cl.city||'')}
${cl.ice?'ICE : '+esc(cl.ice):''}`:'—'}
Réglement
Par virement bancaire
Banque : ${esc(co.bank)}
RIB : ${esc(co.rib)}
DescriptionHonoraires% facturéTotal
${desc}${moneyDoc(honoraires, inv.currency, false)}${pctFacture}%${moneyDoc(montantHT, inv.currency)}
Total HT${moneyDoc(montantHT, inv.currency)}
TVA ${tva}%${moneyDoc(montantTVA, inv.currency)}
Total TTC${moneyDoc(montantTTC, inv.currency)}
${legalFooterLine1(co)}
${legalFooterLine2(co)}
`; } /* ---------- TEMPLATE 3 — Executive Sidebar (sidebar pleine hauteur, bleu SUSTNex) ---------- */ function tplNavy(ctx){ const {inv,p,cl,co,logo,montantHT,tva,montantTVA,montantTTC,honoraires,pctFacture,desc,objet} = ctx; return `Facture ${esc(inv.number)}
${withHeader(ctx, `${logo?`logo`:`
${esc(co.name)}
`}
Émetteur
${esc(co.name)}
${esc(co.address)}
${esc(co.city)}, ${esc(co.country)}
Contact
${esc(co.phone)}
${esc(co.email)}
`)}
Facturé à
${cl?`${esc(cl.name)}
${esc(cl.city||'')}${cl.ice?'
ICE : '+esc(cl.ice):''}`:'—'}
FACTURE
${esc(co.name)}
Facture N°
${esc(inv.number)}
Date
${fmtDateDoc(inv.date)}
Objet
${esc(objet)}
DescriptionHonoraires% facturéTotal
${desc}${moneyDoc(honoraires, inv.currency, false)}${pctFacture}%${moneyDoc(montantHT, inv.currency)}
Total HT${moneyDoc(montantHT, inv.currency)}
TVA ${tva}%${moneyDoc(montantTVA, inv.currency)}
Total TTC${moneyDoc(montantTTC, inv.currency)}
Réglement — Par virement bancaire · Banque : ${esc(co.bank)} · RIB : ${esc(co.rib)}
${legalFooterLine1(co)}
${legalFooterLine2(co)}
`; } /* ---------- TEMPLATE 4 — Minimal Editorial (ultra sobre, style "one-pager" conseil) ---------- */ function tplMinimal(ctx){ const {inv,p,cl,co,logo,montantHT,tva,montantTVA,montantTTC,honoraires,pctFacture,desc,objet} = ctx; return `Facture ${esc(inv.number)}
${withHeader(ctx, `${logo?`logo`:`
${esc(co.name)}
`}
${esc(co.address)} - ${esc(co.city)}
${esc(co.phone)} · ${esc(co.email)}
`)}
FACTURE
Référence
${esc(inv.number)}
Date
${fmtDateDoc(inv.date)}
Objet
${esc(objet)}
DescriptionHonoraires% facturéTotal
${desc}${moneyDoc(honoraires, inv.currency, false)}${pctFacture}%${moneyDoc(montantHT, inv.currency)}
Total HT${moneyDoc(montantHT, inv.currency)}
TVA ${tva}%${moneyDoc(montantTVA, inv.currency)}
Total TTC${moneyDoc(montantTTC, inv.currency)}
Facturé à
${cl?`${esc(cl.name)}
${esc(cl.address||'')}${cl.address?'
':''}${esc(cl.city||'')}${cl.ice?'
ICE : '+esc(cl.ice):''}`:'—'}
Réglement
Par virement bancaire
Banque : ${esc(co.bank)}
RIB : ${esc(co.rib)}
${legalFooterLine1(co)} — ${legalFooterLine2(co)}
`; } /* ---------- TEMPLATE 5 — Corporate Ledger Bleu (encadré, style institutionnel SUSTNex) ---------- */ function tplLedger(ctx){ const {inv,p,cl,co,logo,montantHT,tva,montantTVA,montantTTC,honoraires,pctFacture,desc,objet} = ctx; return `Facture ${esc(inv.number)}
${withHeader(ctx, `${logo?`logo`:`
${esc(co.name)}
`}
${esc(co.name)}
${esc(co.address)} - ${esc(co.city)}, ${esc(co.country)}
${esc(co.phone)} · ${esc(co.email)}
`)}
FACTUREN° ${esc(inv.number)}  ·  ${fmtDateDoc(inv.date)}
Objet
${esc(objet)}
Facturé à
${cl?`${esc(cl.name)}
${esc(cl.address||'')}${cl.address?'
':''}${esc(cl.city||'')}
${cl.ice?'ICE : '+esc(cl.ice):''}`:'—'}
Réglement
Par virement bancaire
Banque : ${esc(co.bank)}
RIB : ${esc(co.rib)}
DescriptionHonoraires% facturéTotal
${desc}${moneyDoc(honoraires, inv.currency, false)}${pctFacture}%${moneyDoc(montantHT, inv.currency)}
Total HT${moneyDoc(montantHT, inv.currency)}
TVA ${tva}%${moneyDoc(montantTVA, inv.currency)}
Total TTC${moneyDoc(montantTTC, inv.currency)}
${legalFooterLine1(co)}
${legalFooterLine2(co)}
`; } var FACTURE_TEMPLATES = [ {id:'classique', label:'Classique', desc:'Identique au modèle SUSTNex existant', fn:tplClassique}, {id:'classique_v3', label:'Modèle 2', desc:"Variante de Classique v2 avec un cartouche de marque SUSTNex (symbole infini blanc sur fond sombre) intégré au pied de page", fn:tplClassiqueV3}, {id:'gid_public', label:'GID (Organismes publics)', desc:"Conforme au modèle GID pour la facturation des organismes publics : en-tête officiel, tableau détaillé (Désignation/Unité/Quantité/Prix unitaire/Montant), montant en lettres, sans QR de paiement", fn:tplGidPublic}, {id:'teal', label:'Signature Bleu', desc:'Bandeau bleu SUSTNex, style premium', fn:tplTeal}, {id:'navy', label:'Executive Sidebar', desc:'Sidebar bleu SUSTNex pleine hauteur, style cabinet international', fn:tplNavy}, {id:'minimal', label:'Minimal Editorial', desc:'Ultra sobre, accent bleu SUSTNex, style one-pager', fn:tplMinimal}, {id:'ledger', label:'Corporate Ledger', desc:'Encadré structuré bleu SUSTNex, style institutionnel', fn:tplLedger}, ]; function getFactureTemplate(id){ return FACTURE_TEMPLATES.find(t=>t.id===id) || FACTURE_TEMPLATES[0]; } function generateFacture(invId, templateId, opts){ const ctx = prepFactureCtx(invId, opts); const tid = templateId || CFG.company.invoiceTemplate || 'classique'; const html = getFactureTemplate(tid).fn(ctx); previewFactureDoc(`Facture_${ctx.inv.number}${ctx.showHeader?'':'_sans-entete'}.html`, html, invId, tid, ctx.showHeader); } function generateBL(projectId){ const p = byId(db.projects, projectId); const cl = byId(db.clients, p.clientId); const ref = docNumber('bl'); const date = todayISO(); const delivered = projectTasks(projectId).filter(t=>t.status==='Terminé'); const body = delivered.length ? `${delivered.map(t=>``).join('')}
DésignationTypeResponsableDate de livraison
${esc(t.name)}${esc(t.type)}${esc(t.owner||'—')}${fmtDate(t.dateEnd)}
` : `
Aucun livrable marqué "Terminé" pour ce projet à ce jour.
`; const note = `Bon de livraison établi dans le cadre du projet ${esc(p.code)} — ${esc(p.name)}. Ce document atteste de la remise des livrables listés ci-dessus au client, sous réserve de validation formelle.`; const html = documentShell('BON DE LIVRAISON', ref, date, 'Projet : '+esc(p.code), clientBlock(cl), body, '', note); previewDocument(`${ref}.html`, html); } /* ========================================================================= MODULE: REPORTING ========================================================================= */ function renderReporting(c){ c.innerHTML = `
Génère un export CSV pour chaque rapport ci-dessous (ouvrable dans Excel).
${[ ['Portfolio projets','projects'],['Pipeline commercial','opportunities'],['Facturation','invoices'], ['Encaissements','payments'],['Charges directes','directCosts'],['Rentabilité par projet','profitability'], ['Ressources & utilisation','resources'],['Risques','risks'],['Clients (CA & marge)','clients'], ].map(([label,key])=>`

${label}

`).join('')}
`; } function csvDownload(filename, rows){ const csv = rows.map(r=>r.map(v=>`"${String(v===undefined||v===null?'':v).replace(/"/g,'""')}"`).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 = filename; a.click(); } function exportReport(key){ if(key==='profitability'){ const rows = [['Projet','Client','CA contractuel','CA facturé','Charges directes','Charges indirectes','Marge brute','Marge nette','Taux marge nette']]; visibleProjects().forEach(p=>{const f=projectFinance(p);rows.push([p.code+' - '+p.name, clientName(p.clientId), f.caContractuel.toFixed(0), f.caFacture.toFixed(0), f.chargesDirectes.toFixed(0), f.chargesIndirectes.toFixed(0), f.margeBrute.toFixed(0), f.margeNette.toFixed(0), pct(f.tauxMargeNette)]);}); csvDownload('rentabilite.csv', rows); return; } if(key==='clients'){ const rows=[['Client','Type','Secteur','CA facturé','Marge nette','Nb projets']]; const vp = visibleProjects(); db.clients.forEach(cl=>{const projs=vp.filter(p=>p.clientId===cl.id);if(myScopeActive() && !projs.length) return;const ca=projs.reduce((s,p)=>s+projectFinance(p).caFacture,0);const mg=projs.reduce((s,p)=>s+projectFinance(p).margeNette,0);rows.push([cl.name,cl.type,cl.sector,ca.toFixed(0),mg.toFixed(0),projs.length]);}); csvDownload('clients.csv',rows); return; } let items = db[key]; if(!items || !items.length){ alert('Aucune donnée à exporter.'); return; } items = items.filter(it=>crudScopeFilter(key, it)); if(!items.length){ alert('Aucune donnée à exporter.'); return; } const cols = Object.keys(items[0]).filter(k=>k!=='checklist' && k!=='file'); const rows = [cols, ...items.map(it=>cols.map(c=>it[c]))]; csvDownload(key+'.csv', rows); } function exportAllCSV(){ renderReporting(document.getElementById('content')); go('reporting'); } /* ========================================================================= MODULE: ADMIN ========================================================================= */ function renderAdmin(c){ const co = CFG.company; c.innerHTML = `

Environnement — stockage totalement isolé entre Démo et Production (aucune donnée partagée)

Environnement actif : ${ENV==='prod'?'🔴 PRODUCTION':'🟢 DÉMO'}
Démo : jeu de données de démonstration réinitialisable à volonté, pour explorer ou faire une présentation. Production : base vide au départ, destinée à vos vraies données clients/projets/factures. Basculer d'un environnement à l'autre ne touche jamais aux données de l'autre.
${canManageUsers() ? `
Une fois activé, l'écran de connexion ne propose plus que Production — utile en exploitation réelle pour éviter qu'une personne se connecte par erreur sur les données de démonstration. Ce sélecteur ci-dessus (dans Admin) reste toujours disponible pour vous permettre d'y revenir à tout moment.
` : ''}
${canManageUsers() ? `

Sauvegardes — points de restauration de l'environnement ${ENV==='prod'?'Production':'Démo'}, stockés sur cet appareil

${listBackups().length}/${BACKUPS_MAX} sauvegarde(s)
${listBackups().length ? `
${listBackups().map(b=>``).join('')}
NomDateParTaille
${esc(b.label)}${fmtDate(b.date)}${esc(b.author)}${backupSizeKB(b)} Ko
` : '
Aucune sauvegarde pour cet environnement.
'}
Une sauvegarde est aussi créée automatiquement avant chaque purge/réinitialisation, et avant chaque restauration (par sécurité). Stockage 100% local à ce navigateur — pensez à télécharger les sauvegardes importantes si vous changez d'ordinateur.

Synchronisation cloud (Supabase) — optionnelle, données partagées quasi en temps réel entre appareils

Statut : ${cloudEnabled() ? '☁ Activée'+(CLOUD_SESSION?(' — connecté : '+esc(CLOUD_SESSION.user.email)):'')+'' : 'Désactivée (mode local)'}
Une fois activée, la connexion se fait par email + mot de passe (compte cloud) au lieu de l'identifiant local, et les données de cet environnement (${ENV==='prod'?'Production':'Démo'}) sont automatiquement synchronisées entre tous les appareils connectés (rafraîchissement toutes les ~4 secondes). Le stockage local reste conservé comme copie de secours hors-ligne. Se désactive à tout moment pour revenir au mode 100% local.

Authentification multi-tenant (Module 1 — migration) — identité résolue via organisations/memberships réels (Supabase), en cours de migration module par module

Statut : ${mtEnabled() ? '🏢 Activée'+(MT_SESSION?(' — connecté : '+esc(MT_SESSION.user.email)+(MT_MEMBERSHIP?(' · '+esc(MT_MEMBERSHIP.role)+' @ '+esc(MT_MEMBERSHIP.orgSlug)):'')):'')+'' : 'Désactivée'}
Une fois activée, l'identité (compte + rôle + organisation) est résolue via Supabase Auth + les tables organisations/memberships réelles au lieu du blob local. Seule l'identité est concernée par ce module : les données métier (clients, projets, tâches…) restent sur ce navigateur jusqu'à leur propre migration. Incompatible avec la synchro cloud legacy ci-dessus (désactivez-la d'abord). Se désactive à tout moment pour revenir au mode précédent (rollback immédiat).

Clients & Contacts (Module 2 — migration) — données servies par Supabase (multi-tenant réel) au lieu du navigateur

Statut : ${mtIsDataBacked('clients') ? '☁ Activée — org. '+esc(CURRENT_USER&&CURRENT_USER.orgSlug||'')+'' : 'Désactivée (données locales)'}
Une fois activé : les clients/contacts sont lus et écrits sur Supabase (isolés par organisation, lecture pour tous les membres actifs, écriture réservée à admin/manager), et rechargés automatiquement à chaque connexion. Les autres modules (opportunités, projets, tâches, documents…) continuent de fonctionner sans changement : ils lisent la liste des clients en mémoire comme avant. Nécessite le mode multi-tenant (identité) activé ci-dessus. Une sauvegarde est créée automatiquement avant l'activation (point de rollback).

Projets (Module 3 — migration) — données servies par Supabase (multi-tenant réel) au lieu du navigateur

Statut : ${mtIsDataBacked('projects') ? '☁ Activée — org. '+esc(CURRENT_USER&&CURRENT_USER.orgSlug||'')+'' : 'Désactivée (données locales)'}
Une fois activé : les projets sont lus et écrits sur Supabase (isolés par organisation, lecture pour tous les membres actifs, écriture réservée à admin/manager), et rechargés automatiquement à chaque connexion — y compris la suppression groupée. Le champ Client référence les clients déjà migrés (Module 2) par leur identifiant, qui reste inchangé quel que soit le module actif. Les tâches/Gantt/ressources/timesheets/factures (pas encore migrés) continuent de lire la liste des projets en mémoire comme avant. Nécessite le mode multi-tenant (identité) activé ci-dessus. Une sauvegarde est créée automatiquement avant l'activation (point de rollback). Note : le périmètre "Mes projets" (Chef de projet/Consultant) reste basé sur les rôles métier historiques et ne s'applique pas encore aux rôles multi-tenant (admin/manager/collaborator) — sujet ouvert, non traité par ce module.

Tâches / Gantt / Mes tâches (Module 4 — migration) — données servies par Supabase (multi-tenant réel) au lieu du navigateur

Statut : ${mtIsDataBacked('tasks') ? '☁ Activée — org. '+esc(CURRENT_USER&&CURRENT_USER.orgSlug||'')+'' : 'Désactivée (données locales)'}
Une fois activé : les tâches sont lues et écrites sur Supabase (isolées par organisation, lecture pour tous les membres actifs, écriture réservée à admin/manager), y compris la suppression groupée (moteur générique) et la génération par modèle de tâches (hook dédié). Le champ Projet référence les projets déjà migrés (Module 3). Gantt, Mes tâches et le dashboard continuent de fonctionner sans changement : ils lisent la liste des tâches en mémoire comme avant. Nécessite le mode multi-tenant (identité) activé ci-dessus. Une sauvegarde est créée automatiquement avant l'activation (point de rollback). Note : comme "Mes projets", "Mes tâches" filtre par nom (t.owner === nom affiché de l'utilisateur) — ce filtre fonctionne nativement pour les comptes multi-tenant puisque leur nom affiché est déjà renseigné, sans traitement particulier de ce module.

Ressources & Affectations (Module 5 — migration) — données servies par Supabase (multi-tenant réel) au lieu du navigateur

Statut : ${mtIsDataBacked('resources') ? '☁ Activée — org. '+esc(CURRENT_USER&&CURRENT_USER.orgSlug||'')+'' : 'Désactivée (données locales)'}
Un seul bouton contrôle les deux tables (comme Clients & Contacts) : consultants et affectations sont lus/écrits sur Supabase ensemble (isolés par organisation, lecture pour tous les membres actifs, écriture réservée à admin/manager). Couvre le moteur CRUD générique (onglet Consultants, onglet Affectations, import CSV consultants, suppression groupée) ainsi que les 3 chemins spécifiques hors CRUD générique : affecter/corriger/retirer une affectation directement depuis le Plan de charge d'une fiche projet. Le champ Projet référence les projets migrés (Module 3), le champ Consultant les ressources de ce module. Charge hebdomadaire et budget timesheet continuent de fonctionner sans changement (lecture en mémoire). Nécessite le mode multi-tenant (identité) activé ci-dessus. Une sauvegarde est créée automatiquement avant l'activation (point de rollback). Note : le périmètre "Mes projets" basé sur resourceId du profil (consultant lié à sa propre fiche Ressource) reste un rôle métier historique, non traité par ce module — même statut ouvert que "Mes projets"/"Mes tâches".

Timesheets (Module 6 — migration) — données servies par Supabase (multi-tenant réel) au lieu du navigateur

Statut : ${mtIsDataBacked('timesheets') ? '☁ Activée — org. '+esc(CURRENT_USER&&CURRENT_USER.orgSlug||'')+'' : 'Désactivée (données locales)'}
Une fois activé : les saisies de temps sont lues et écrites sur Supabase (isolées par organisation, lecture pour tous les membres actifs, écriture réservée à admin/manager) via le seul moteur CRUD générique — aucun chemin de mutation spécifique n'a été trouvé lors de la cartographie de ce module (pas d'ajout rapide de saisie depuis la fiche projet, contrairement aux affectations du Module 5). Les champs Projet et Consultant référencent les entités déjà migrées (Modules 3 et 5). Budget vs consommé (Plan de charge, fiche ressource) et Analyse Timesheet (portefeuille et par projet) continuent de fonctionner sans changement : lecture seule en mémoire, aucun hook nécessaire. Nécessite le mode multi-tenant (identité) activé ci-dessus. Une sauvegarde est créée automatiquement avant l'activation (point de rollback).

Risques & Documents (Module 7 — migration) — données servies par Supabase (multi-tenant réel) au lieu du navigateur

Statut : ${mtIsDataBacked('risks') ? '☁ Activée — org. '+esc(CURRENT_USER&&CURRENT_USER.orgSlug||'')+'' : 'Désactivée (données locales)'}
Un seul bouton contrôle les deux tables indépendantes (comme Clients & Contacts et Ressources & Affectations). Aucun chemin de mutation spécifique n'a été trouvé pour ces deux modules lors de la cartographie : uniquement le moteur CRUD générique (create/edit/delete/suppression groupée/import CSV). Note technique : le champ Fichier des documents (upload local, encodé en base64, 4 Mo max) est stocké tel quel dans une colonne jsonb côté Supabase — comportement identique à l'existant (déjà stocké tel quel dans le blob JSON local), aucune limite de taille de ligne rencontrée en pratique à cette échelle ; un vrai stockage de fichiers (Supabase Storage) serait une amélioration future hors périmètre de cette migration. Nécessite le mode multi-tenant (identité) activé ci-dessus. Une sauvegarde est créée automatiquement avant l'activation (point de rollback).

Finance (Module 8 — migration) — données servies par Supabase (multi-tenant réel) au lieu du navigateur

Statut : ${mtIsDataBacked('opportunities') ? '☁ Activée — org. '+esc(CURRENT_USER&&CURRENT_USER.orgSlug||'')+'' : 'Désactivée (données locales)'}
Un seul bouton contrôle les cinq tables (Pipeline commercial, AO, Charges directes, Factures, Encaissements), comme les modules précédents à tables multiples. Deux chemins de mutation hors CRUD générique trouvés lors de la cartographie : la suppression groupée des opportunités (même pattern que Projets, Module 3) et le cochage de la checklist de soumission d'un AO (mutation directe du champ 'checklist', stocké en colonne jsonb). generateDevis/generateFacture/generateBL restent des fonctions de lecture + aperçu pur, sans écriture. La numérotation automatique des factures (consumeInvoiceNumber) reste un compteur 100% local (CFG), non concerné par cette migration. Nécessite le mode multi-tenant (identité) activé ci-dessus. Une sauvegarde est créée automatiquement avant l'activation (point de rollback).
` : ''}

Référentiels configurables

${CFG.sources.map(s=>`${s}`).join('')}
${CFG.oppStatuses.map(s=>`${s}`).join('')}
${CFG.projectStatuses.map(s=>`${s}`).join('')}
${CFG.domains.map(s=>`${s}`).join('')}

Paramètres financiers

%
${CFG.currencies.filter(c=>c!=='MAD').map(c=>`
1 ${c} = MAD
`).join('')}
Format du n° de facture : AAAA + n° d'exercice (2 chiffres) + séquence (3 chiffres). Prochain numéro : ${esc(peekNextInvoiceNumber())}. Se réinitialise à 001 à chaque nouvel exercice.
Utilisé par le bouton "Facture" dans le module Facturation. Modifiable aussi à la volée depuis l'aperçu d'une facture.

Informations légales de l'entreprise — utilisées comme en-tête sur les devis, factures et bons de livraison générés

${canManageUsers() ? `

Rôles & permissions — personnalisez ce que chaque profil peut voir et modifier

« Visible » affiche ou masque le module dans le menu de ce profil. « Modifiable » autorise la création/modification/suppression (sinon lecture seule). Le module Administration reste toujours visible pour Managing Partner et Admin, pour ne jamais perdre l'accès à cet écran. Ces réglages s'appliquent à tous les comptes ayant ce profil (voir "Utilisateurs & accès" ci-dessous pour attribuer un profil à chaque personne).
` : ''} ${canManageUsers() ? `

Modèles de tâches — proposés par type de mission lors de la génération du planning d'un projet

${(CFG.taskTemplates||[]).length ? `
${CFG.taskTemplates.map(t=>``).join('')}
Type de missionTâches
${esc(t.label)}${t.tasks.length}
` : '
Aucun modèle de tâches.
'}
Chaque modèle correspond à un type de mission (ex : PMO, Audit & Diagnostic, Due diligence...) et propose une liste de tâches par défaut (nom, type, durée). Depuis la fiche projet ou le Planning, le bouton « 📋 Modèle de tâches » permet de choisir un modèle, décocher les tâches non pertinentes et les générer directement dans le planning. Modifier ou supprimer un modèle ici n'affecte jamais les tâches déjà générées dans les projets existants.
` : ''} ${canManageUsers() ? `

Utilisateurs & accès — un identifiant/mot de passe par personne

${db.users.some(u=>u.status==='pending') ? `
⚠ ${db.users.filter(u=>u.status==='pending').length} demande(s) de compte en attente de validation
` : ''}
${db.users.map(u=>``).join('')}
NomIdentifiantRôlePérimètre2FA
${esc(u.name)}${esc(u.username)}${u.status==='pending'?'En attente de validation':`${esc(u.role)}`} ${u.scopeCustom?`🎯 Personnalisé (${(u.scopeProjectIds||[]).length})`:'Auto (rôle)'} ${u.totpEnabled?'🔐 Activée':''} ${u.status==='pending' ? ` ` : `${u.totpEnabled?` `:''}${db.users.length>1?` `:''}` }
Chaque personne se connecte avec son identifiant et son mot de passe ; le rôle choisi ici détermine ce qu'elle peut voir et modifier dans l'app. Les demandes envoyées via "Demander un accès" sur l'écran de connexion apparaissent ici en attente et ne peuvent pas se connecter tant qu'elles ne sont pas validées. Chaque personne peut activer la double authentification (code d'application authenticator) en cliquant sur son nom en haut à droite de l'app → « Mon compte ».
` : ''}

SUSTNex — Confidentialité & propriété

Les données affichées dans cette application sont strictement confidentielles et réservées à l'usage exclusif des personnes autorisées par SUSTNex. Toute reproduction, diffusion ou utilisation en dehors du cadre prévu est interdite sans accord préalable. © SUSTNex — Project & Portfolio Management. Tous droits réservés.
`; if(canManageUsers()) renderRpMatrix(); } // État local (non persisté) de l'éditeur de permissions : mémorise le profil sélectionné entre deux // re-render (ex. après "Enregistrer") pour que l'écran ne saute pas au premier profil de la liste. var RP_STATE = {role:null}; function renderRpMatrix(){ const sel = document.getElementById('rpRoleSelect'); const wrap = document.getElementById('rpMatrixWrap'); if(!sel || !wrap) return; const role = sel.value; RP_STATE.role = role; const locked = roleAlwaysAdmin(role); wrap.innerHTML = `
${PERM_MODULES.map(m=>{ const vis = permVisible(role, m.nav); const hasEdit = !!m.edit; const ed = hasEdit ? permEditable(role, m.edit) : false; const forceAdmin = locked && m.nav==='admin'; return ``; }).join('')}
ModuleVisibleModifiable
${esc(m.label)} ${hasEdit?``:''}
`; } function saveRolePermissions(){ const role = document.getElementById('rpRoleSelect').value; const visible = [...document.querySelectorAll('.rpVis')].filter(cb=>cb.checked).map(cb=>cb.dataset.nav); if(roleAlwaysAdmin(role) && !visible.includes('admin')) visible.push('admin'); // filet de sécurité const editSet = new Set(); document.querySelectorAll('.rpEdit').forEach(cb=>{ if(cb.checked) cb.dataset.edit.split(',').forEach(k=>editSet.add(k)); }); CFG.rolePermissions = CFG.rolePermissions || {}; CFG.rolePermissions[role] = {visible, edit:[...editSet]}; RP_STATE.role = role; persistCfg(); if(CURRENT_USER && CURRENT_ROLE===role && visibleNavIds() && !visibleNavIds().includes(ROUTE)) ROUTE = 'dashboard'; alert('Permissions enregistrées pour le profil « '+role+' ».'); render(); } function resetRolePermissions(){ const role = document.getElementById('rpRoleSelect').value; if(!confirm('Réinitialiser les permissions du profil « '+role+' » aux valeurs par défaut ?')) return; if(CFG.rolePermissions) delete CFG.rolePermissions[role]; RP_STATE.role = role; persistCfg(); if(CURRENT_USER && CURRENT_ROLE===role && visibleNavIds() && !visibleNavIds().includes(ROUTE)) ROUTE = 'dashboard'; render(); } function saveAdminSettings(){ const rate = Number(document.getElementById('admIndirect').value); CFG.indirectRate = rate/100; CFG.currencies.filter(c=>c!=='MAD').forEach(c=>{ CFG.fx[c] = Number(document.getElementById('admFx_'+c).value); }); CFG.company.devisValidityDays = Number(document.getElementById('admDevisValidity').value)||30; CFG.company.defaultPaymentTerms = document.getElementById('admPaymentTerms').value; CFG.company.foundingYear = Number(document.getElementById('admFoundingYear').value)||CFG.company.foundingYear; CFG.company.invoiceSeqYear = new Date().getFullYear(); CFG.company.invoiceSeq = Number(document.getElementById('admInvoiceSeq').value)||1; CFG.company.invoiceTemplate = document.getElementById('admInvoiceTemplate').value; persistCfg(); alert('Paramètres enregistrés.'); render(); } function saveCompanyInfo(){ const co = CFG.company; const map = {coName:'name',coLegalName:'legalName',coLegalForm:'legalForm',coCapital:'capital',coAddress:'address',coCity:'city',coCountry:'country',coIce:'ice',coIf:'if',coRc:'rc',coPatente:'patente',coCnss:'cnss',coPhone:'phone',coEmail:'email',coWebsite:'website',coBank:'bank',coRib:'rib'}; Object.entries(map).forEach(([elId,key])=>{ co[key] = document.getElementById(elId).value; }); persistCfg(); alert('Informations légales enregistrées. Elles apparaîtront sur les prochains documents générés.'); render(); } function persistCfg(){ localStorage.setItem(cfgKey(), JSON.stringify({indirectRate:CFG.indirectRate, fx:CFG.fx, company:CFG.company, docCounters:CFG.docCounters, cloudSync:CFG.cloudSync, multiTenant:CFG.multiTenant, rolePermissions:CFG.rolePermissions, taskTemplates:CFG.taskTemplates})); } /* ========================================================================= SEED DATA — jeu de démonstration réaliste SUSTNex ========================================================================= */ function seedDatabase(force){ if(!force && loadDB()) return; const clients = [ {id:'CLI-01',name:'OCP Group',type:'Client',sector:'Mines & Industrie',segment:'Grand compte',country:'Maroc',city:'Casablanca',ice:'000123456000078',phone:'0522 23 40 40',email:'contact@ocpgroup.ma',owner:'Nadia Berrada',notes:'Compte stratégique, plusieurs BU.'}, {id:'CLI-02',name:'ONCF',type:'Client',sector:'Transport ferroviaire',segment:'Public',country:'Maroc',city:'Rabat',ice:'000234567000045',phone:'0537 71 22 22',email:'dsi@oncf.ma',owner:'Karim El Yacoubi',notes:''}, {id:'CLI-03',name:'Maroc Telecom',type:'Client',sector:'Télécom',segment:'Grand compte',country:'Maroc',city:'Rabat',ice:'000345678000012',phone:'0537 71 20 00',email:'procurement@iam.ma',owner:'Nadia Berrada',notes:''}, {id:'CLI-04',name:'ONEE',type:'Client',sector:'Énergie & Eau',segment:'Public',country:'Maroc',city:'Rabat',ice:'000456789000067',phone:'0537 66 40 40',email:'contact@onee.ma',owner:'Sara Amrani',notes:''}, {id:'CLI-05',name:'Attijariwafa Bank',type:'Client',sector:'Banque & Finance',segment:'Grand compte',country:'Maroc',city:'Casablanca',ice:'000567890000034',phone:'0522 22 41 69',email:'achats@attijariwafa.com',owner:'Karim El Yacoubi',notes:''}, {id:'CLI-06',name:'Lydec',type:'Client',sector:'Utilities',segment:'Grand compte',country:'Maroc',city:'Casablanca',ice:'000678901000023',phone:'0522 43 41 11',email:'contact@lydec.ma',owner:'Sara Amrani',notes:''}, {id:'CLI-07',name:'Marjane Holding',type:'Prospect',sector:'Distribution',segment:'PME',country:'Maroc',city:'Casablanca',ice:'000789012000056',phone:'0522 97 97 97',email:'dg@marjane.ma',owner:'Nadia Berrada',notes:'Consultation privée en cours.'}, {id:'CLI-08',name:'Ministère de la Transition Énergétique',type:'Client',sector:'Secteur public',segment:'Public',country:'Maroc',city:'Rabat',ice:'000890123000011',phone:'0537 68 87 00',email:'marches@energie.gov.ma',owner:'Sara Amrani',notes:''}, {id:'CLI-09',name:'CDG Capital',type:'Prospect',sector:'Investissement',segment:'Grand compte',country:'Maroc',city:'Rabat',ice:'000901234000089',phone:'0537 66 90 00',email:'contact@cdgcapital.ma',owner:'Karim El Yacoubi',notes:'Recommandation via partenaire.'}, {id:'CLI-10',name:'Royal Air Maroc',type:'Client',sector:'Transport aérien',segment:'Grand compte',country:'Maroc',city:'Casablanca',ice:'001012345000090',phone:'0890 000 800',email:'procurement@royalairmaroc.com',owner:'Nadia Berrada',notes:''}, ]; const contacts = [ {id:'CNT-01',firstName:'Yassine',lastName:'Tazi',function:'DSI',clientId:'CLI-01',influence:'Décideur',email:'y.tazi@ocpgroup.ma',phone:'0661 000 001',linkedin:''}, {id:'CNT-02',firstName:'Salma',lastName:'Fassi',function:'Directrice PMO',clientId:'CLI-02',influence:'Prescripteur',email:'s.fassi@oncf.ma',phone:'0661 000 002',linkedin:''}, {id:'CNT-03',firstName:'Omar',lastName:'Idrissi',function:'Responsable Achats',clientId:'CLI-03',influence:'Acheteur',email:'o.idrissi@iam.ma',phone:'0661 000 003',linkedin:''}, {id:'CNT-04',firstName:'Leila',lastName:'Bennis',function:'Directrice CapEx',clientId:'CLI-04',influence:'Décideur',email:'l.bennis@onee.ma',phone:'0661 000 004',linkedin:''}, {id:'CNT-05',firstName:'Hicham',lastName:'Ouazzani',function:'Responsable Contrôle de Gestion',clientId:'CLI-05',influence:'Prescripteur',email:'h.ouazzani@attijariwafa.com',phone:'0661 000 005',linkedin:''}, {id:'CNT-06',firstName:'Imane',lastName:'Sabri',function:'Directrice RSE',clientId:'CLI-06',influence:'Décideur',email:'i.sabri@lydec.ma',phone:'0661 000 006',linkedin:''}, {id:'CNT-07',firstName:'Rachid',lastName:'Alaoui',function:'DG',clientId:'CLI-07',influence:'Décideur',email:'r.alaoui@marjane.ma',phone:'0661 000 007',linkedin:''}, {id:'CNT-08',firstName:'Fatima',lastName:'Zahra Rami',function:'Chef de Division',clientId:'CLI-08',influence:'Utilisateur',email:'f.rami@energie.gov.ma',phone:'0661 000 008',linkedin:''}, ]; const opportunities = [ {id:'OPP-01',name:'PMO Programme Transformation Digitale',clientId:'CLI-01',domain:'PMO',source:'Client existant',status:'Gagnée',owner:'Nadia Berrada',dateIdentified:'2025-11-02',deadline:'2025-12-01',amountEstimated:1450000,currency:'MAD',probability:100,days:180,tjm:5500,description:'Mise en place PMO central pour le programme de transformation digitale du groupe.'}, {id:'OPP-02',name:'Inventaire physique des immobilisations sites miniers',clientId:'CLI-01',domain:"Inventaires d'immobilisations",source:'AO',status:'Gagnée',owner:'Nadia Berrada',dateIdentified:'2025-09-10',deadline:'2025-10-15',amountEstimated:2200000,currency:'MAD',probability:100,days:220,tjm:4800,description:'Inventaire CapEx multi-sites avec réconciliation comptable.'}, {id:'OPP-03',name:'AMO Refonte Processus Achats',clientId:'CLI-02',domain:'AMO',source:'BC',status:'Gagnée',owner:'Karim El Yacoubi',dateIdentified:'2025-08-01',deadline:'2025-09-01',amountEstimated:980000,currency:'MAD',probability:100,days:120,tjm:5000,description:''}, {id:'OPP-04',name:'Diagnostic organisationnel Direction Exploitation',clientId:'CLI-02',domain:'Conseil en organisation',source:'Recommandation',status:'En cours',owner:'Karim El Yacoubi',dateIdentified:'2026-02-01',deadline:'2026-03-15',amountEstimated:620000,currency:'MAD',probability:60,days:60,tjm:5200}, {id:'OPP-05',name:'PMO Déploiement Réseau 5G',clientId:'CLI-03',domain:'Telecom / Infrastructure',source:'AO',status:'Gagnée',owner:'Nadia Berrada',dateIdentified:'2025-06-01',deadline:'2025-07-10',amountEstimated:3100000,currency:'MAD',probability:100,days:300,tjm:6000,description:''}, {id:'OPP-06',name:'Audit performance centres d\'appel',clientId:'CLI-03',domain:'Audit & Diagnostic',source:'Contact personnel',status:'Négociation',owner:'Nadia Berrada',dateIdentified:'2026-04-01',deadline:'2026-05-20',amountEstimated:540000,currency:'MAD',probability:50,days:45,tjm:5000}, {id:'OPP-07',name:'Stratégie CapEx 2026-2030',clientId:'CLI-04',domain:'CapEx',source:'AO',status:'Gagnée',owner:'Sara Amrani',dateIdentified:'2025-05-10',deadline:'2025-06-20',amountEstimated:1750000,currency:'MAD',probability:100,days:200,tjm:5800}, {id:'OPP-08',name:'Digitalisation processus maintenance',clientId:'CLI-04',domain:'Digitalisation',source:'Client existant',status:'Offre soumise',owner:'Sara Amrani',dateIdentified:'2026-03-01',deadline:'2026-05-01',amountEstimated:890000,currency:'MAD',probability:40,days:90,tjm:5100}, {id:'OPP-09',name:'Refonte Contrôle de Gestion Groupe',clientId:'CLI-05',domain:'Management Control',source:'Consultation directe',status:'Gagnée',owner:'Karim El Yacoubi',dateIdentified:'2025-07-01',deadline:'2025-08-05',amountEstimated:1320000,currency:'MAD',probability:100,days:150,tjm:5600}, {id:'OPP-10',name:'Cadrage programme ESG Groupe',clientId:'CLI-05',domain:'Sustainability / ESG',source:'Renewal / Extension',status:'Shortlist',owner:'Karim El Yacoubi',dateIdentified:'2026-05-01',deadline:'2026-06-30',amountEstimated:760000,currency:'MAD',probability:65,days:70,tjm:5300}, {id:'OPP-11',name:'Stratégie ESG & reporting extra-financier',clientId:'CLI-06',domain:'Sustainability / ESG',source:'Client existant',status:'Gagnée',owner:'Sara Amrani',dateIdentified:'2025-04-01',deadline:'2025-05-15',amountEstimated:680000,currency:'MAD',probability:100,days:100,tjm:5000}, {id:'OPP-12',name:'Optimisation processus recouvrement',clientId:'CLI-06',domain:'Process Reengineering',source:'Contact personnel',status:'Analyse en cours',owner:'Sara Amrani',dateIdentified:'2026-06-01',deadline:'2026-07-15',amountEstimated:410000,currency:'MAD',probability:30,days:40,tjm:4900}, {id:'OPP-13',name:'Diagnostic supply chain',clientId:'CLI-07',domain:'Process Reengineering',source:'Consultation directe',status:'Offre en préparation',owner:'Nadia Berrada',dateIdentified:'2026-06-15',deadline:'2026-08-01',amountEstimated:350000,currency:'MAD',probability:35,days:35,tjm:4700}, {id:'OPP-14',name:'Accompagnement gouvernance projets',clientId:'CLI-08',domain:'PMO',source:'AO',status:'Perdue',owner:'Sara Amrani',dateIdentified:'2025-10-01',deadline:'2025-11-05',amountEstimated:920000,currency:'MAD',probability:0,days:100,tjm:5000,lossReason:'Prix — concurrent moins-disant'}, {id:'OPP-15',name:'Stratégie efficacité énergétique nationale',clientId:'CLI-08',domain:'Sustainability / ESG',source:'AO',status:'Offre soumise',owner:'Sara Amrani',dateIdentified:'2026-05-20',deadline:'2026-08-20',amountEstimated:2400000,currency:'MAD',probability:45,days:250,tjm:5900}, {id:'OPP-16',name:'Étude de faisabilité fonds ESG',clientId:'CLI-09',domain:'Sustainability / ESG',source:'Recommandation',status:'Contact établi',owner:'Karim El Yacoubi',dateIdentified:'2026-07-01',deadline:'',amountEstimated:480000,currency:'MAD',probability:20,days:50,tjm:5400}, {id:'OPP-17',name:'PMO Programme flotte & maintenance',clientId:'CLI-10',domain:'PMO',source:'BC',status:'Gagnée',owner:'Nadia Berrada',dateIdentified:'2025-03-01',deadline:'2025-04-01',amountEstimated:1580000,currency:'MAD',probability:100,days:240,tjm:5700}, {id:'OPP-18',name:'Audit CapEx flotte aérienne',clientId:'CLI-10',domain:'CapEx',source:'Client existant',status:'Négociation',owner:'Nadia Berrada',dateIdentified:'2026-04-10',deadline:'2026-06-01',amountEstimated:1100000,currency:'MAD',probability:55,days:110,tjm:5600}, {id:'OPP-19',name:'Mise en place tableau de bord Direction Générale',clientId:'CLI-01',domain:'Digitalisation',source:'Renewal / Extension',status:'Prospect identifié',owner:'Nadia Berrada',dateIdentified:'2026-07-20',deadline:'',amountEstimated:390000,currency:'MAD',probability:15,days:30,tjm:5200}, {id:'OPP-20',name:'Étude d\'impact transition énergétique sites industriels',clientId:'CLI-04',domain:'Sustainability / ESG',source:'Partenaire',status:'Expression d\'intérêt',owner:'Sara Amrani',dateIdentified:'2026-06-25',deadline:'2026-09-01',amountEstimated:960000,currency:'MAD',probability:25,days:100,tjm:5300}, ]; const tenders = [ {id:'TND-01',opportunityId:'OPP-02',tenderRef:'AO-OCP-2025-114',org:'OCP Group',publicationDate:'2025-08-15',deadline:'2025-10-15',openingDate:'2025-10-16',amountProposed:2200000,currency:'MAD',competitors:'Mazars, Deloitte',result:'Gagné',checklist:{'RC':true,'CPS signé':true,'Déclaration sur l\'honneur':true,'Références':true,'CV consultants':true,'Offre technique':true,'Offre financière':true,'Caution provisoire':true,'Documents fiscaux':true,'Documents administratifs':true}}, {id:'TND-02',opportunityId:'OPP-05',tenderRef:'AO-IAM-2025-089',org:'Maroc Telecom',publicationDate:'2025-05-01',deadline:'2025-07-10',openingDate:'2025-07-11',amountProposed:3100000,currency:'MAD',competitors:'PwC, Local Partner Co',result:'Gagné',checklist:{'RC':true,'CPS signé':true,'Déclaration sur l\'honneur':true,'Références':true,'CV consultants':true,'Offre technique':true,'Offre financière':true,'Caution provisoire':true,'Documents fiscaux':true,'Documents administratifs':true}}, {id:'TND-03',opportunityId:'OPP-07',tenderRef:'AO-ONEE-2025-052',org:'ONEE',publicationDate:'2025-04-01',deadline:'2025-06-20',openingDate:'2025-06-21',amountProposed:1750000,currency:'MAD',competitors:'KPMG',result:'Gagné',checklist:{'RC':true,'CPS signé':true,'Déclaration sur l\'honneur':true,'Références':true,'CV consultants':true,'Offre technique':true,'Offre financière':true,'Caution provisoire':true,'Documents fiscaux':true,'Documents administratifs':true}}, {id:'TND-04',opportunityId:'OPP-14',tenderRef:'AO-MTE-2025-071',org:'Ministère Transition Énergétique',publicationDate:'2025-09-01',deadline:'2025-11-05',openingDate:'2025-11-06',amountProposed:920000,currency:'MAD',competitors:'Local Partner Co, EY',result:'Perdu',lossReason:'Prix — concurrent moins-disant',checklist:{'RC':true,'CPS signé':true,'Déclaration sur l\'honneur':true,'Références':true,'CV consultants':true,'Offre technique':true,'Offre financière':true,'Caution provisoire':false,'Documents fiscaux':true,'Documents administratifs':true}}, {id:'TND-05',opportunityId:'OPP-15',tenderRef:'AO-MTE-2026-018',org:'Ministère Transition Énergétique',publicationDate:'2026-05-20',deadline:'2026-08-20',openingDate:'',amountProposed:2400000,currency:'MAD',competitors:'PwC, Mazars',result:'En cours',checklist:{'RC':true,'CPS signé':true,'Déclaration sur l\'honneur':true,'Références':true,'CV consultants':true,'Offre technique':true,'Offre financière':false,'Caution provisoire':false,'Documents fiscaux':true,'Documents administratifs':true}}, ]; const projects = [ {id:'PRJ-01',code:'PRJ-2025-01',name:'PMO Programme Transformation Digitale',clientId:'CLI-01',opportunityId:'OPP-01',sponsor:'Yassine Tazi',pm:'Nadia Berrada',dateStartPlanned:'2025-12-01',dateStartActual:'2025-12-03',dateEndPlanned:'2026-11-30',dateEndActual:'',status:'En cours',priority:'Haute',domain:'PMO',location:'Casablanca',contractAmount:1450000,currency:'MAD',tva:20,paymentTerms:'30 jours net'}, {id:'PRJ-02',code:'PRJ-2025-02',name:'Inventaire physique immobilisations sites miniers',clientId:'CLI-01',opportunityId:'OPP-02',sponsor:'Yassine Tazi',pm:'Sara Amrani',dateStartPlanned:'2025-10-20',dateStartActual:'2025-10-22',dateEndPlanned:'2026-03-01',dateEndActual:'2026-02-25',status:'Terminé',priority:'Haute',domain:"Inventaires d'immobilisations",location:'Khouribga',contractAmount:2200000,currency:'MAD',tva:20,paymentTerms:'30 jours net'}, {id:'PRJ-03',code:'PRJ-2025-03',name:'AMO Refonte Processus Achats',clientId:'CLI-02',opportunityId:'OPP-03',sponsor:'Salma Fassi',pm:'Karim El Yacoubi',dateStartPlanned:'2025-09-05',dateStartActual:'2025-09-05',dateEndPlanned:'2026-01-05',dateEndActual:'',status:'En retard',priority:'Moyenne',domain:'AMO',location:'Rabat',contractAmount:980000,currency:'MAD',tva:20,paymentTerms:'45 jours net'}, {id:'PRJ-04',code:'PRJ-2025-04',name:'PMO Déploiement Réseau 5G',clientId:'CLI-03',opportunityId:'OPP-05',sponsor:'Omar Idrissi',pm:'Nadia Berrada',dateStartPlanned:'2025-07-15',dateStartActual:'2025-07-15',dateEndPlanned:'2026-09-15',dateEndActual:'',status:'En cours',priority:'Haute',domain:'Telecom / Infrastructure',location:'Rabat',contractAmount:3100000,currency:'MAD',tva:20,paymentTerms:'30 jours net'}, {id:'PRJ-05',code:'PRJ-2025-05',name:'Stratégie CapEx 2026-2030',clientId:'CLI-04',opportunityId:'OPP-07',sponsor:'Leila Bennis',pm:'Sara Amrani',dateStartPlanned:'2025-07-01',dateStartActual:'2025-07-01',dateEndPlanned:'2026-01-31',dateEndActual:'2026-01-28',status:'Clôturé',priority:'Haute',domain:'CapEx',location:'Rabat',contractAmount:1750000,currency:'MAD',tva:20,paymentTerms:'30 jours net'}, {id:'PRJ-06',code:'PRJ-2025-06',name:'Refonte Contrôle de Gestion Groupe',clientId:'CLI-05',opportunityId:'OPP-09',sponsor:'Hicham Ouazzani',pm:'Karim El Yacoubi',dateStartPlanned:'2025-08-15',dateStartActual:'2025-08-20',dateEndPlanned:'2026-01-15',dateEndActual:'',status:'En risque',priority:'Haute',domain:'Management Control',location:'Casablanca',contractAmount:1320000,currency:'MAD',tva:20,paymentTerms:'30 jours net'}, {id:'PRJ-07',code:'PRJ-2025-07',name:'Stratégie ESG & reporting extra-financier',clientId:'CLI-06',opportunityId:'OPP-11',sponsor:'Imane Sabri',pm:'Sara Amrani',dateStartPlanned:'2025-05-15',dateStartActual:'2025-05-15',dateEndPlanned:'2025-08-15',dateEndActual:'2025-08-10',status:'Clôturé',priority:'Moyenne',domain:'Sustainability / ESG',location:'Casablanca',contractAmount:680000,currency:'MAD',tva:20,paymentTerms:'30 jours net'}, {id:'PRJ-08',code:'PRJ-2026-08',name:'PMO Programme flotte & maintenance',clientId:'CLI-10',opportunityId:'OPP-17',sponsor:'DG RAM',pm:'Nadia Berrada',dateStartPlanned:'2025-04-01',dateStartActual:'2025-04-01',dateEndPlanned:'2026-10-31',dateEndActual:'',status:'En cours',priority:'Haute',domain:'PMO',location:'Casablanca',contractAmount:1580000,currency:'MAD',tva:20,paymentTerms:'45 jours net'}, {id:'PRJ-09',code:'PRJ-2026-09',name:'Diagnostic organisationnel Direction Exploitation',clientId:'CLI-02',opportunityId:'OPP-04',sponsor:'Salma Fassi',pm:'Karim El Yacoubi',dateStartPlanned:'2026-03-01',dateStartActual:'2026-03-05',dateEndPlanned:'2026-09-30',dateEndActual:'',status:'En cours',priority:'Moyenne',domain:'Conseil en organisation',location:'Rabat',contractAmount:620000,currency:'MAD',tva:20,paymentTerms:'30 jours net'}, {id:'PRJ-10',code:'PRJ-2026-10',name:'Audit performance centres d\'appel',clientId:'CLI-03',opportunityId:'OPP-06',sponsor:'Omar Idrissi',pm:'Nadia Berrada',dateStartPlanned:'2026-06-01',dateStartActual:'',dateEndPlanned:'2026-08-15',dateEndActual:'',status:'À démarrer',priority:'Basse',domain:'Audit & Diagnostic',location:'Rabat',contractAmount:540000,currency:'MAD',tva:20,paymentTerms:'30 jours net'}, ]; const resources = [ {id:'RES-01',name:'Nadia Berrada',role:'Directrice PMO',grade:'Directeur',expertise:'PMO / Transformation',tjm:6500,dailyCost:3200,capacityMonthly:18,status:'Affecté'}, {id:'RES-02',name:'Karim El Yacoubi',role:'Senior Manager',grade:'Manager',expertise:'AMO / Contrôle de gestion',tjm:5800,dailyCost:2800,capacityMonthly:19,status:'Affecté'}, {id:'RES-03',name:'Sara Amrani',role:'Senior Manager',grade:'Manager',expertise:'ESG / CapEx',tjm:5600,dailyCost:2700,capacityMonthly:19,status:'Surchargé'}, {id:'RES-04',name:'Yassir Kabbaj',role:'Consultant Senior',grade:'Senior',expertise:'PMO / Digitalisation',tjm:4200,dailyCost:2000,capacityMonthly:20,status:'Affecté'}, {id:'RES-05',name:'Meryem Chraibi',role:'Consultant Senior',grade:'Senior',expertise:"Inventaires d'immobilisations",tjm:4000,dailyCost:1900,capacityMonthly:20,status:'Disponible'}, {id:'RES-06',name:'Anas Benjelloun',role:'Consultant Confirmé',grade:'Confirmé',expertise:'Process Reengineering',tjm:3400,dailyCost:1600,capacityMonthly:20,status:'Affecté'}, {id:'RES-07',name:'Hind Alami',role:'Consultant Confirmé',grade:'Confirmé',expertise:'ESG / Sustainability',tjm:3400,dailyCost:1600,capacityMonthly:20,status:'Affecté'}, {id:'RES-08',name:'Tarik Moujahid',role:'Consultant Junior',grade:'Junior',expertise:'CapEx / Audit',tjm:2400,dailyCost:1100,capacityMonthly:20,status:'Disponible'}, {id:'RES-09',name:'Zineb Rguibi',role:'Consultant Junior',grade:'Junior',expertise:'Digitalisation',tjm:2400,dailyCost:1100,capacityMonthly:20,status:'Affecté'}, {id:'RES-10',name:'Mehdi Sqalli',role:'Consultant Confirmé',grade:'Confirmé',expertise:'Telecom / Infrastructure',tjm:3600,dailyCost:1700,capacityMonthly:20,status:'Surchargé'}, ]; const allocations = [ {id:'ALC-01',projectId:'PRJ-01',resourceId:'RES-01',daysPlanned:60},{id:'ALC-02',projectId:'PRJ-01',resourceId:'RES-04',daysPlanned:90},{id:'ALC-03',projectId:'PRJ-01',resourceId:'RES-09',daysPlanned:70}, {id:'ALC-04',projectId:'PRJ-02',resourceId:'RES-05',daysPlanned:120},{id:'ALC-05',projectId:'PRJ-02',resourceId:'RES-08',daysPlanned:130}, {id:'ALC-06',projectId:'PRJ-03',resourceId:'RES-02',daysPlanned:70},{id:'ALC-07',projectId:'PRJ-03',resourceId:'RES-06',daysPlanned:80}, {id:'ALC-08',projectId:'PRJ-04',resourceId:'RES-01',daysPlanned:100},{id:'ALC-09',projectId:'PRJ-04',resourceId:'RES-10',daysPlanned:150}, {id:'ALC-10',projectId:'PRJ-05',resourceId:'RES-03',daysPlanned:90},{id:'ALC-11',projectId:'PRJ-05',resourceId:'RES-07',daysPlanned:60}, {id:'ALC-12',projectId:'PRJ-06',resourceId:'RES-02',daysPlanned:110},{id:'ALC-13',projectId:'PRJ-06',resourceId:'RES-06',daysPlanned:60}, {id:'ALC-14',projectId:'PRJ-07',resourceId:'RES-03',daysPlanned:50},{id:'ALC-15',projectId:'PRJ-07',resourceId:'RES-07',daysPlanned:40}, {id:'ALC-16',projectId:'PRJ-08',resourceId:'RES-01',daysPlanned:130},{id:'ALC-17',projectId:'PRJ-08',resourceId:'RES-10',daysPlanned:100}, {id:'ALC-18',projectId:'PRJ-09',resourceId:'RES-02',daysPlanned:45},{id:'ALC-19',projectId:'PRJ-09',resourceId:'RES-06',daysPlanned:30}, {id:'ALC-20',projectId:'PRJ-10',resourceId:'RES-01',daysPlanned:25}, ]; const timesheets = []; let tsCounter=1; function addTS(projectId,resourceId,days,activity,daysAgo){ timesheets.push({id:'TS-'+(tsCounter++),projectId,resourceId,date:new Date(Date.now()-daysAgo*86400000).toISOString().slice(0,10),days,activity,comment:''}); } addTS('PRJ-01','RES-01',18,'Pilotage PMO',10); addTS('PRJ-01','RES-04',55,'Mise en place outils PMO',20); addTS('PRJ-01','RES-09',30,'Digitalisation reporting',15); addTS('PRJ-02','RES-05',120,'Inventaire terrain',60); addTS('PRJ-02','RES-08',130,'Réconciliation comptable',45); addTS('PRJ-03','RES-02',75,'Cadrage processus achats',90); addTS('PRJ-03','RES-06',82,'Ateliers métier',85); addTS('PRJ-04','RES-01',60,'Pilotage programme 5G',30); addTS('PRJ-04','RES-10',95,'Suivi déploiement sites',25); addTS('PRJ-05','RES-03',90,'Élaboration stratégie CapEx',120); addTS('PRJ-05','RES-07',60,'Analyse portefeuille actifs',110); addTS('PRJ-06','RES-02',95,'Refonte processus CG',40); addTS('PRJ-06','RES-06',58,'Paramétrage outils',35); addTS('PRJ-07','RES-03',50,'Cadrage stratégie ESG',150); addTS('PRJ-07','RES-07',40,'Reporting extra-financier',140); addTS('PRJ-08','RES-01',95,'Pilotage PMO flotte',20); addTS('PRJ-08','RES-10',70,'Suivi maintenance',18); addTS('PRJ-09','RES-02',20,'Diagnostic organisationnel',10); addTS('PRJ-09','RES-06',15,'Entretiens métier',8); addTS('PRJ-10','RES-01',3,'Cadrage initial',3); const tasks = []; let taskCounter=1; function addTask(projectId,name,type,owner,weight,progress,status,dateStart,dateEnd){ tasks.push({id:'TSK-'+(taskCounter++),projectId,name,type,owner,weight,progress,status,dateStart,dateEnd}); } addTask('PRJ-01','Cadrage & mobilisation PMO','Jalon','Nadia Berrada',15,100,'Terminé','2025-12-01','2025-12-20'); addTask('PRJ-01','Déploiement outils de pilotage','Livrable','Yassir Kabbaj',35,80,'En cours','2025-12-20','2026-03-01'); addTask('PRJ-01','Reporting exécutif mensuel','Livrable','Zineb Rguibi',30,55,'En cours','2026-01-01','2026-05-01'); addTask('PRJ-01','Bilan & recommandations','Livrable','Nadia Berrada',20,0,'À faire','2026-05-01','2026-05-31'); addTask('PRJ-02','Diagnostic & plan d\'inventaire','Livrable','Meryem Chraibi',20,100,'Terminé','2025-10-22','2025-11-10'); addTask('PRJ-02','Inventaire terrain multi-sites','Livrable','Meryem Chraibi',40,100,'Terminé','2025-11-10','2026-01-20'); addTask('PRJ-02','Réconciliation comptable','Livrable','Tarik Moujahid',30,100,'Terminé','2026-01-20','2026-02-15'); addTask('PRJ-02','Restitution finale','Jalon','Sara Amrani',10,100,'Terminé','2026-02-15','2026-02-25'); addTask('PRJ-03','Diagnostic processus achats actuel','Livrable','Karim El Yacoubi',25,100,'Terminé','2025-09-05','2025-10-05'); addTask('PRJ-03','Conception nouveau processus','Livrable','Anas Benjelloun',35,60,'En retard','2025-10-05','2025-12-01'); addTask('PRJ-03','Accompagnement au changement','Livrable','Anas Benjelloun',25,20,'En retard','2025-12-01','2026-01-05'); addTask('PRJ-03','Clôture & transfert de compétences','Jalon','Karim El Yacoubi',15,0,'À faire','2026-01-05','2026-01-05'); addTask('PRJ-04','Cadrage & gouvernance programme','Jalon','Nadia Berrada',10,100,'Terminé','2025-07-15','2025-08-15'); addTask('PRJ-04','Déploiement phase 1 (zones urbaines)','Livrable','Mehdi Sqalli',30,90,'En cours','2025-08-15','2026-01-15'); addTask('PRJ-04','Déploiement phase 2 (zones rurales)','Livrable','Mehdi Sqalli',35,35,'En cours','2026-01-15','2026-07-15'); addTask('PRJ-04','Clôture programme','Jalon','Nadia Berrada',25,0,'À faire','2026-07-15','2026-09-15'); addTask('PRJ-05','Diagnostic portefeuille actifs','Livrable','Sara Amrani',30,100,'Terminé','2025-07-01','2025-09-01'); addTask('PRJ-05','Élaboration stratégie CapEx 5 ans','Livrable','Hind Alami',40,100,'Terminé','2025-09-01','2025-12-01'); addTask('PRJ-05','Présentation & validation COMEX','Jalon','Sara Amrani',30,100,'Terminé','2025-12-01','2026-01-28'); addTask('PRJ-06','Diagnostic contrôle de gestion','Livrable','Karim El Yacoubi',25,100,'Terminé','2025-08-20','2025-10-01'); addTask('PRJ-06','Refonte processus & outils','Livrable','Anas Benjelloun',40,45,'En cours','2025-10-01','2026-01-01'); addTask('PRJ-06','Formation & déploiement','Livrable','Karim El Yacoubi',35,10,'En retard','2026-01-01','2026-01-15'); addTask('PRJ-07','Cadrage stratégie ESG','Livrable','Sara Amrani',30,100,'Terminé','2025-05-15','2025-06-15'); addTask('PRJ-07','Reporting extra-financier','Livrable','Hind Alami',40,100,'Terminé','2025-06-15','2025-07-30'); addTask('PRJ-07','Restitution & feuille de route','Jalon','Sara Amrani',30,100,'Terminé','2025-07-30','2025-08-10'); addTask('PRJ-08','Mobilisation PMO flotte','Jalon','Nadia Berrada',15,100,'Terminé','2025-04-01','2025-04-30'); addTask('PRJ-08','Pilotage maintenance préventive','Livrable','Mehdi Sqalli',45,70,'En cours','2025-04-30','2025-11-01'); addTask('PRJ-08','Optimisation coûts flotte','Livrable','Nadia Berrada',40,40,'En cours','2025-11-01','2026-01-31'); addTask('PRJ-09','Diagnostic organisationnel','Livrable','Karim El Yacoubi',50,60,'En cours','2026-03-05','2026-04-30'); addTask('PRJ-09','Recommandations & plan d\'action','Livrable','Anas Benjelloun',50,10,'À faire','2026-04-30','2026-06-01'); const invoices = [ {id:'INV-01',number:'FA2025-101',projectId:'PRJ-01',date:'2025-12-15',dueDate:'2026-01-14',amountHT:435000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-02',number:'FA2026-014',projectId:'PRJ-01',date:'2026-03-01',dueDate:'2026-03-31',amountHT:435000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-03',number:'FA2026-052',projectId:'PRJ-01',date:'2026-06-01',dueDate:'2026-07-01',amountHT:290000,tva:20,currency:'MAD',status:'En retard'}, {id:'INV-04',number:'FA2025-088',projectId:'PRJ-02',date:'2025-11-01',dueDate:'2025-12-01',amountHT:880000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-05',number:'FA2026-005',projectId:'PRJ-02',date:'2026-02-25',dueDate:'2026-03-25',amountHT:1320000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-06',number:'FA2025-095',projectId:'PRJ-03',date:'2025-10-10',dueDate:'2025-11-24',amountHT:392000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-07',number:'FA2026-021',projectId:'PRJ-03',date:'2026-01-05',dueDate:'2026-02-19',amountHT:294000,tva:20,currency:'MAD',status:'En retard'}, {id:'INV-08',number:'FA2025-072',projectId:'PRJ-04',date:'2025-08-20',dueDate:'2025-09-19',amountHT:930000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-09',number:'FA2026-002',projectId:'PRJ-04',date:'2026-01-20',dueDate:'2026-02-19',amountHT:930000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-10',number:'FA2026-061',projectId:'PRJ-04',date:'2026-07-01',dueDate:'2026-07-31',amountHT:620000,tva:20,currency:'MAD',status:'Envoyée'}, {id:'INV-11',number:'FA2025-060',projectId:'PRJ-05',date:'2025-08-05',dueDate:'2025-09-04',amountHT:875000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-12',number:'FA2026-004',projectId:'PRJ-05',date:'2026-01-28',dueDate:'2026-02-27',amountHT:875000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-13',number:'FA2025-081',projectId:'PRJ-06',date:'2025-09-20',dueDate:'2025-10-20',amountHT:528000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-14',number:'FA2026-030',projectId:'PRJ-06',date:'2026-01-15',dueDate:'2026-02-14',amountHT:396000,tva:20,currency:'MAD',status:'Partiellement payée'}, {id:'INV-15',number:'FA2025-050',projectId:'PRJ-07',date:'2025-06-01',dueDate:'2025-07-01',amountHT:340000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-16',number:'FA2025-078',projectId:'PRJ-07',date:'2025-08-10',dueDate:'2025-09-09',amountHT:340000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-17',number:'FA2025-055',projectId:'PRJ-08',date:'2025-05-01',dueDate:'2025-06-15',amountHT:632000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-18',number:'FA2025-099',projectId:'PRJ-08',date:'2025-11-01',dueDate:'2025-12-16',amountHT:632000,tva:20,currency:'MAD',status:'Payée'}, {id:'INV-19',number:'FA2026-058',projectId:'PRJ-08',date:'2026-06-15',dueDate:'2026-07-30',amountHT:316000,tva:20,currency:'MAD',status:'À facturer'}, {id:'INV-20',number:'FA2026-045',projectId:'PRJ-09',date:'2026-04-01',dueDate:'2026-05-01',amountHT:186000,tva:20,currency:'MAD',status:'Payée'}, ]; const payments = [ {id:'PAY-01',invoiceId:'INV-01',date:'2026-01-10',amount:522000,method:'Virement'}, {id:'PAY-02',invoiceId:'INV-02',date:'2026-03-28',amount:522000,method:'Virement'}, {id:'PAY-03',invoiceId:'INV-04',date:'2025-11-28',amount:1056000,method:'Virement'}, {id:'PAY-04',invoiceId:'INV-05',date:'2026-03-20',amount:1584000,method:'Virement'}, {id:'PAY-05',invoiceId:'INV-06',date:'2025-11-20',amount:470400,method:'Chèque'}, {id:'PAY-06',invoiceId:'INV-08',date:'2025-09-15',amount:1116000,method:'Virement'}, {id:'PAY-07',invoiceId:'INV-09',date:'2026-02-15',amount:1116000,method:'Virement'}, {id:'PAY-08',invoiceId:'INV-11',date:'2025-08-30',amount:1050000,method:'Virement'}, {id:'PAY-09',invoiceId:'INV-12',date:'2026-02-25',amount:1050000,method:'Virement'}, {id:'PAY-10',invoiceId:'INV-13',date:'2025-10-18',amount:633600,method:'Virement'}, {id:'PAY-11',invoiceId:'INV-14',date:'2026-02-10',amount:250000,method:'Chèque'}, {id:'PAY-12',invoiceId:'INV-15',date:'2025-06-25',amount:408000,method:'Virement'}, {id:'PAY-13',invoiceId:'INV-16',date:'2025-09-05',amount:408000,method:'Virement'}, {id:'PAY-14',invoiceId:'INV-17',date:'2025-06-10',amount:758400,method:'Virement'}, {id:'PAY-15',invoiceId:'INV-18',date:'2025-12-10',amount:758400,method:'Virement'}, {id:'PAY-16',invoiceId:'INV-20',date:'2026-04-28',amount:223200,method:'Virement'}, ]; const directCosts = [ {id:'DC-01',projectId:'PRJ-01',date:'2025-12-10',supplier:'Royal Air Maroc',category:'Déplacement',amountHT:12000,currency:'MAD',status:'Validé'}, {id:'DC-02',projectId:'PRJ-01',date:'2026-01-15',supplier:'Licences Monday.com',category:'Matériel',amountHT:8500,currency:'MAD',status:'Validé'}, {id:'DC-03',projectId:'PRJ-02',date:'2025-11-05',supplier:'Hôtel Khouribga',category:'Hébergement',amountHT:45000,currency:'MAD',status:'Validé'}, {id:'DC-04',projectId:'PRJ-02',date:'2025-12-01',supplier:'Cabinet Expertise Externe',category:'Sous-traitance',amountHT:180000,currency:'MAD',status:'Validé'}, {id:'DC-05',projectId:'PRJ-02',date:'2026-01-10',supplier:'Transport local',category:'Transport',amountHT:22000,currency:'MAD',status:'Validé'}, {id:'DC-06',projectId:'PRJ-03',date:'2025-10-01',supplier:'Consultant Freelance Achats',category:'Freelance',amountHT:95000,currency:'MAD',status:'Validé'}, {id:'DC-07',projectId:'PRJ-03',date:'2025-11-20',supplier:'Impression & documentation',category:'Impression',amountHT:6000,currency:'MAD',status:'Validé'}, {id:'DC-08',projectId:'PRJ-04',date:'2025-08-01',supplier:'Déplacements sites régionaux',category:'Déplacement',amountHT:65000,currency:'MAD',status:'Validé'}, {id:'DC-09',projectId:'PRJ-04',date:'2026-02-01',supplier:'Sous-traitant technique réseau',category:'Sous-traitance',amountHT:220000,currency:'MAD',status:'Validé'}, {id:'DC-10',projectId:'PRJ-05',date:'2025-09-15',supplier:'Étude marché externe',category:'Sous-traitance',amountHT:75000,currency:'MAD',status:'Validé'}, {id:'DC-11',projectId:'PRJ-06',date:'2025-10-01',supplier:'Freelance change management',category:'Freelance',amountHT:110000,currency:'MAD',status:'Validé'}, {id:'DC-12',projectId:'PRJ-06',date:'2025-12-01',supplier:'Logiciel de modélisation',category:'Matériel',amountHT:35000,currency:'MAD',status:'Validé'}, {id:'DC-13',projectId:'PRJ-06',date:'2026-01-05',supplier:'Ateliers formation',category:'Logistique',amountHT:18000,currency:'MAD',status:'À valider'}, {id:'DC-14',projectId:'PRJ-07',date:'2025-06-10',supplier:'Cabinet audit ESG externe',category:'Sous-traitance',amountHT:60000,currency:'MAD',status:'Validé'}, {id:'DC-15',projectId:'PRJ-08',date:'2025-05-05',supplier:'Déplacements aéroport',category:'Déplacement',amountHT:38000,currency:'MAD',status:'Validé'}, {id:'DC-16',projectId:'PRJ-08',date:'2025-11-10',supplier:'Sous-traitant maintenance',category:'Sous-traitance',amountHT:145000,currency:'MAD',status:'Validé'}, {id:'DC-17',projectId:'PRJ-08',date:'2026-05-01',supplier:'Matériel diagnostic',category:'Matériel',amountHT:27000,currency:'MAD',status:'Validé'}, {id:'DC-18',projectId:'PRJ-09',date:'2026-03-10',supplier:'Déplacements Rabat-Casa',category:'Transport',amountHT:9000,currency:'MAD',status:'Validé'}, {id:'DC-19',projectId:'PRJ-10',date:'2026-06-05',supplier:'Frais de cadrage',category:'Autre',amountHT:5000,currency:'MAD',status:'À valider'}, {id:'DC-20',projectId:'PRJ-04',date:'2026-05-01',supplier:'Communication programme',category:'Autre',amountHT:15000,currency:'MAD',status:'Validé'}, {id:'DC-21',projectId:'PRJ-09',date:'2026-05-15',supplier:'Cabinet spécialisé (renfort diagnostic)',category:'Sous-traitance',amountHT:245000,currency:'MAD',status:'Validé'}, ]; const risks = [ {id:'RSK-01',projectId:'PRJ-03',description:'Retard de mobilisation des équipes métier ONCF',category:'Planning',probability:4,impact:4,owner:'Karim El Yacoubi',actionPlan:'Escalade au sponsor + renfort consultant',targetDate:'2026-09-01',status:'Ouvert'}, {id:'RSK-02',projectId:'PRJ-03',description:'Dérive de périmètre non cadrée',category:'Contractuel',probability:3,impact:4,owner:'Karim El Yacoubi',actionPlan:'Avenant en cours de négociation',targetDate:'2026-09-15',status:'En traitement'}, {id:'RSK-03',projectId:'PRJ-06',description:'Résistance au changement des équipes finance',category:'Ressources',probability:4,impact:3,owner:'Karim El Yacoubi',actionPlan:'Plan de conduite du changement renforcé',targetDate:'2026-08-30',status:'Ouvert'}, {id:'RSK-04',projectId:'PRJ-04',description:'Dépendance fournisseur télécom tiers pour déploiement',category:'Technique',probability:3,impact:5,owner:'Nadia Berrada',actionPlan:'Plan de contingence multi-fournisseurs',targetDate:'2026-10-01',status:'En traitement'}, {id:'RSK-05',projectId:'PRJ-08',description:'Surcharge de la ressource clé (Mehdi Sqalli)',category:'Ressources',probability:4,impact:3,owner:'Nadia Berrada',actionPlan:'Renfort junior à prévoir T3',targetDate:'2026-09-01',status:'Ouvert'}, {id:'RSK-06',projectId:'PRJ-01',description:'Adoption lente des nouveaux outils PMO',category:'Autre',probability:2,impact:2,owner:'Nadia Berrada',actionPlan:'Sessions de coaching utilisateurs',targetDate:'2026-08-01',status:'Clôturé'}, {id:'RSK-07',projectId:'PRJ-09',description:'Dépassement budgétaire lié au renfort d\'un cabinet spécialisé',category:'Budget',probability:4,impact:4,owner:'Karim El Yacoubi',actionPlan:'Renégocier le périmètre restant avec le sponsor',targetDate:'2026-09-01',status:'Ouvert'}, ]; const documents = [ {id:'DOC-01',name:'CPS_AO-OCP-2025-114.pdf',type:'CPS',projectId:'PRJ-02',clientId:'CLI-01',dateAdded:'2025-08-15',url:''}, {id:'DOC-02',name:'Contrat_PRJ-2025-01.pdf',type:'Contrat',projectId:'PRJ-01',clientId:'CLI-01',dateAdded:'2025-12-01',url:''}, {id:'DOC-03',name:'Offre_technique_5G_IAM.pdf',type:'Offre technique',projectId:'PRJ-04',clientId:'CLI-03',dateAdded:'2025-06-20',url:''}, {id:'DOC-04',name:'PV_Comite_Pilotage_PRJ06_Jan26.pdf',type:'PV',projectId:'PRJ-06',clientId:'CLI-05',dateAdded:'2026-01-30',url:''}, {id:'DOC-05',name:'Rapport_final_Inventaire_OCP.pdf',type:'Rapport',projectId:'PRJ-02',clientId:'CLI-01',dateAdded:'2026-02-25',url:''}, ]; db = {clients, contacts, opportunities, tenders, projects, resources, allocations, timesheets, tasks, invoices, payments, directCosts, risks, documents}; saveDB(); if(typeof ensureUsersSeed==='function') ensureUsersSeed(); } /* ========================================================================= INIT / BOOT ========================================================================= */ function loadAdminCfgOverrides(){ const raw = localStorage.getItem(cfgKey()); if(raw){ try{ const o=JSON.parse(raw); if(o.indirectRate) CFG.indirectRate=o.indirectRate; if(o.fx) Object.assign(CFG.fx,o.fx); if(o.company) Object.assign(CFG.company,o.company); if(o.docCounters) Object.assign(CFG.docCounters,o.docCounters); if(o.cloudSync) Object.assign(CFG.cloudSync,o.cloudSync); if(o.multiTenant) Object.assign(CFG.multiTenant,o.multiTenant); if(o.rolePermissions) CFG.rolePermissions = o.rolePermissions; if(o.taskTemplates && o.taskTemplates.length) CFG.taskTemplates = o.taskTemplates; }catch(e){} } } (function boot(){ initDatabaseForEnv(); ensureUsersSeed(); loadAdminCfgOverrides(); if(typeof mermaid !== 'undefined'){ try{ mermaid.initialize({ startOnLoad:false, theme:'base', themeVariables:{ primaryColor:'#0d9488', primaryTextColor:'#0b1220', primaryBorderColor:'#0d9488', lineColor:'#9aa4b2', secondaryColor:'#dbeafe', tertiaryColor:'#f4f6f9', fontFamily:'-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif' }}); }catch(e){} } function enterApp(){ ROUTE = 'dashboard'; document.getElementById('loginScreen').style.display = 'none'; document.getElementById('app').style.display = ''; render(); } // Retour depuis un email Supabase (reset password / invite) : Supabase redirige vers // AUTH_REDIRECT_URL avec les paramètres dans le fragment d'URL (#access_token=...&type=...), // ou avec #error=...&error_code=... si le lien est expiré/invalide/déjà utilisé. On traite ce // cas AVANT toute reprise de session normale, et on nettoie systématiquement le fragment de // l'URL (history.replaceState) pour ne jamais laisser un token trainer dans l'historique du // navigateur ni le revoir si la page est rechargée. function handleAuthRedirectHash(){ if(!location.hash || location.hash.length<2) return false; let params; try{ params = new URLSearchParams(location.hash.substring(1)); }catch(e){ return false; } const hasToken = params.has('access_token'); const hasError = params.has('error') || params.has('error_code'); if(!hasToken && !hasError) return false; const cleanUrl = location.pathname + location.search; history.replaceState(null, '', cleanUrl); showLoginScreen(); if(hasError){ const msg = authErrorMessageFR(new Error(params.get('error_code') || params.get('error_description') || params.get('error') || '')); showForgotStep(); const err = document.getElementById('loginError'); err.style.color = '#dc2626'; err.textContent = msg; return true; } const type = params.get('type') || ''; if(type === 'recovery' || type === 'invite'){ showResetPasswordStep(params.get('access_token'), type === 'invite'); return true; } // Autre type de redirection Auth (ex: magiclink, email_change) non utilisé par cette app : // on ignore silencieusement et on laisse l'écran de connexion normal s'afficher. return true; } const handledAuthRedirect = handleAuthRedirectHash(); if(handledAuthRedirect){ // Un lien de récupération/invite vient d'être traité : ne pas tenter de reprendre une // session existante par-dessus (l'utilisateur doit d'abord définir son nouveau mot de passe). } else if(mtEnabled()){ mtRestoreSession().then(outcome=>{ if(outcome && outcome.status==='needs_org_selection'){ renderOrgSelectScreen(false); return; } if(outcome) enterApp(); else showLoginScreen(); }); } else if(cloudEnabled()){ cloudRestoreSession().then(user=>{ if(user) enterApp(); else showLoginScreen(); }); } else { const savedUserId = sessionStorage.getItem(sessionKey()); const savedUser = savedUserId ? byId(db.users, savedUserId) : null; if(savedUser){ CURRENT_USER = savedUser; CURRENT_ROLE = savedUser.role; enterApp(); } else { showLoginScreen(); } } })();