// gp-data.jsx — icons, module registry, mock data, recipes, helpers
// ───────────────────────────────────────────────────────────────

// Minimal line-icon set (UI glyphs only)
const ICONS = {
  grid:      ['M3 3h7v7H3zM14 3h7v7h-7zM14 14h7v7h-7zM3 14h7v7H3z'],
  layers:    ['M12 2 2 7l10 5 10-5z','M2 12l10 5 10-5','M2 17l10 5 10-5'],
  foundation:['M3 21h18','M5 21V10l7-5 7 5v11','M9 21v-6h6v6'],
  wall:      ['M3 3h18v18H3z','M3 9h18','M3 15h18','M8 3v6','M16 9v6','M8 15v6'],
  roof:      ['M3 12 12 4l9 8','M5 11v9h14v-9','M10 20v-5h4v5'],
  insulation:['M4 4h16v16H4z','M4 9c3 0 3 3 6 3s3-3 6-3 3 3 4 3','M4 15c3 0 3 3 6 3s3-3 6-3 3 3 4 3'],
  batt:      ['M3 5h18v5H3zM3 14h18v5H3z','M7 5v5M14 5v5M10 14v5M17 14v5'],
  hemp:      ['M5 13h14v8H5z','M12 13V8','M12 8c-2 0-3-1-3-3 2 0 3 1 3 3z','M12 8c2 0 3-1 3-3-2 0-3 1-3 3z'],
  sound:     ['M11 5 6 9H2v6h4l5 4z','M15.5 8.5a5 5 0 0 1 0 7','M19 5a9 9 0 0 1 0 14'],
  window:    ['M4 3h16v18H4z','M12 3v18','M4 12h16'],
  plank:     ['M3 5h18v4H3zM3 11h18v4H3zM3 17h18v4H3z','M8 5v4M15 11v4M11 17v4'],
  floor:     ['M3 4h18v16H3z','M3 9h18M3 14h18','M9 4v5M15 9v5M9 14v6'],
  frame:     ['M4 3v18M20 3v18M4 3h16M4 21h16','M4 9h16M4 15h16'],
  tape:      ['M4 7h16v6a4 4 0 0 1-4 4H4z','M4 7a4 4 0 0 1 4-4h12v4','M8 11h.01M12 11h.01M16 11h.01'],
  fan:       ['M12 12a3 3 0 0 0 3-3c0-3-3-5-3-7 0 0-5 1-5 6a3 3 0 0 0 5 4z','M12 12a3 3 0 0 0-3 3c0 3 3 5 3 7 0 0 5-1 5-6a3 3 0 0 0-5-4z'],
  thermo:    ['M14 14V5a2 2 0 0 0-4 0v9a4 4 0 1 0 4 0z','M12 9v5'],
  box:       ['M3 7l9-4 9 4v10l-9 4-9-4z','M3 7l9 4 9-4M12 11v10'],
  receipt:   ['M5 3h14v18l-2.5-1.6L14 21l-2-1.6L10 21l-2.5-1.6L5 21z','M9 8h6M9 12h6'],
  beaker:    ['M9 3h6M10 3v6l-5 9a2 2 0 0 0 2 3h10a2 2 0 0 0 2-3l-5-9V3','M7.5 15h9'],
  clipboard: ['M9 4h6a1 1 0 0 1 1 1v1H8V5a1 1 0 0 1 1-1z','M8 6H6a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-2','M8 12h8M8 16h5'],
  folders:   ['M4 7a2 2 0 0 1 2-2h3l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z','M8 5V3.5A1.5 1.5 0 0 1 9.5 2H13l2 2'],
  folderPlus:['M4 7a2 2 0 0 1 2-2h3l2 2h7a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z','M12 11v6M9 14h6'],
  pencil:    ['M12 20h9','M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z'],
  gripV:     ['M9 5h.01M9 12h.01M9 19h.01M15 5h.01M15 12h.01M15 19h.01'],
  mail:      ['M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z','M3 7l9 6 9-6'],
  history:   ['M3 12a9 9 0 1 0 3-6.7L3 8','M3 4v4h4','M12 8v4l3 2'],
  users:     ['M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2','M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z','M22 21v-2a4 4 0 0 0-3-3.9','M16 3.1a4 4 0 0 1 0 7.8'],
  shield:    ['M12 3l8 3v6c0 5-3.5 8-8 9-4.5-1-8-4-8-9V6z','M9 12l2 2 4-4'],
  logout:    ['M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4','M16 17l5-5-5-5','M21 12H9'],
  sparkles:  ['M12 3l1.9 5.1L19 10l-5.1 1.9L12 17l-1.9-5.1L5 10l5.1-1.9z','M19 3l.7 1.9L21.5 5.5l-1.8.6L19 8l-.6-1.9L16.5 5.5l1.9-.6z'],
  keyboard:  ['M4 6h16a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1z','M7 10h.01M11 10h.01M15 10h.01M17 14H7'],
  square:    ['M4 4h16v16H4z'],
  polygon:   ['M12 3l8.5 6.2-3.2 10H6.7l-3.2-10z'],
  circle:    ['M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z'],
  textT:     ['M5 5h14','M12 5v14','M9 19h6'],
  cube:      ['M12 2 3 7v10l9 5 9-5V7z','M3 7l9 5 9-5','M12 12v10'],
  dim:       ['M3 6v12M21 6v12M5 12h14','M8 9l-3 3 3 3M16 9l3 3-3 3'],
  duct:      ['M3 8h12a3 3 0 0 1 3 3v0a3 3 0 0 0 3 3h0','M3 8v8h12v-5','M3 12h12'],
  grille:    ['M4 5h16v14H4z','M4 9h16M4 13h16M4 17h16'],
  sync:      ['M21 2v6h-6','M3 12a9 9 0 0 1 15-6.7L21 8','M3 22v-6h6','M21 12a9 9 0 0 1-15 6.7L3 16'],
  cloud:     ['M17.5 19a4.5 4.5 0 0 0 0-9 6 6 0 0 0-11.6 1.5A3.5 3.5 0 0 0 6.5 19z'],
  plug:      ['M9 2v6M15 2v6','M7 8h10v3a5 5 0 0 1-10 0z','M12 16v6'],
  alert:     ['M12 3 2 20h20z','M12 10v5M12 18h.01'],
  search:    ['M11 18a7 7 0 1 0 0-14 7 7 0 0 0 0 14z','M21 21l-4.35-4.35'],
  globe:     ['M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z','M3 12h18','M12 3a14 14 0 0 1 0 18a14 14 0 0 1 0-18z'],
  key:       ['M14 7a4 4 0 1 0 0 8 4 4 0 0 0 0-8z','M11 13l-7 7','M7 17l2 2','M5 19l1.5 1.5'],
  eye:       ['M2 12s4-7 10-7 10 7 10 7-4 7-10 7-10-7-10-7z','M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'],
  robot:     ['M7 8h10a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2z','M12 4v4','M9 13h.01M15 13h.01','M3 12v3M21 12v3'],
  sun:       ['M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10z','M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4'],
  moon:      ['M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z'],
  sliders:   ['M4 8h16M4 16h16','M9 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4z','M15 14a2 2 0 1 0 0 4 2 2 0 0 0 0-4z'],
  ruler:     ['M3 17 17 3l4 4L7 21z','M7 9l2 2M11 5l2 2M9 13l2 2M13 9l2 2'],
  chev:      ['M9 6l6 6-6 6'],
  chevd:     ['M6 9l6 6 6-6'],
  plus:      ['M12 5v14M5 12h14'],
  trash:     ['M3 6h18M8 6V4h8v2M6 6l1 14h10l1-14'],
  copy:      ['M9 9h11v11H9z','M5 15H4V4h11v1'],
  file:      ['M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z','M14 2v6h6'],
  download:  ['M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4','M7 10l5 5 5-5','M12 15V3'],
  settings:  ['M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z','M19 12a7 7 0 0 0-.1-1l2-1.6-2-3.4-2.4 1a7 7 0 0 0-1.7-1l-.4-2.5h-4l-.4 2.5a7 7 0 0 0-1.7 1l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 1.7 1l.4 2.5h4l.4-2.5a7 7 0 0 0 1.7-1l2.4 1 2-3.4-2-1.6a7 7 0 0 0 .1-1z'],
  check:     ['M20 6 9 17l-5-5'],
  link:      ['M9 15l6-6','M11 6l1-1a4 4 0 0 1 6 6l-1 1','M13 18l-1 1a4 4 0 0 1-6-6l1-1'],
  scale:     ['M3 12h18M3 9v6M21 9v6M8 10v4M13 10v4M18 10v4'],
  line:      ['M5 19 19 5','M4 20a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0','M17 4a1.5 1.5 0 1 0 3 0 1.5 1.5 0 0 0-3 0'],
  surface:   ['M4 8l8-4 8 4-8 4z','M4 8v8l8 4 8-4V8'],
  pan:       ['M5 9l-3 3 3 3M9 5l3-3 3 3M15 19l-3 3-3-3M19 9l3 3-3 3M2 12h20M12 2v20'],
  angle:     ['M3 21h18','M3 21 21 8','M8 21a5 5 0 0 0-5-5'],
  cursor:    ['M3 3l7 18 2.5-7.5L20 11z'],
  upload:    ['M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4','M7 9l5-5 5 5','M12 4v12'],
  book:      ['M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z','M19 19H6a2 2 0 0 0-2 2','M9 7h6M9 11h6'],
  play:      ['M5 4v16l14-8z'],
  bulb:      ['M9 18h6','M10 21h4','M12 3a6 6 0 0 0-4 10.5c.7.7 1 1.2 1 2.5h6c0-1.3.3-1.8 1-2.5A6 6 0 0 0 12 3z'],
  calc:      ['M6 2h12a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z','M8 6h8v3H8z','M8 13h.01M12 13h.01M16 13h.01M8 17h.01M12 17h.01M16 17h.01'],
  grad:      ['M22 9 12 5 2 9l10 4 10-4z','M6 11v5c0 1 2.7 3 6 3s6-2 6-3v-5'],
  help:      ['M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z','M9.5 9a2.5 2.5 0 1 1 3.5 2.3c-.8.4-1 .9-1 1.7v.5','M12 17h.01'],
};

function Icon({ n, style, className }) {
  const paths = ICONS[n] || ICONS.box;
  return (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeLinecap="round"
         strokeLinejoin="round" style={style} className={className} aria-hidden="true">
      {paths.map((d, i) => <path key={i} d={d} />)}
    </svg>
  );
}

// ── Module registry (grouped for sidebar, ordered by workflow) ──
const MODULE_GROUPS = [
  { group:'Démarrage', items:[
    { id:'portfolio', name:'Portefeuille', cat:'Démarrage', icon:'folders', status:null, system:true },
    { id:'dashboard', name:'Tableau de bord', cat:'Démarrage', icon:'grid', status:'prog' },
    { id:'projet', name:'Projet', cat:'Démarrage', icon:'clipboard', status:'done', system:true },
    { id:'config', name:'Configuration', cat:'Démarrage', icon:'sliders', status:'done', system:true },
  ]},
  { group:'Structure', items:[
    { id:'structure', name:'Structure', cat:'Structure', icon:'frame', status:'none', q:'—', qu:'', mat:'Ossature porteuse' },
    { id:'bois', name:'Bois d’œuvre', cat:'Structure', icon:'plank', status:'none', q:'—', qu:'pmp', mat:'SPF / LVL' },
    { id:'sous-dalle', name:'Sous-dalle', cat:'Structure', icon:'layers', status:'done', q:'1 240', qu:'pi²', mat:'Polystyrène extrudé' },
    { id:'fondation', name:'Fondation', cat:'Structure', icon:'foundation', status:'prog', q:'168', qu:'pi lin.', mat:'Panneau semi-rigide' },
  ]},
  { group:'Enveloppe', items:[
    { id:'murs', name:'Murs hors sol', cat:'Enveloppe', icon:'wall', status:'prog', built:true, q:'2 845', qu:'pi²', mat:'Cellulose haute densité' },
    { id:'prefab', name:'Mur préfab GO·PASSIF', cat:'Enveloppe', icon:'box', status:'none', q:'—', qu:'panneaux', mat:'Panneaux préfabriqués', soon:true },
    { id:'toiture', name:'Toiture', cat:'Enveloppe', icon:'roof', status:'done', q:'2 209', qu:'pi²', mat:'Bardeaux + membrane' },
  ]},
  { group:'Isolation', items:[
    { id:'isolation', name:'Isolation soufflée', cat:'Isolation', icon:'insulation', status:'done', q:'214', qu:'sacs', mat:'PROFIB CELL' },
    { id:'matelas', name:'Isolation en matelas', cat:'Isolation', icon:'batt', status:'none', q:'—', qu:'paquets', mat:'Laine / matelas' },
    { id:'chanvre', name:'Béton de chanvre', cat:'Isolation', icon:'hemp', status:'none', q:'—', qu:'sacs', mat:'Chaux + chènevotte' },
    { id:'acoustique', name:'Acoustique int.', cat:'Isolation', icon:'sound', status:'none', q:'—', qu:'', mat:'Laine minérale' },
  ]},
  { group:'Fenestration', items:[
    { id:'fenestration', name:'Fenestration', cat:'Fenestration', icon:'window', status:'prog', built:true, q:'34', qu:'unités', mat:'Cadres bois' },
  ]},
  { group:'Finition', items:[
    { id:'plancher', name:'Plancher', cat:'Finition', icon:'floor', status:'none', q:'—', qu:'pi²', mat:'Bois franc / ingénierie' },
    { id:'revetement', name:'Revêtement extérieur', cat:'Finition', icon:'plank', status:'none', q:'—', qu:'pi²', mat:'Pin blanc / Maibec' },
  ]},
  { group:'Étanchéité', items:[
    { id:'membranes', name:'Membranes & rubans', cat:'Étanchéité', icon:'tape', status:'prog', q:'412', qu:'pi lin.', mat:'Pare-air + rubans' },
  ]},
  { group:'Mécanique', items:[
    { id:'ventilation', name:'Ventilation', cat:'Mécanique', icon:'fan', status:'none', q:'—', qu:'', mat:'VRC + conduits' },
    { id:'grille-ventilation', name:'Grille de ventilation', cat:'Mécanique', icon:'grille', status:'none', q:'—', qu:'grilles', mat:'Grilles & diffuseurs' },
    { id:'charge', name:'Charge chauffage & clim.', cat:'Mécanique', icon:'thermo', status:'none', q:'—', qu:'BTU/h', mat:'Bilan thermique' },
  ]},
  { group:'Livrable', items:[
    { id:'soumission', name:'Soumission', cat:'Livrable', icon:'receipt', status:'prog', system:true },
  ]},
  { group:'Réglages', items:[
    { id:'organisation', name:'Organisation des widgets', cat:'Réglages', icon:'folderPlus', status:null, system:true, admin:true, cap:'organisation' },
    { id:'builder', name:'Builder de widgets', cat:'Réglages', icon:'cube', status:null, system:true, admin:true, cap:'organisation' },
    { id:'courriels', name:'Courriels', cat:'Réglages', icon:'mail', status:null, system:true, admin:true, cap:'integrations', superOnly:true },
    { id:'systemes', name:'Systèmes de produits', cat:'Réglages', icon:'box', status:null, system:true, admin:true, cap:'recipes' },
    { id:'services', name:'Connexions & intégrations', cat:'Réglages', icon:'plug', status:null, system:true, admin:true, cap:'integrations' },
    { id:'recettes', name:'Recettes & ratios', cat:'Réglages', icon:'beaker', status:null, system:true, admin:true, cap:'recipes' },
    { id:'historique', name:'Journal d’activité', cat:'Réglages', icon:'history', status:null, system:true },
    { id:'equipe', name:'Utilisateurs & rôles', cat:'Réglages', icon:'users', status:null, system:true, cap:'team' },
  ]},
  { group:'Outils', items:[
    { id:'mesure', name:'Mesure sur plan', cat:'Outils', icon:'ruler', status:'none', tool:true },
  ]},
];
const ALL_MODULES = MODULE_GROUPS.flatMap(g => g.items);
const moduleById = (id) => ALL_MODULES.find(m => m.id === id);
// estimation modules only (excludes system, admin, tool, dashboard)
const ESTIMATION_MODULES = ALL_MODULES.filter(m => !m.tool && !m.system && m.id !== 'dashboard');

// ── Organisation des widgets : hiérarchie catégorie › sous-catégorie ──
// Hiérarchie extensible et pilotée par les admins. Les widgets (modules)
// d'estimation sont rangés sous une sous-catégorie d'une catégorie.
// `order` permet d'insérer de futurs niveaux sans refonte.
const WIDGET_CATEGORIES = MODULE_GROUPS
  .map(g => g.group)
  .filter(c => !['Démarrage','Livrable','Réglages','Outils'].includes(c));
// Sous-catégories par défaut (l'admin peut en créer/renommer/supprimer)
const SUBCATEGORIES = {
  Enveloppe: [
    { id:'sc-toiture', name:'Toiture' },
    { id:'sc-murs', name:'Murs' },
  ],
  Structure: [
    { id:'sc-fondations', name:'Fondations' },
  ],
};
// Affectation widget → sous-catégorie (les non affectés vont dans « Non classé »)
const WIDGET_SUBCAT = {
  toiture: 'sc-toiture',
  murs: 'sc-murs',
  prefab: 'sc-murs',
  fondation: 'sc-fondations',
  'sous-dalle': 'sc-fondations',
};
// État d'organisation par défaut (sérialisable, persisté + éditable par l'admin)
// cats : ordre des catégories de 1er niveau (base + créées par l'admin)
// widgetCat : déplacement d'un widget vers une autre catégorie (sinon module.cat)
// hidden : modules désactivés (masqués pour tous, gérables par l'admin)
const DEFAULT_ORG = { cats: [...WIDGET_CATEGORIES], widgetCat: {}, subcats: SUBCATEGORIES, assign: WIDGET_SUBCAT, hidden: {} };
const slugSubcat = (cat) => 'sc-' + Math.random().toString(36).slice(2, 8);

// Reconstruit les groupes de la barre latérale à partir de l'organisation admin :
// Démarrage (système) → catégories d'estimation dans l'ordre de org.cats
// (widgets rangés selon org.widgetCat) → Livrable / Réglages / Outils (système).
const SYS_GROUPS = ['Démarrage', 'Livrable', 'Réglages', 'Outils'];
const sidebarGroups = (org) => {
  const cats = (org && org.cats && org.cats.length) ? org.cats : WIDGET_CATEGORIES;
  const widgetCat = (org && org.widgetCat) || {};
  const catOf = (m) => widgetCat[m.id] || m.cat;
  const demarrage = MODULE_GROUPS.find(g => g.group === 'Démarrage');
  const after = MODULE_GROUPS.filter(g => ['Livrable', 'Réglages', 'Outils'].includes(g.group));
  const estGroups = cats.map(c => ({ group: c, items: ESTIMATION_MODULES.filter(m => catOf(m) === c) }));
  return [demarrage, ...estGroups, ...after].filter(Boolean);
};

// ── Projects (portefeuille) ────────────────────────────────────
// Le « projet » est un contexte orthogonal aux modules : changer de
// projet ne change pas la section courante.
const PROJECTS = [
  {
    id:'p1', no:'2026-042', nom:'Résidence Beaumont-Nord',
    clientPrenom:'Marc', clientNom:'Bélanger',
    adresse:'142 chemin du Lac', ville:'Saint-Hippolyte', province:'QC', cp:'J8A 2R3',
    tel:'(450) 555-0142', courriel:'m.belanger@example.com',
    ent:'Constructions Boréal', estimateur:'A. Tremblay',
    type:'Construction neuve', superficie:'2 240', etages:'2',
    date:'2026-06-09', cible:'Passive House / Novoclimat',
    statut:'En cours', avancement:62, montant:'248 600 $', modulesFaits:3,
  },
  {
    id:'p2', no:'2026-051', nom:'Chalet Mont-Tremblant',
    clientPrenom:'Sophie', clientNom:'Lavoie',
    adresse:'88 montée du Sommet', ville:'Mont-Tremblant', province:'QC', cp:'J8E 1T4',
    tel:'(819) 555-0188', courriel:'s.lavoie@example.com',
    ent:'Bâtiment Laurentides', estimateur:'A. Tremblay',
    type:'Construction neuve', superficie:'1 680', etages:'2',
    date:'2026-05-22', cible:'Passive House',
    statut:'Soumission', avancement:88, montant:'196 400 $', modulesFaits:7,
  },
  {
    id:'p3', no:'2026-058', nom:'Duplex Rosemont',
    clientPrenom:'Étienne', clientNom:'Côté',
    adresse:'3120 rue Masson', ville:'Montréal', province:'QC', cp:'H1Y 1X9',
    tel:'(514) 555-0211', courriel:'e.cote@example.com',
    ent:'Rénovation Urbaine', estimateur:'M. Gagnon',
    type:'Rénovation majeure', superficie:'2 950', etages:'2',
    date:'2026-06-15', cible:'Novoclimat',
    statut:'Relevé', avancement:24, montant:'—', modulesFaits:1,
  },
];
const projectById = (id) => PROJECTS.find(p => p.id === id) || PROJECTS[0];
// alias rétro-compatible (défaut = premier projet)
const PROJECT = PROJECTS[0];

// ── Utilisateurs & rôles ───────────────────────────────────────
const ROLES = {
  super_admin: { label:'Super-administrateur', cls:'admin', desc:'Compte fondateur — accès total, non supprimable, unique.' },
  store_manager: { label:'Gérant de magasin', cls:'admin', desc:'Gère les comptes et les accès ; voit les projets de tous les magasins.' },
  admin: { label:'Administrateur', cls:'admin', desc:'Accès complet — recettes, utilisateurs, export, journal.' },
  user:  { label:'Estimateur', cls:'user', desc:'Crée et modifie les relevés et soumissions selon ses modules autorisés.' },
};
const USERS = [
  { id:'u1', prenom:'Admin', nom:'Go Passif', initiales:'AG', role:'super_admin',
    courriel:'js@gopassif.com', titre:'Compte administrateur', actif:true, dernier:'2026-06-20 08:42' },
  { id:'u2', prenom:'Marc', nom:'Gagnon', initiales:'MG', role:'user',
    courriel:'m.gagnon@gopassif.ca', titre:'Estimateur', actif:true, dernier:'2026-06-19 16:10' },
  { id:'u3', prenom:'Sophie', nom:'Bélair', initiales:'SB', role:'user',
    courriel:'s.belair@gopassif.ca', titre:'Technicienne au relevé', actif:true, dernier:'2026-06-18 11:27' },
];
const userById = (id) => USERS.find(u => u.id === id);

// ── Permissions granulaires (capabilities) ─────────────────────
// Chaque module sensible exige une « capability ». Les rôles en
// accordent un lot par défaut ; on peut en accorder/retirer
// individuellement par utilisateur (overrides), sans changer le rôle.
const CAPABILITIES = [
  { id:'recipes',        label:'Recettes & ratios',     desc:'Voir et modifier les recettes et ratios.', icon:'beaker' },
  { id:'team',           label:'Utilisateurs & rôles',  desc:'Gérer les membres et leurs accès.', icon:'users' },
  { id:'auditExport',    label:'Exporter le journal',   desc:'Télécharger l’historique d’audit complet.', icon:'history' },
  { id:'submission',     label:'Générer les soumissions', desc:'Produire et exporter les bordereaux.', icon:'receipt' },
  { id:'config',         label:'Configuration globale', desc:'Modifier les hypothèses appliquées au projet.', icon:'sliders' },
  { id:'manageProjects', label:'Gérer les projets',     desc:'Créer, archiver et supprimer des projets.', icon:'folders' },
  { id:'organisation',   label:'Organisation des widgets', desc:'Créer et gérer les catégories et sous-catégories.', icon:'folderPlus' },
  { id:'integrations',   label:'Connexions & intégrations', desc:'Configurer Odoo, Cloud, Cloudflare, agents et clés d’API.', icon:'plug' },
];
// Capabilities accordées par chaque rôle (défaut)
const ROLE_CAPS = {
  admin: CAPABILITIES.map(c => c.id),     // tout
  // Gérant de magasin : gestion des comptes + estimation, PAS les réglages globaux
  // (ni recettes, ni builder, ni organisation, ni intégrations, ni courriels).
  store_manager: ['team', 'submission', 'config', 'manageProjects', 'auditExport'],
  user:  ['submission', 'config'],        // socle estimateur
};
// Overrides individuels (démo : Marc a reçu l'accès Recettes sans être admin)
const USER_PERMS = {
  u2: { recipes: true },
};
// Capability effective : rôle ∪ overrides (admin = tout)
function hasCap(user, cap, perms = {}) {
  if (!cap) return true;            // module non restreint
  if (!user) return false;
  if (user.role === 'admin' || user.role === 'super_admin') return true;
  if ((ROLE_CAPS[user.role] || []).includes(cap)) return true;
  return !!(perms[user.id] && perms[user.id][cap]);
}
// Capability vient-elle du rôle (héritée, non modifiable individuellement) ?
const capFromRole = (role, cap) => role === 'admin' || role === 'super_admin' || (ROLE_CAPS[role] || []).includes(cap);

// ── Journal d'activité (audit) ─────────────────────────────────
// type: submission | edit | measure | recipe | login | user
const ACTIVITY_LOG = [
  { id:'a1', userId:'u1', type:'submission', action:'a généré la soumission', cible:'Bordereau v3 — 14 lignes', projet:'2026-042', ts:'2026-06-20 09:15' },
  { id:'a2', userId:'u2', type:'measure', action:'a relevé une forme', cible:'Mur arrière (340 pi²)', projet:'2026-042', ts:'2026-06-20 08:58' },
  { id:'a3', userId:'u1', type:'login', action:'s’est connectée', cible:'', projet:'', ts:'2026-06-20 08:42' },
  { id:'a4', userId:'u3', type:'edit', action:'a modifié le relevé', cible:'Fenestration — F-03 ×2', projet:'2026-051', ts:'2026-06-19 16:40' },
  { id:'a5', userId:'u1', type:'recipe', action:'a modifié une recette', cible:'Étanchéité à l’air — Tescon Vana 3/rouleau', projet:'', ts:'2026-06-19 14:22' },
  { id:'a6', userId:'u2', type:'submission', action:'a exporté un PDF', cible:'Soumission — Chalet Mont-Tremblant', projet:'2026-051', ts:'2026-06-19 11:05' },
  { id:'a7', userId:'u3', type:'measure', action:'a relevé 4 formes', cible:'Toiture — pans + débords', projet:'2026-058', ts:'2026-06-18 15:33' },
  { id:'a8', userId:'u1', type:'user', action:'a ajouté un utilisateur', cible:'Sophie Bélair (Estimateur)', projet:'', ts:'2026-06-18 09:12' },
  { id:'a9', userId:'u2', type:'edit', action:'a ajusté le surplus', cible:'Murs hors sol — 8 %', projet:'2026-042', ts:'2026-06-17 13:48' },
];

// ── Global configuration (Configuration module) ────────────────
const CONFIG = {
  ossature:'Double ossature 2×4 décalée',
  epaisseurMur:12,            // po
  hauteurMur:9,               // pi
  isolantMur:'Cellulose haute densité',
  rMur:'R-40', rToit:'R-60', rSousDalle:'R-20',
  fondation:'Béton coulé 8″',
  membraneInt:'Pro Clima Intello Plus',
  pareIntemperies:'Pro Clima Solitex Mento 1000',
  ventilation:'VRC haute efficacité',
  cible:'Passive House',
};
const OSSATURE_OPTS = ['Ossature simple 2×6 @ 16″','Ossature simple 2×8 @ 24″','Double ossature 2×4 décalée','Mur préfab GO·PASSIF','Béton de chanvre + ossature'];
const CIBLE_OPTS = ['Code de base','Novoclimat','LEED','Passive House','Passive House Plus'];

const SECTIONS = ['Rez-de-chaussée','Étage','Garage'];

// ── Mock takeoff data: Murs hors sol ───────────────────────────
const MURS_ROWS = [
  { id:'m1', ident:'Mur avant',   sec:'Rez-de-chaussée', qte:1, l:[42,6], h:[8,0], plan:true },
  { id:'m2', ident:'Mur arrière', sec:'Rez-de-chaussée', qte:1, l:[42,6], h:[8,0], plan:true },
  { id:'m3', ident:'Pignon ouest',sec:'Rez-de-chaussée', qte:1, l:[28,4], h:[9,0], plan:true },
  { id:'m4', ident:'Pignon est',  sec:'Rez-de-chaussée', qte:1, l:[28,4], h:[9,0], plan:false },
  { id:'m5', ident:'Mur avant',   sec:'Étage', qte:1, l:[42,6], h:[8,0], plan:true },
  { id:'m6', ident:'Mur arrière', sec:'Étage', qte:1, l:[42,6], h:[8,0], plan:true },
];
const MURS_OPEN = [
  { id:'o1', ident:'Porte avant', sec:'Rez-de-chaussée', qte:1, w:[3,0], h:[6,8] },
  { id:'o2', ident:'Fenêtres salon', sec:'Rez-de-chaussée', qte:3, w:[4,0], h:[5,0] },
  { id:'o3', ident:'Fenêtres ch.', sec:'Étage', qte:4, w:[2,8], h:[4,0] },
];

// ── Mock takeoff data: Fenestration (bordereau) ────────────────
const FEN_ROWS = [
  { id:'f1', code:'F-01', loc:'Salon',     qte:2, w:[2,4], h:[5,11], type:'Battant', frame:'Bois' },
  { id:'f2', code:'F-02', loc:'Cuisine',   qte:1, w:[3,3], h:[5,11], type:'Fixe',    frame:'Bois' },
  { id:'f3', code:'F-03', loc:'Ch. maître',qte:2, w:[3,10],h:[4,10], type:'Battant + fixe', frame:'Bois' },
  { id:'f4', code:'F-04', loc:'Bureau',    qte:1, w:[2,0], h:[7,0],  type:'Fixe',    frame:'Bois' },
  { id:'f5', code:'F-05', loc:'Ch. 2',     qte:2, w:[3,10],h:[3,6],  type:'Battant + fixe', frame:'Bois' },
  { id:'f6', code:'F-06', loc:'Salle de bain', qte:2, w:[2,0], h:[3,10], type:'Battant', frame:'Bois' },
  { id:'f7', code:'P-01', loc:'Entrée',    qte:1, w:[3,0], h:[6,8],  type:'Porte pleine', frame:'Bois' },
  { id:'f8', code:'P-02', loc:'Patio',     qte:1, w:[5,11],h:[6,8],  type:'Coulissante', frame:'Bois' },
];

// ── Persistent plan measurements (shared across modules) ───────
// Each shape relevée dans l'outil persiste et alimente un module cible.
const PLAN_MEASURES = [
  { id:'me1', name:'Toiture — pan sud', kind:'surface', val:547.4, unit:'pi²', target:'toiture', page:6 },
  { id:'me2', name:'Toiture — pan nord', kind:'surface', val:547.4, unit:'pi²', target:'toiture', page:6 },
  { id:'me3', name:'Mur avant', kind:'surface', val:340.0, unit:'pi²', target:'murs', page:2 },
  { id:'me4', name:'Mur arrière', kind:'surface', val:340.0, unit:'pi²', target:'murs', page:2 },
  { id:'me5', name:'Périmètre fondation', kind:'ligne', val:141.6, unit:'pi lin.', target:'fondation', page:1 },
  { id:'me6', name:'Dalle sous-sol', kind:'surface', val:1240, unit:'pi²', target:'sous-dalle', page:1 },
  { id:'me7', name:'Surface fondation', kind:'surface', val:1180, unit:'pi²', target:'fondation', page:1 },
  { id:'me8', name:'Enveloppe étanche', kind:'surface', val:4054, unit:'pi²', target:'membranes', page:3 },
  { id:'me9', name:'Périmètre étanchéité', kind:'ligne', val:412, unit:'pi lin.', target:'membranes', page:3 },
  { id:'me10', name:'Combles soufflés', kind:'surface', val:1095, unit:'pi²', target:'isolation', page:6 },
  { id:'me11', name:'Murs en matelas', kind:'surface', val:1850, unit:'pi²', target:'matelas', page:2 },
  { id:'me12', name:'Mur de chanvre', kind:'surface', val:920, unit:'pi²', target:'chanvre', page:2 },
];

// ── Recipes / ratios (admin) ───────────────────────────────────
// driver: 'area' (pi²) | 'perim' (pi lin.) | 'prev' (ligne précédente)
const RECIPES = [
  { id:'air-intello', module:'membranes', name:'Étanchéité à l’air — intérieur', brand:'Pro Clima',
    brands:['Pro Clima','SIGA'], driver:'Surface intérieure + périmètre',
    lines:[
      { item:'Membrane Intello Plus', sku:'1IN1.5', driver:'area', per:1, base:1614, unit:'rouleau', note:'1 rouleau / 1 614 pi²' },
      { item:'Ruban Tescon Vana 60 mm', sku:'TV60', driver:'prev', per:3, base:1, unit:'rouleau', note:'3 rouleaux / rouleau Intello' },
      { item:'Contega HF (raccords)', sku:'CHF', driver:'perim', per:1, base:30, unit:'cartouche', note:'1 / 30 pi lin. de mur' },
      { item:'Orcon F (colle joints)', sku:'ORF', driver:'perim', per:1, base:48, unit:'cartouche', note:'1 / 48 pi lin.' },
    ]},
  { id:'pare-mento', module:'membranes', name:'Pare-intempéries extérieur', brand:'Pro Clima',
    brands:['Pro Clima','VaproShield'], driver:'Surface des murs extérieurs',
    cat:'Pare-intempéries extérieur', variant:'v-mento1000',
    variants:[
      { id:'v-mento1000', name:'Protection 3 mois', sub:'Solitex Mento 1000 · non auto-adhésif',
        params:{ 'UV':'3 mois', 'Perméance':'38 perms', 'Pose':'Agrafage + ruban' },
        lines:[
          { item:'Solitex Mento 1000', sku:'MEN1000', driver:'area', per:1, base:807, unit:'rouleau', note:'1 / 807 pi²' },
          { item:'Tescon Naideck (bas de mur)', sku:'TND', driver:'perim', per:1, base:50, unit:'rouleau', note:'1 / 50 pi lin.' },
          { item:'Tescon Vana (joints)', sku:'TV60', driver:'area', per:1, base:430, unit:'rouleau', note:'1 / 430 pi²' },
        ]},
      { id:'v-mento3000', name:'Protection 6 mois', sub:'Solitex Mento 3000 · haute exposition',
        params:{ 'UV':'6 mois', 'Perméance':'48 perms', 'Pose':'Agrafage + ruban' },
        lines:[
          { item:'Solitex Mento 3000', sku:'MEN3000', driver:'area', per:1, base:807, unit:'rouleau', note:'1 / 807 pi²' },
          { item:'Tescon Naideck (bas de mur)', sku:'TND', driver:'perim', per:1, base:50, unit:'rouleau', note:'1 / 50 pi lin.' },
          { item:'Tescon Vana (joints)', sku:'TV60', driver:'area', per:1, base:430, unit:'rouleau', note:'1 / 430 pi²' },
        ]},
      { id:'v-adhero', name:'Auto-adhésif', sub:'Solitex Adhero · pose collée',
        params:{ 'UV':'6 mois', 'Perméance':'40 perms', 'Pose':'Auto-adhésif' },
        lines:[
          { item:'Solitex Adhero 3000', sku:'ADH3000', driver:'area', per:1, base:430, unit:'rouleau', note:'1 / 430 pi²' },
          { item:'Primer Tescon (apprêt)', sku:'PRM', driver:'area', per:1, base:600, unit:'cartouche', note:'1 / 600 pi²' },
          { item:'Rouleau de marouflage', sku:'ROL', driver:'prev', per:1, base:1, unit:'mcx', note:'1 / chantier' },
        ]},
      { id:'v-mento-mech', name:'Non auto-adhésif', sub:'Solitex Mento 1000 · fixation mécanique',
        params:{ 'UV':'3 mois', 'Perméance':'38 perms', 'Pose':'Fixation mécanique' },
        lines:[
          { item:'Solitex Mento 1000', sku:'MEN1000', driver:'area', per:1, base:807, unit:'rouleau', note:'1 / 807 pi²' },
          { item:'Contre-lattes 1×4', sku:'CL14', driver:'area', per:1, base:16, unit:'mcx', note:'1 / 16 pi²' },
          { item:'Vis à platelage', sku:'VIS', driver:'area', per:1, base:8, unit:'boîte', note:'1 / 8 pi²' },
        ]},
    ],
    lines:[
      { item:'Solitex Mento 1000', sku:'MEN1000', driver:'area', per:1, base:807, unit:'rouleau', note:'1 / 807 pi²' },
      { item:'Tescon Naideck (bas de mur)', sku:'TND', driver:'perim', per:1, base:50, unit:'rouleau', note:'1 / 50 pi lin.' },
      { item:'Tescon Vana (joints)', sku:'TV60', driver:'area', per:1, base:430, unit:'rouleau', note:'1 / 430 pi²' },
    ]},
  { id:'murs-cellulose', module:'murs', name:'Mur cellulose haute densité', brand:'PROFIB CELL',
    brands:['PROFIB CELL','Greenfiber'], driver:'Surface nette des murs',
    lines:[
      { item:'Cellulose haute densité (4 lb/pi³)', sku:'PFC-HD', driver:'area', per:1, base:32, unit:'sac', note:'1 sac / 32 pi² @ 5,5″' },
      { item:'Membrane de retenue', sku:'MR', driver:'area', per:1, base:161, unit:'rouleau', note:'1 / 161 pi²' },
      { item:'Fourrures 1×3 épinette', sku:'F13', driver:'area', per:1, base:16, unit:'mcx', note:'1 / 16 pi²' },
    ]},
  { id:'toit-cellulose', module:'toiture', name:'Toiture cellulose soufflée', brand:'PROFIB CELL',
    brands:['PROFIB CELL','Greenfiber'], driver:'Surface de toiture (projetée)',
    lines:[
      { item:'Cellulose soufflée (1,6 lb/pi³)', sku:'PFC-SD', driver:'area', per:1, base:42, unit:'sac', note:'1 sac / 42 pi² @ R-60' },
      { item:'Déflecteurs de ventilation', sku:'DEF', driver:'area', per:1, base:24, unit:'mcx', note:'1 / 24 pi²' },
    ]},
  { id:'fond-semirigide', module:'fondation', name:'Isolation fondation', brand:'Roxul',
    brands:['Roxul','Rockwool'], driver:'Périmètre × hauteur de fondation',
    lines:[
      { item:'Panneau semi-rigide R-14', sku:'CB80', driver:'area', per:1, base:48, unit:'paquet', note:'1 paquet / 48 pi²' },
      { item:'Membrane de drainage', sku:'DELTA', driver:'perim', per:1, base:65, unit:'rouleau', note:'1 / 65 pi lin.' },
    ]},
  { id:'matelas-laine', module:'matelas', name:'Isolation en matelas — laine', brand:'Roxul',
    brands:['Roxul','Owens Corning','Knauf'], driver:'Surface nette des murs/plafonds',
    lines:[
      { item:'Matelas ComfortBatt R-24', sku:'CB-R24', driver:'area', per:1, base:59.7, unit:'paquet', note:'1 paquet / 59,7 pi²' },
      { item:'Supports à isolant', sku:'SUP', driver:'area', per:1, base:32, unit:'paquet', note:'1 / 32 pi²' },
    ]},
  { id:'chanvre-banche', module:'chanvre', name:'Béton de chanvre banché', brand:'IsoHemp',
    brands:['IsoHemp','Nature Fibres'], driver:'Volume (surface × épaisseur)',
    lines:[
      { item:'Chènevotte (sac 200 L)', sku:'CHE', driver:'area', per:1, base:18, unit:'sac', note:'1 sac / 18 pi² @ 12″' },
      { item:'Liant chaux (sac 25 kg)', sku:'LIA', driver:'area', per:1, base:24, unit:'sac', note:'1 sac / 24 pi²' },
    ]},
  { id:'sd-xps', module:'sous-dalle', name:'Sous-dalle — panneau rigide', brand:'Soprema',
    brands:['Soprema','Owens Corning','Kingspan'], driver:'Surface de dalle',
    lines:[
      { item:'Panneau XPS R-5 (4×8)', sku:'XPS-R5', driver:'area', per:1, base:32, unit:'panneau', note:'1 panneau / 32 pi²' },
      { item:'Pare-vapeur 10 mil', sku:'PV10', driver:'area', per:1, base:198, unit:'rouleau', note:'1 / 198 pi²' },
      { item:'Ruban de scellement', sku:'RS', driver:'area', per:1, base:200, unit:'rouleau', note:'1 / 200 pi²' },
    ]},
  { id:'grille-std', module:'grille-ventilation', name:'Grilles & diffuseurs', brand:'Continental',
    brands:['Continental','Price','Titus','Imperial'],  driver:'Nombre de bouches (count)',
    lines:[
      { item:'Diffuseur / grille', sku:'GR-DIF', driver:'units', per:1, base:1, unit:'grille', note:'1 / bouche' },
      { item:'Collet de raccord', sku:'GR-COL', driver:'units', per:1, base:1, unit:'mcx', note:'1 / grille' },
      { item:'Conduit flexible isolé (pi)', sku:'GR-FLEX', driver:'units', per:8, base:1, unit:'pi', note:'≈ 8 pi / grille' },
      { item:'Ruban d’aluminium', sku:'GR-TAPE', driver:'units', per:1, base:4, unit:'rouleau', note:'1 / 4 grilles' },
    ]},
];
const recipesForModule = (id) => RECIPES.filter(r => r.module === id);

// ── Systèmes de produits prédéfinis (fabricant / approche complète) ──
// Un « système » applique d'un coup une recette + marque aux modules
// concernés. L'utilisateur part d'un système, puis raffine dans Recettes.
const PRODUCT_SYSTEMS = [
  { id:'proclima', name:'Pro Clima', mono:'PC', accent:'#1f8a5b',
    tagline:'Étanchéité à l’air & pare-intempéries',
    picks:{ membranes:{ recipe:'air-intello', brand:'Pro Clima' } } },
  { id:'siga', name:'SIGA', mono:'SG', accent:'#d24b3e',
    tagline:'Membranes & rubans intérieurs',
    picks:{ membranes:{ recipe:'air-intello', brand:'SIGA' } } },
  { id:'soprema', name:'SOPREMA', mono:'SO', accent:'#1455a8',
    tagline:'Isolation rigide & sous-dalle',
    picks:{ 'sous-dalle':{ recipe:'sd-xps', brand:'Soprema' } } },
  { id:'rockwool', name:'Rockwool', mono:'RW', accent:'#cf5b2c',
    tagline:'Laine minérale — murs & fondation',
    picks:{ matelas:{ recipe:'matelas-laine', brand:'Roxul' }, fondation:{ recipe:'fond-semirigide', brand:'Roxul' } } },
  { id:'profibcell', name:'PROFIB CELL', mono:'PF', accent:'#9a6a2c',
    tagline:'Cellulose soufflée — murs & toiture',
    picks:{ murs:{ recipe:'murs-cellulose', brand:'PROFIB CELL' }, toiture:{ recipe:'toit-cellulose', brand:'PROFIB CELL' } } },
  { id:'isohemp', name:'IsoHemp', mono:'IH', accent:'#6a8a3a',
    tagline:'Béton de chanvre banché',
    picks:{ chanvre:{ recipe:'chanvre-banche', brand:'IsoHemp' } } },
];
// systèmes couvrant un module donné (depuis une liste fournie, sinon défaut)
const systemsForModule = (id, list) => (list || PRODUCT_SYSTEMS).filter(s => s.picks[id]);
const systemById = (id) => PRODUCT_SYSTEMS.find(s => s.id === id) || null;
// Quel système de produits implémente une recette donnée (relation inverse)
const systemForRecipe = (recipeId, moduleId) =>
  PRODUCT_SYSTEMS.find(s => s.picks[moduleId] && s.picks[moduleId].recipe === recipeId) || null;
// Marques disponibles pour un module = union des marques de ses recettes + systèmes
const brandsForModule = (id) => {
  const set = new Set();
  recipesForModule(id).forEach(r => (r.brands || [r.brand]).forEach(b => set.add(b)));
  systemsForModule(id).forEach(s => set.add(s.picks[id].brand));
  return [...set];
};
// choix initiaux de recette par module (1ère recette dispo) → store partagé
const DEFAULT_RECIPE_CHOICES = RECIPES.reduce((acc, r) => {
  if (!acc[r.module]) acc[r.module] = { recipe: r.id, brand: r.brand };
  return acc;
}, {});

// ── Helpers ────────────────────────────────────────────────────
const toIn = (pair) => (Number(pair[0])||0)*12 + (Number(pair[1])||0);
const toFt = (pair) => toIn(pair)/12;
const fmt = (n, d=0) => Number(n).toLocaleString('fr-CA',{minimumFractionDigits:d,maximumFractionDigits:d});
const ftin = (pair) => `${pair[0]}′ ${pair[1]}″`;

function mursCalc(rows, opens){
  let surfBrute=0; rows.forEach(r=>{ surfBrute += (Number(r.qte)||0)*toFt(r.l)*toFt(r.h); });
  let surfOpen=0; opens.forEach(o=>{ surfOpen += (Number(o.qte)||0)*toFt(o.w)*toFt(o.h); });
  const net = surfBrute - surfOpen;
  const bySec = {};
  rows.forEach(r=>{ bySec[r.sec]=(bySec[r.sec]||0)+(Number(r.qte)||0)*toFt(r.l)*toFt(r.h); });
  return { surfBrute, surfOpen, net, bySec };
}
function fenCalc(rows){
  let units=0, glaze=0, perim=0; const byType={};
  rows.forEach(r=>{ const q=Number(r.qte)||0; units+=q;
    const a=toFt(r.w)*toFt(r.h); glaze+=q*a; perim+=q*2*(toFt(r.w)+toFt(r.h));
    byType[r.type]=(byType[r.type]||0)+q; });
  return { units, glaze, perim, byType };
}

// ── Canonical calc path — used by modules, dashboard AND soumission ──
// Derive {area, perim, units} from plan measures + manual rows.
function computeDrivers(measures = [], rows = [], rowMode = 'surface') {
  let area = 0, perim = 0, units = 0;
  measures.forEach(m => {
    if (m.kind === 'surface') area += m.val;
    else if (m.kind === 'ligne') perim += m.val;
    else if (m.kind === 'ouvert') units += m.val;
  });
  rows.forEach(r => {
    const q = Number(r.qte) || 0;
    if (rowMode === 'surface') area += q * (Number(r.l) || 0) * (Number(r.h) || 0);
    else if (rowMode === 'linear') perim += q * (Number(r.l) || 0);
    else if (rowMode === 'count') units += q;
  });
  return { area, perim, units };
}

// Driving quantities per estimation module, derived from the SAME measures
// the modules use — guarantees the soumission matches every module screen.
function moduleDrivers(measures = PLAN_MEASURES){
  const out = {};
  ESTIMATION_MODULES.forEach(mod => {
    if (mod.id === 'murs') {
      const m = mursCalc(MURS_ROWS, MURS_OPEN);
      out.murs = { area: m.net, perim: 142, label:`${fmt(m.net)} pi² nets` };
    } else if (mod.id === 'fenestration') {
      const f = fenCalc(FEN_ROWS);
      out.fenestration = { area: f.glaze, perim: f.perim, units: f.units, label:`${f.units} unités` };
    } else {
      const linked = measures.filter(x => x.target === mod.id);
      if (!linked.length) return;
      const d = computeDrivers(linked);
      const parts = [];
      if (d.area)  parts.push(`${fmt(d.area)} pi²`);
      if (d.perim) parts.push(`${fmt(d.perim,1)} pi lin.`);
      out[mod.id] = { ...d, label: parts.join(' · ') || '—' };
    }
  });
  return out;
}

// Compute a bill of materials from a recipe + drivers {area, perim}
function bomForRecipe(recipe, drivers){
  let prev = 0;
  return recipe.lines.map(l => {
    const src = l.driver === 'area' ? (drivers.area||0)
              : l.driver === 'perim' ? (drivers.perim||0)
              : l.driver === 'units' ? (drivers.units||0)
              : prev;
    const qty = Math.ceil(src / (l.base||1) * (l.per||1));
    prev = qty;
    return { item:l.item, sku:l.sku, qty, unit:l.unit, note:l.note };
  });
}

// ── Odoo (intégration CRM externe — démo) ──────────────────────
// Carnet res.partner simulé : recherche + import + détection de doublons.
const ODOO_CONNECTION = { server:'go-passif.odoo.com', db:'gopassif_prod', model:'res.partner' };
const ODOO_CONTACTS = [
  { id:8821, name:'Marc Bélanger', company:'—', email:'m.belanger@example.com', phone:'(450) 555-0142', addr:'142 chemin du Lac', city:'Saint-Hippolyte', province:'QC', cp:'J8A 2R3' },
  { id:9043, name:'Sophie Lavoie', company:'Lavoie Immobilier', email:'s.lavoie@example.com', phone:'(819) 555-0188', addr:'88 montée du Sommet', city:'Mont-Tremblant', province:'QC', cp:'J8E 1T4' },
  { id:9210, name:'Julie Bernard', company:'Bernard & Fils inc.', email:'j.bernard@example.com', phone:'(514) 555-0301', addr:'77 boul. des Mille-Îles', city:'Laval', province:'QC', cp:'H7A 1B2' },
  { id:9233, name:'Patrick Gagnon', company:'Gagnon Construction', email:'p.gagnon@example.com', phone:'(418) 555-0455', addr:'12 rue Saint-Jean', city:'Québec', province:'QC', cp:'G1R 1N5' },
  { id:9301, name:'Caroline Dubé', company:'—', email:'c.dube@example.com', phone:'(450) 555-0512', addr:'5 rue des Érables', city:'Blainville', province:'QC', cp:'J7C 3K9' },
];

Object.assign(window, {
  Icon, ICONS, MODULE_GROUPS, ALL_MODULES, ESTIMATION_MODULES, moduleById,
  WIDGET_CATEGORIES, SUBCATEGORIES, WIDGET_SUBCAT, DEFAULT_ORG, slugSubcat, sidebarGroups,
  PROJECT, PROJECTS, projectById, CONFIG, OSSATURE_OPTS, CIBLE_OPTS, SECTIONS,
  ROLES, USERS, userById, ACTIVITY_LOG,
  CAPABILITIES, ROLE_CAPS, USER_PERMS, hasCap, capFromRole,
  MURS_ROWS, MURS_OPEN, FEN_ROWS, PLAN_MEASURES, RECIPES, recipesForModule,
  PRODUCT_SYSTEMS, systemsForModule, systemById, systemForRecipe, brandsForModule, DEFAULT_RECIPE_CHOICES,
  ODOO_CONTACTS, ODOO_CONNECTION,
  toIn, toFt, fmt, ftin, mursCalc, fenCalc, computeDrivers, moduleDrivers, bomForRecipe,
});
