// gp-system.jsx — pivot modules: Projet, Configuration, Recettes, Soumission
// ───────────────────────────────────────────────────────────────
const {
  Icon: YIcon, moduleById: yModById, ESTIMATION_MODULES: EST_MODS,
  PROJECT: PROJ0, CONFIG: CFG0, OSSATURE_OPTS, CIBLE_OPTS,
  RECIPES: RCP, recipesForModule: rcpFor, moduleDrivers, bomForRecipe, fmt: yFmt,
  recipeIssues: yRecipeIssues, ODOO_CONTACTS: ODOO, ODOO_CONNECTION: ODOO_CONN,
  WIDGET_CATEGORIES: ORG_CATS, MODULE_GROUPS: ORG_GROUPS, slugSubcat: ORG_SLUG, DEFAULT_ORG: DEF_ORG,
  PRODUCT_SYSTEMS: PSYS, systemForRecipe: ySysForRecipe, systemById: ySysById, systemsForModule: ySysForMod
} = window;

// shared system header (no status pill / step nav)
function SysHeader({ icon, title, sub, children }) {
  return (
    <div className="mhead">
      <div className="mhead-ic"><YIcon n={icon} /></div>
      <div className="mhead-tx">
        <div className="mhead-tt">{title}</div>
        <div className="mhead-sub">{sub}</div>
      </div>
      <div className="mhead-side">{children}</div>
    </div>);

}

function Field({ label, value, onChange, span, type = 'text', opts }) {
  return (
    <div className={'fcell' + (span ? ' full' : '')}>
      <label className="lbl">{label}</label>
      {opts ?
      <select className="field" value={value} onChange={(e) => onChange && onChange(e.target.value)}>
            {opts.map((o) => <option key={o}>{o}</option>)}
          </select> :
      <input className={'field' + (type === 'mono' ? ' mono' : '')} value={value}
      onChange={(e) => onChange && onChange(e.target.value)} />}
    </div>);

}

// ── Odoo : synchronisation client (démo d'intégration CRM) ─────
const ODOO_SYNC = {
  synced: { cls: 'ok', icon: 'check', label: 'Synchronisé' },
  local: { cls: 'warn', icon: 'cloud', label: 'Local — à synchroniser' },
  error: { cls: 'bad', icon: 'alert', label: 'Erreur de connexion' }
};
function OdooStatusChip({ sync, onOpen }) {
  const s = ODOO_SYNC[sync] || ODOO_SYNC.local;
  return (
    <div className="odoo-chip-wrap">
      <span className={'odoo-chip ' + s.cls}><YIcon n={s.icon} style={{ width: 12, height: 12 }} /> Odoo · {s.label}</span>
      <button className="btn" onClick={onOpen}><YIcon n="cloud" /> Rechercher dans Odoo</button>
    </div>);
}
function OdooSyncBar({ sync, odooId, lastSync, onPush, onSearch }) {
  return (
    <div className="odoo-bar">
      <div className="odoo-bar-l">
        <span className={'odoo-dot ' + (sync === 'synced' ? 'ok' : sync === 'error' ? 'bad' : 'warn')} />
        {sync === 'synced' && <span>Lié à Odoo <b>#{odooId || '—'}</b> · synchronisé {lastSync}</span>}
        {sync === 'local' && <span>Modifications locales non synchronisées</span>}
        {sync === 'error' && <span>Connexion à Odoo impossible</span>}
      </div>
      <div className="odoo-bar-r">
        {sync !== 'synced' ?
        <button className="btn accent" onClick={onPush}><YIcon n="sync" /> {odooId ? 'Resynchroniser' : 'Créer dans Odoo'}</button> :
        <button className="btn" onClick={onSearch}><YIcon n="sync" /> Lier un autre contact</button>}
      </div>
    </div>);
}
function OdooSearchModal({ client, onClose, onImport }) {
  const [conn, setConn] = React.useState('online');
  const [query, setQuery] = React.useState((client.clientNom || '').trim());
  const norm = (s) => (s || '').toLowerCase().trim();
  const q = norm(query);
  const results = conn === 'online' && q ?
  ODOO.filter((c) => norm(c.name + ' ' + c.company + ' ' + c.email).includes(q)) : [];
  return (
    <div className="odoo-backdrop" onClick={onClose}>
      <div className="odoo-modal" onClick={(e) => e.stopPropagation()}>
        <div className="odoo-head">
          <div className="odoo-head-l"><span className="odoo-logo">odoo</span>
            <div className="odoo-head-tx"><b>Synchronisation client</b><span>{ODOO_CONN.model} · {ODOO_CONN.db}</span></div></div>
          <button className="mz-ai-x" onClick={onClose}>×</button>
        </div>

        <div className="odoo-conn">
          <span className={'odoo-conn-dot ' + (conn === 'online' ? 'ok' : 'bad')} />
          {conn === 'online' ?
          <span>Connecté à <b>{ODOO_CONN.server}</b></span> :
          <span className="odoo-conn-off">Hors ligne — {ODOO_CONN.server} injoignable</span>}
          <button className="odoo-conn-toggle" onClick={() => setConn((c) => c === 'online' ? 'offline' : 'online')}>
            {conn === 'online' ? 'Simuler une panne' : 'Reconnecter'}
          </button>
        </div>

        {conn === 'offline' &&
        <div className="odoo-err"><YIcon n="alert" style={{ width: 15, height: 15, flexShrink: 0 }} />
          <span>Échec de connexion à l’API Odoo. Vérifiez le serveur, puis réessayez.</span>
          <button onClick={() => setConn('online')}>Réessayer</button>
        </div>}

        <div className="odoo-search">
          <YIcon n="search" style={{ width: 15, height: 15, flexShrink: 0 }} />
          <input autoFocus value={query} onChange={(e) => setQuery(e.target.value)}
            placeholder="Rechercher un client (nom, courriel, entreprise)…" disabled={conn === 'offline'} />
        </div>

        <div className="odoo-results">
          {conn === 'online' && !q &&
          <div className="odoo-empty"><div className="odoo-empty-s">Tapez un nom pour interroger le carnet Odoo.</div></div>}
          {conn === 'online' && q && results.length === 0 &&
          <div className="odoo-empty">
            <div className="odoo-empty-t">Aucun contact Odoo pour « {query} »</div>
            <div className="odoo-empty-s">Ce client n’existe pas encore dans Odoo. Fermez et utilisez <b>« Créer dans Odoo »</b> pour y pousser la fiche locale.</div>
          </div>}
          {results.map((c) => {
            const linked = c.id === client.odooId;
            const dup = !linked && norm(c.email) === norm(client.courriel) && !!q;
            return (
              <div className="odoo-res" key={c.id}>
                <div className="odoo-res-tx">
                  <div className="odoo-res-name">{c.name}{c.company !== '—' && <span className="odoo-res-co"> · {c.company}</span>}</div>
                  <div className="odoo-res-meta">{c.email} · {c.phone} · {c.city}</div>
                  <div className="odoo-res-tags">
                    <span className="odoo-id">Odoo #{c.id}</span>
                    {linked && <span className="odoo-tag linked"><YIcon n="check" style={{ width: 10, height: 10 }} /> Déjà lié</span>}
                    {dup && <span className="odoo-tag dup"><YIcon n="alert" style={{ width: 10, height: 10 }} /> Doublon possible</span>}
                  </div>
                </div>
                <button className="btn accent" disabled={linked} onClick={() => onImport(c)}>{linked ? 'Lié' : dup ? 'Fusionner' : 'Importer'}</button>
              </div>);
          })}
        </div>

        <div className="odoo-foot">Démo d’intégration — la version connectée interrogera l’API Odoo (JSON-RPC, modèle <b>res.partner</b>) et dédoublonnera par courriel.</div>
      </div>
    </div>);
}

// ═══════════════════════ PROJET ═══════════════════════
function ProjetModule({ project, onLog }) {
  const [p, setP] = React.useState(project || PROJ0);
  const [sync, setSync] = React.useState('synced');     // 'synced' | 'local' | 'error'
  const [odooOpen, setOdooOpen] = React.useState(false);
  const [lastSync, setLastSync] = React.useState('aujourd’hui 09:14');
  React.useEffect(() => {if (project) {setP(project);setSync('synced');}}, [project && project.id]);
  // édition d'un champ client → marque la fiche à resynchroniser
  const setC = (k) => (v) => {setP((s) => ({ ...s, [k]: v }));setSync((st) => st === 'error' ? st : 'local');};
  const set = (k) => (v) => setP((s) => ({ ...s, [k]: v }));
  const importContact = (c) => {
    const [prenom, ...rest] = c.name.split(' ');
    setP((s) => ({ ...s, clientPrenom: prenom, clientNom: rest.join(' '), courriel: c.email,
      tel: c.phone, adresse: c.addr, ville: c.city, province: c.province, cp: c.cp,
      ent: c.company && c.company !== '—' ? c.company : s.ent, odooId: c.id }));
    setSync('synced'); setLastSync('à l’instant'); setOdooOpen(false);
  };
  const pushToOdoo = () => {setSync('synced'); setLastSync('à l’instant'); setP((s) => ({ ...s, odooId: s.odooId || Math.floor(9400 + Math.random() * 99) }));};
  return (
    <div className="fade-in">
      <SysHeader icon="clipboard" title="Projet"
      sub="Informations du client et du projet — réutilisées sur la soumission et tous les modules.">
        <button className="btn"><YIcon n="copy" /> Dupliquer projet</button>
        <button className="btn accent" onClick={() => onLog && onLog('edit', 'a enregistré la fiche projet', `${p.clientPrenom} ${p.clientNom}`, p.no || '')}><YIcon n="check" /> Enregistrer</button>
      </SysHeader>

      <div className="grid-2">
        <div className="col">
          <div className="card">
            <div className="card-h">
              <div className="card-h-l"><span className="badge-n">1</span><span className="card-t">Client</span></div>
              <OdooStatusChip sync={sync} onOpen={() => setOdooOpen(true)} />
            </div>
            <div className="fgrid">
              <Field label="Prénom" value={p.clientPrenom} onChange={setC('clientPrenom')} />
              <Field label="Nom" value={p.clientNom} onChange={setC('clientNom')} />
              <Field label="Téléphone" value={p.tel} onChange={setC('tel')} type="mono" />
              <Field label="Courriel" value={p.courriel} onChange={setC('courriel')} />
              <Field label="Adresse du projet" value={p.adresse} onChange={setC('adresse')} span />
              <Field label="Ville" value={p.ville} onChange={setC('ville')} />
              <Field label="Province" value={p.province} onChange={setC('province')} opts={['QC', 'ON', 'NB', 'NÉ']} />
              <Field label="Code postal" value={p.cp} onChange={setC('cp')} type="mono" />
            </div>
            <OdooSyncBar sync={sync} odooId={p.odooId} lastSync={lastSync} onPush={pushToOdoo} onSearch={() => setOdooOpen(true)} />
          </div>

          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">2</span><span className="card-t">Projet</span></div></div>
            <div className="fgrid">
              <Field label="Nom du projet" value={p.nom} onChange={set('nom')} span />
              <Field label="Type" value={p.type} onChange={set('type')} opts={['Construction neuve', 'Agrandissement', 'Rénovation majeure']} />
              <Field label="Cible de performance" value={p.cible} onChange={set('cible')} opts={CIBLE_OPTS} />
              <Field label="Superficie habitable (pi²)" value={p.superficie} onChange={set('superficie')} type="mono" />
              <Field label="Nombre d’étages" value={p.etages} onChange={set('etages')} type="mono" />
              <Field label="Entrepreneur" value={p.ent} onChange={set('ent')} />
              <Field label="Estimateur" value={p.estimateur} onChange={set('estimateur')} />
            </div>
          </div>
        </div>

        <aside className="aside">
          <div className="sum tickbox">
            <div className="sum-hero">
              <div className="sum-hero-lbl">Dossier</div>
              <div className="sum-hero-val"><b style={{ fontSize: 30 }}>{p.no}</b></div>
              <div className="sum-hero-row"><span>Ouvert le</span><b>{p.date}</b></div>
              <div className="sum-hero-row"><span>Cible</span><b style={{ fontSize: 12 }}>{p.cible}</b></div>
            </div>
            <div className="sum-body">
              <div className="sum-title">Aperçu</div>
              <div className="sum-row"><div><div className="sum-row-n">{p.clientPrenom} {p.clientNom}</div><div className="sum-row-s">{p.tel}</div></div></div>
              <div className="sum-row"><div><div className="sum-row-n">{p.adresse}</div><div className="sum-row-s">{p.ville} ({p.province}) {p.cp}</div></div></div>
              <div className="note" style={{ marginTop: 10 }}>Prochaine étape : <b>Configuration</b> globale, puis téléverser le plan dans <b>Mesure sur plan</b>.</div>
            </div>
          </div>
        </aside>
      </div>

      {odooOpen && <OdooSearchModal client={p} onClose={() => setOdooOpen(false)} onImport={importContact} />}
    </div>);

}

// ═══════════════════════ CONFIGURATION ═══════════════════════
function ConfigModule({ onLog }) {
  const [c, setC] = React.useState(CFG0);
  const set = (k) => (v) => setC((s) => ({ ...s, [k]: v }));
  return (
    <div className="fade-in">
      <SysHeader icon="sliders" title="Configuration globale"
      sub="Hypothèses de construction appliquées par défaut à tous les modules d’estimation.">
        <button className="btn accent" onClick={() => onLog && onLog('edit', 'a appliqué la configuration globale', 'Hypothèses du projet')}><YIcon n="check" /> Appliquer aux modules</button>
      </SysHeader>

      <div className="grid-2">
        <div className="col">
          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">1</span><span className="card-t">Ossature & murs</span></div></div>
            <div className="fgrid">
              <Field label="Type d’ossature" value={c.ossature} onChange={set('ossature')} opts={OSSATURE_OPTS} span />
              <Field label="Épaisseur de mur (po)" value={c.epaisseurMur} onChange={set('epaisseurMur')} type="mono" />
              <Field label="Hauteur des murs (pi)" value={c.hauteurMur} onChange={set('hauteurMur')} type="mono" />
              <Field label="Isolant principal" value={c.isolantMur} onChange={set('isolantMur')}
              opts={['Cellulose haute densité', 'Laine minérale', 'Béton de chanvre', 'Polyuréthane giclé']} />
              <Field label="Fondation" value={c.fondation} onChange={set('fondation')}
              opts={['Béton coulé 8″', 'Béton coulé 10″', 'ICF / coffrage isolant', 'Pieux vissés']} />
            </div>
          </div>

          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">2</span><span className="card-t">Valeurs R cibles</span></div></div>
            <div className="fgrid" style={{ gridTemplateColumns: 'repeat(3,1fr)' }}>
              <Field label="Murs" value={c.rMur} onChange={set('rMur')} type="mono" />
              <Field label="Toiture" value={c.rToit} onChange={set('rToit')} type="mono" />
              <Field label="Sous-dalle" value={c.rSousDalle} onChange={set('rSousDalle')} type="mono" />
            </div>
          </div>

          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">3</span><span className="card-t">Étanchéité & mécanique</span></div></div>
            <div className="fgrid">
              <Field label="Membrane intérieure" value={c.membraneInt} onChange={set('membraneInt')}
              opts={['Pro Clima Intello Plus', 'SIGA Majrex', 'Polyéthylène 6 mil']} />
              <Field label="Pare-intempéries" value={c.pareIntemperies} onChange={set('pareIntemperies')}
              opts={['Pro Clima Solitex Mento 1000', 'Tyvek CommercialWrap', 'VaproShield']} />
              <Field label="Ventilation" value={c.ventilation} onChange={set('ventilation')}
              opts={['VRC haute efficacité', 'VRC standard', 'VEC (récup. enthalpie)']} />
              <Field label="Cible de performance" value={c.cible} onChange={set('cible')} opts={CIBLE_OPTS} />
            </div>
          </div>
        </div>

        <aside className="aside">
          <div className="sum tickbox">
            <div className="sum-hero">
              <div className="sum-hero-lbl">Enveloppe cible</div>
              <div className="sum-hero-val"><b>{c.rMur}</b><span>murs hors sol</span></div>
              <div className="sum-hero-row"><span>Toiture</span><b>{c.rToit}</b></div>
              <div className="sum-hero-row"><span>Sous-dalle</span><b>{c.rSousDalle}</b></div>
            </div>
            <div className="sum-body">
              <div className="sum-title">Hypothèses actives</div>
              <div className="sum-row"><div><div className="sum-row-n">{c.ossature}</div><div className="sum-row-s">mur de {c.epaisseurMur}″ · {c.hauteurMur} pi</div></div></div>
              <div className="sum-row"><div><div className="sum-row-n">{c.membraneInt}</div><div className="sum-row-s">étanchéité à l’air</div></div></div>
              <div className="note" style={{ marginTop: 10 }}>Ces valeurs préremplissent les paramètres de chaque module ; chacun reste ajustable.</div>
            </div>
          </div>
        </aside>
      </div>
    </div>);

}

// ═══════════════════════ CATALOGUE VENTILATION (admin) ═══════════════════════

const UNITE_LBL = { count: 'Qté', length: 'Long.', area: 'Surface', mixte: 'Mixte' };

function CatalogueVentilation({ onLog }) {
  const [cat, setCat] = React.useState(null);   // null = chargement
  const [err, setErr] = React.useState(null);
  const [search, setSearch] = React.useState('');
  const [filterCat, setFilterCat] = React.useState('');
  const [editCell, setEditCell] = React.useState(null); // { sys, fam, dim, field }
  const [importing, setImporting] = React.useState(false);
  const [importLog, setImportLog] = React.useState([]);
  const [dirty, setDirty] = React.useState(false);

  // Chargement initial
  React.useEffect(() => {
    fetch('data/vent-catalog.json')
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(d => setCat(d))
      .catch(e => setErr('Impossible de charger vent-catalog.json : ' + e));
  }, []);

  if (err)  return <div className="note" style={{color:'#c00'}}>{err}</div>;
  if (!cat) return <div className="note">Chargement du catalogue…</div>;

  // ── données aplaties pour l'affichage ──
  const bFamilles = cat.systemes?.bourcier?.familles || [];
  const alnorProd = cat.systemes?.alnor?.produits || [];

  const allCats = [...new Set(bFamilles.map(f => f.categorie)), 'Alnor'];

  const q = search.toLowerCase();
  const filteredFam = bFamilles.filter(f => {
    if (filterCat && filterCat !== 'Alnor' && f.categorie !== filterCat) return false;
    if (filterCat === 'Alnor') return false;
    if (!q) return true;
    return (f.famille + f.categorie + Object.values(f.dimensions||{}).join(' ')).toLowerCase().includes(q);
  });
  const filteredAlnor = (!filterCat || filterCat === 'Alnor') && alnorProd.filter(p =>
    !q || (p.code + p.libelle).toLowerCase().includes(q));

  // ── édition inline ──
  const patchDim = (famIdx, dim, val) => {
    const next = JSON.parse(JSON.stringify(cat));
    next.systemes.bourcier.familles[famIdx].dimensions[dim] = val;
    setCat(next); setDirty(true);
  };
  const patchNote = (famIdx, val) => {
    const next = JSON.parse(JSON.stringify(cat));
    next.systemes.bourcier.familles[famIdx].note = val;
    setCat(next); setDirty(true);
  };
  const patchAlnorField = (idx, field, val) => {
    const next = JSON.parse(JSON.stringify(cat));
    next.systemes.alnor.produits[idx][field] = val;
    setCat(next); setDirty(true);
  };

  // ── sauvegarde (download JSON) ──
  const handleSave = () => {
    const blob = new Blob([JSON.stringify(cat, null, 2)], {type:'application/json'});
    const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
    a.download = 'vent-catalog.json'; a.click();
    onLog && onLog('recipe', 'a téléchargé vent-catalog.json mis à jour');
    setDirty(false);
  };

  // ── import xlsx ──
  const handleXlsx = (e) => {
    const file = e.target.files[0]; if (!file) return;
    if (!window.XLSX) { setImportLog(['❌ SheetJS non chargé']); return; }
    setImporting(true); setImportLog([]);
    const reader = new FileReader();
    reader.onload = (ev) => {
      try {
        const wb = XLSX.read(ev.target.result, {type:'array'});
        const ws = wb.Sheets[wb.SheetNames[0]];
        const rows = XLSX.utils.sheet_to_json(ws, {defval:''});
        const log = [];
        const next = JSON.parse(JSON.stringify(cat));
        let added = 0, updated = 0;

        rows.forEach((row, i) => {
          const famille = String(row['famille']||row['Famille']||'').trim();
          const categorie = String(row['categorie']||row['Catégorie']||row['categorie']||'').trim();
          const dim = String(row['dimension']||row['Dimension']||row['dim']||'').trim();
          const code = String(row['code']||row['Code']||'').trim();
          const unite = String(row['unite']||row['Unité']||'').trim() || 'count';
          const note = String(row['note']||row['Note']||'').trim();

          if (!famille || !dim || !code) {
            log.push(`Ligne ${i+2} ignorée : famille/dim/code requis`);
            return;
          }

          const fams = next.systemes.bourcier.familles;
          let fam = fams.find(f => f.famille === famille);
          if (!fam) {
            fam = { categorie: categorie || 'Autre', famille, unite: unite||'count', dimensions: {} };
            fams.push(fam);
            log.push(`✦ Nouvelle famille : ${famille}`);
          }
          const existed = fam.dimensions[dim] !== undefined;
          fam.dimensions[dim] = code;
          if (note) fam.note = note;
          existed ? updated++ : added++;
        });

        setCat(next);
        setDirty(true);
        log.unshift(`✅ ${added} ajoutés, ${updated} mis à jour sur ${rows.length} lignes`);
        setImportLog(log);
      } catch(ex) {
        setImportLog(['❌ Erreur lecture xlsx : ' + ex.message]);
      } finally {
        setImporting(false);
      }
    };
    reader.readAsArrayBuffer(file);
    e.target.value = '';
  };

  const cellKey = (sys, famIdx, dim, field) => `${sys}-${famIdx}-${dim}-${field}`;
  const isEdit = (k) => editCell === k;

  return (
    <div className="fade-in">
      {/* Barre d'outils */}
      <div style={{display:'flex', gap:8, flexWrap:'wrap', alignItems:'center', marginBottom:14}}>
        <input
          type="search" placeholder="Rechercher famille, code, dimension…"
          value={search} onChange={e => setSearch(e.target.value)}
          style={{flex:1, minWidth:180, padding:'6px 10px', border:'1px solid var(--border)', borderRadius:6, fontSize:13, background:'var(--bg-card)', color:'var(--text)'}}
        />
        <select value={filterCat} onChange={e => setFilterCat(e.target.value)}
          style={{padding:'6px 8px', border:'1px solid var(--border)', borderRadius:6, fontSize:13, background:'var(--bg-card)', color:'var(--text)'}}>
          <option value="">Toutes catégories</option>
          {allCats.map(c => <option key={c} value={c}>{c}</option>)}
        </select>

        <label className={'btn' + (importing?' disabled':'')} style={{cursor:'pointer', margin:0}}>
          <YIcon n="upload" /> Importer xlsx
          <input type="file" accept=".xlsx,.xls" onChange={handleXlsx} style={{display:'none'}} disabled={importing} />
        </label>
        {dirty &&
          <button className="btn accent" onClick={handleSave}>
            <YIcon n="download" /> Sauvegarder JSON
          </button>}
      </div>

      {/* Log import */}
      {importLog.length > 0 &&
        <div className="note" style={{marginBottom:12, lineHeight:1.7}}>
          {importLog.map((l,i) => <div key={i}>{l}</div>)}
        </div>}

      {/* Tableau Bourcier */}
      {(!filterCat || filterCat !== 'Alnor') && filteredFam.length > 0 && (
        <div style={{marginBottom:20}}>
          <div style={{fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'.05em', color:'var(--text-sub)', marginBottom:8}}>
            Bourcier — {filteredFam.reduce((n,f)=>n+Object.keys(f.dimensions||{}).length,0)} entrées
          </div>
          <div style={{border:'1px solid var(--border)', borderRadius:8, overflow:'hidden'}}>
            <table style={{width:'100%', borderCollapse:'collapse', fontSize:13}}>
              <thead>
                <tr style={{background:'var(--bg-sub)'}}>
                  {['Catégorie','Famille','Dimension','Code produit','Unité','Note'].map(h =>
                    <th key={h} style={{textAlign:'left', padding:'7px 10px', fontSize:11, fontWeight:600, textTransform:'uppercase', letterSpacing:'.04em', color:'var(--text-sub)', borderBottom:'1px solid var(--border)'}}>{h}</th>
                  )}
                </tr>
              </thead>
              <tbody>
                {filteredFam.map((fam) => {
                  const famIdx = bFamilles.indexOf(fam);
                  const dims = Object.entries(fam.dimensions || {});
                  return dims.map(([dim, code], di) => {
                    const ckCode = cellKey('b', famIdx, dim, 'code');
                    const ckNote = cellKey('b', famIdx, dim, 'note');
                    const isLmi = String(code).startsWith('LMI:');
                    return (
                      <tr key={famIdx+'-'+dim} style={{borderBottom:'1px solid var(--border)'}}>
                        {di === 0 && <td rowSpan={dims.length} style={{padding:'6px 10px', color:'var(--text-sub)', fontSize:12, verticalAlign:'top', borderRight:'1px solid var(--border)'}}>{fam.categorie}</td>}
                        {di === 0 && <td rowSpan={dims.length} style={{padding:'6px 10px', fontWeight:500, verticalAlign:'top', borderRight:'1px solid var(--border)'}}>{fam.famille}</td>}
                        <td style={{padding:'6px 10px', fontFamily:'monospace', fontSize:12, color:'var(--text-sub)'}}>{dim}</td>
                        <td style={{padding:'4px 6px'}}>
                          {isEdit(ckCode)
                            ? <input autoFocus className="cell-in" defaultValue={code}
                                onBlur={e=>{patchDim(famIdx,dim,e.target.value);setEditCell(null);}}
                                onKeyDown={e=>{if(e.key==='Enter'){patchDim(famIdx,dim,e.target.value);setEditCell(null);}if(e.key==='Escape')setEditCell(null);}}
                                style={{width:'100%', fontFamily:'monospace', fontSize:12}} />
                            : <span
                                onClick={()=>setEditCell(ckCode)}
                                style={{display:'inline-block', fontFamily:'monospace', fontSize:12, fontWeight:600,
                                  background: isLmi ? '#fff7ed' : 'var(--bg-sub)',
                                  color: isLmi ? '#b45309' : 'inherit',
                                  padding:'2px 7px', borderRadius:4, cursor:'text'}}>
                                {code}
                              </span>}
                        </td>
                        <td style={{padding:'6px 10px'}}>
                          <span style={{fontSize:11, padding:'2px 7px', borderRadius:10, background:'var(--bg-sub)', color:'var(--text-sub)', fontWeight:500}}>
                            {UNITE_LBL[fam.unite]||fam.unite}
                          </span>
                        </td>
                        {di === 0 && <td rowSpan={dims.length} style={{padding:'4px 6px', verticalAlign:'top'}}>
                          {isEdit(ckNote)
                            ? <input autoFocus className="cell-in" defaultValue={fam.note||''}
                                onBlur={e=>{patchNote(famIdx,e.target.value);setEditCell(null);}}
                                onKeyDown={e=>{if(e.key==='Enter'){patchNote(famIdx,e.target.value);setEditCell(null);}if(e.key==='Escape')setEditCell(null);}}
                                style={{width:'100%', fontSize:12}} />
                            : <span onClick={()=>setEditCell(ckNote)}
                                style={{fontSize:11, color:fam.note?'#b45309':'var(--text-sub)', fontStyle:fam.note?'italic':'normal', cursor:'text'}}>
                                {fam.note || <span style={{opacity:.35}}>Ajouter note…</span>}
                              </span>}
                        </td>}
                      </tr>);
                  });
                })}
              </tbody>
            </table>
          </div>
        </div>
      )}

      {/* Tableau Alnor */}
      {filteredAlnor && filteredAlnor.length > 0 && (
        <div>
          <div style={{fontSize:11, fontWeight:700, textTransform:'uppercase', letterSpacing:'.05em', color:'var(--text-sub)', marginBottom:8}}>
            Alnor — {filteredAlnor.length} produits en stock
          </div>
          <div style={{border:'1px solid var(--border)', borderRadius:8, overflow:'hidden'}}>
            <table style={{width:'100%', borderCollapse:'collapse', fontSize:13}}>
              <thead>
                <tr style={{background:'var(--bg-sub)'}}>
                  {['Code','Description','Stock','Unité'].map(h =>
                    <th key={h} style={{textAlign:'left', padding:'7px 10px', fontSize:11, fontWeight:600, textTransform:'uppercase', letterSpacing:'.04em', color:'var(--text-sub)', borderBottom:'1px solid var(--border)'}}>{h}</th>
                  )}
                </tr>
              </thead>
              <tbody>
                {filteredAlnor.map((p, i) => {
                  const idx = alnorProd.indexOf(p);
                  const ckCode = cellKey('a', idx, '', 'code');
                  const ckLib  = cellKey('a', idx, '', 'libelle');
                  return (
                    <tr key={p.code+i} style={{borderBottom:'1px solid var(--border)'}}>
                      <td style={{padding:'4px 6px'}}>
                        {isEdit(ckCode)
                          ? <input autoFocus className="cell-in" defaultValue={p.code}
                              onBlur={e=>{patchAlnorField(idx,'code',e.target.value);setEditCell(null);}}
                              onKeyDown={e=>{if(e.key==='Enter'){patchAlnorField(idx,'code',e.target.value);setEditCell(null);}if(e.key==='Escape')setEditCell(null);}}
                              style={{fontFamily:'monospace', fontSize:12}} />
                          : <span onClick={()=>setEditCell(ckCode)} style={{fontFamily:'monospace', fontSize:12, fontWeight:600, background:'var(--bg-sub)', padding:'2px 7px', borderRadius:4, cursor:'text', display:'inline-block'}}>{p.code}</span>}
                      </td>
                      <td style={{padding:'4px 6px'}}>
                        {isEdit(ckLib)
                          ? <input autoFocus className="cell-in" defaultValue={p.libelle}
                              onBlur={e=>{patchAlnorField(idx,'libelle',e.target.value);setEditCell(null);}}
                              onKeyDown={e=>{if(e.key==='Enter'){patchAlnorField(idx,'libelle',e.target.value);setEditCell(null);}if(e.key==='Escape')setEditCell(null);}}
                              style={{width:'100%', fontSize:12}} />
                          : <span onClick={()=>setEditCell(ckLib)} style={{fontSize:12, cursor:'text'}}>{p.libelle}</span>}
                      </td>
                      <td style={{padding:'6px 10px', fontSize:12, color:'var(--text-sub)'}}>{p.en_stock}</td>
                      <td style={{padding:'6px 10px'}}>
                        <span style={{fontSize:11, padding:'2px 7px', borderRadius:10, background:'var(--bg-sub)', color:'var(--text-sub)', fontWeight:500}}>
                          {UNITE_LBL[p.unite]||p.unite||'Qté'}
                        </span>
                      </td>
                    </tr>);
                })}
              </tbody>
            </table>
          </div>
        </div>
      )}

      {filteredFam.length === 0 && (!filteredAlnor || filteredAlnor.length === 0) &&
        <div style={{textAlign:'center', padding:32, color:'var(--text-sub)'}}>Aucun résultat</div>}
    </div>
  );
}

// ═══════════════════════ RECETTES (admin) ═══════════════════════
function RecettesModule({ onLog }) {
  const [tab, setTab] = React.useState('recettes'); // 'recettes' | 'ventilation'
  const [recipes, setRecipes] = React.useState(RCP);
  const [openId, setOpenId] = React.useState(RCP[0].id);
  // variante active par recette (pour les recettes catégorisées brand → cat → variante)
  const [variantOf, setVariantOf] = React.useState(() =>
  RCP.reduce((a, r) => { if (r.variants) a[r.id] = r.variant || r.variants[0].id; return a; }, {}));
  const [editVar, setEditVar] = React.useState(null);

  const activeVid = (r) => variantOf[r.id] || (r.variants ? r.variants[0].id : null);
  // édite les lignes de la variante active ET les reflète dans r.lines (→ soumission)
  const editLines = (r, fn) => {
    if (!r.variants) return { ...r, lines: fn(r.lines) };
    const vid = activeVid(r);
    const variants = r.variants.map((v) => v.id !== vid ? v : { ...v, lines: fn(v.lines) });
    return { ...r, variants, lines: (variants.find((v) => v.id === vid) || {}).lines || r.lines };
  };
  const pickVariant = (rid, vid, name) => {
    setVariantOf((o) => ({ ...o, [rid]: vid }));
    setRecipes((rs) => rs.map((r) => r.id !== rid ? r : { ...r, variant: vid, lines: (r.variants.find((v) => v.id === vid) || {}).lines || r.lines }));
    onLog && onLog('recipe', `a sélectionné la variante « ${name} »`);
  };

  const setLine = (rid, li, patch) => setRecipes((rs) => rs.map((r) =>
  r.id !== rid ? r : editLines(r, (ls) => ls.map((l, i) => i === li ? { ...l, ...patch } : l))));
  const addLine = (rid) => setRecipes((rs) => rs.map((r) =>
  r.id !== rid ? r : editLines(r, (ls) => [...ls, { item: 'Nouveau composant', sku: '—', driver: 'area', per: 1, base: 100, unit: 'mcx', note: '' }])));
  const delLine = (rid, li) => setRecipes((rs) => rs.map((r) =>
  r.id !== rid ? r : editLines(r, (ls) => ls.filter((_, i) => i !== li))));

  // ── édition du catalogue de variantes (créer / renommer / supprimer) ──
  const addVariant = (rid) => {
    const r0 = recipes.find((r) => r.id === rid);
    if (!r0) return;
    const vid = 'v' + Date.now();
    const base = r0.variants && r0.variants.length ? r0.variants[0] : null;
    const nv = { id: vid, name: 'Nouvelle variante', sub: 'À définir',
      params: base ? Object.keys(base.params || {}).reduce((a, k) => (a[k] = '—', a), {}) : {},
      lines: base ? base.lines.map((l) => ({ ...l })) : [...(r0.lines || [])] };
    setRecipes((rs) => rs.map((r) => r.id !== rid ? r :
    { ...r, variants: [...(r.variants || []), nv], variant: vid, cat: r.cat || 'Nouvelle catégorie', lines: nv.lines }));
    setVariantOf((o) => ({ ...o, [rid]: vid }));
    onLog && onLog('recipe', `a créé une variante dans « ${r0.name} »`);
  };
  const delVariant = (rid, vid) => {
    const r0 = recipes.find((r) => r.id === rid);
    if (!r0 || !r0.variants || r0.variants.length <= 1) return;
    const variants = r0.variants.filter((v) => v.id !== vid);
    const nextVid = activeVid(r0) === vid ? variants[0].id : activeVid(r0);
    setRecipes((rs) => rs.map((r) => r.id !== rid ? r :
    { ...r, variants, variant: nextVid, lines: (variants.find((v) => v.id === nextVid) || {}).lines || r.lines }));
    setVariantOf((o) => ({ ...o, [rid]: nextVid }));
  };
  const setVariantField = (rid, vid, patch) => setRecipes((rs) => rs.map((r) =>
  r.id !== rid || !r.variants ? r : { ...r, variants: r.variants.map((v) => v.id !== vid ? v : { ...v, ...patch }) }));
  const setVariantParam = (rid, vid, key, val) => setRecipes((rs) => rs.map((r) =>
  r.id !== rid || !r.variants ? r : { ...r, variants: r.variants.map((v) => v.id !== vid ? v : { ...v, params: { ...v.params, [key]: val } }) }));
  const setCat = (rid, val) => setRecipes((rs) => rs.map((r) => r.id !== rid ? r : { ...r, cat: val }));

  const DRIVER_LBL = { area: '/ pi²', perim: '/ pi lin.', prev: '/ ligne préc.' };

  return (
    <div className="fade-in">
      <SysHeader icon="beaker" title="Recettes & ratios"
      sub="Définissez les consommables et ratios de chaque composition. Ils alimentent automatiquement la soumission.">
        {tab === 'recettes' && <>
          <button className="btn" onClick={() => onLog && onLog('recipe', 'a exporté les recettes')}><YIcon n="download" /> Exporter</button>
          <button className="btn accent" onClick={() => onLog && onLog('recipe', 'a créé une recette')}><YIcon n="plus" /> Nouvelle recette</button>
        </>}
      </SysHeader>

      {/* Onglets */}
      <div className="steps" style={{marginBottom:16}}>
        <button className={'step' + (tab==='recettes'?' on':'')} onClick={()=>setTab('recettes')}>
          <span className="step-n">1</span> Recettes & ratios
        </button>
        <button className={'step' + (tab==='ventilation'?' on':'')} onClick={()=>setTab('ventilation')}>
          <span className="step-n">2</span> Catalogue ventilation
        </button>
      </div>

      {tab === 'ventilation' && <CatalogueVentilation onLog={onLog} />}

      {tab === 'recettes' && <>
      <div className="note" style={{ marginBottom: 14 }}>
        <b>Exemple.</b> « 3 rouleaux de Tescon Vana / 1 rouleau d’Intello » ou « 1 Contega HF / 30 pi lin. de mur » se saisissent ici une seule fois, puis s’appliquent à tous les projets.
      </div>

      <div className="rcp-list">
        {recipes.map((r) => {
          const open = openId === r.id;
          const mod = yModById(r.module);
          const issues = yRecipeIssues ? yRecipeIssues(r) : [];
          const linkedSys = ySysForRecipe ? ySysForRecipe(r.id, r.module) : null;
          return (
            <div className={'rcp' + (open ? ' open' : '') + (issues.length ? ' bad' : '')} key={r.id}>
              <button className="rcp-head" onClick={() => setOpenId(open ? null : r.id)}>
                <span className="rcp-ic"><YIcon n={mod ? mod.icon : 'beaker'} /></span>
                <div className="rcp-head-tx">
                  <div className="rcp-name">{r.name}</div>
                  <div className="rcp-meta">{mod ? mod.name : r.module} · pilote : {r.driver}</div>
                </div>
                {issues.length > 0 && <span className="rcp-warn" title={issues.join(' · ')}>⚠ {issues[0]}</span>}
                {r.cat && <span className="catchip"><YIcon n="folders" style={{ width: 12, height: 12 }} /> {r.cat}</span>}
                {linkedSys ?
                  <span className="syschip" style={{ '--sys': linkedSys.accent }} title={`Système lié : ${linkedSys.name}`}><span className="syschip-mono">{linkedSys.mono}</span> {linkedSys.name}</span> :
                  <span className="chip-type">{r.brand}</span>}
                <span className="rcp-count">{r.variants ? `${r.variants.length} variantes · ` : ''}{r.lines.length} composants</span>
                <YIcon n={open ? 'chevd' : 'chev'} style={{ width: 16, height: 16, opacity: .5 }} />
              </button>

              {open &&
              <div className="rcp-body">
                  {r.variants &&
                  <div className="varsel">
                    <div className="varsel-h">
                      <span className="varsel-cat"><YIcon n="folders" style={{ width: 13, height: 13 }} />
                        {editVar === r.id ?
                        <input className="cell-in varsel-catin" value={r.cat || ''} onChange={(e) => setCat(r.id, e.target.value)} placeholder="Nom de la catégorie" /> :
                        <span>{r.cat}</span>}
                      </span>
                      <span className="varsel-sub">{r.brand} · type de système précis</span>
                      <div className="varsel-actions">
                        <button className={'minibtn' + (editVar === r.id ? ' on' : '')} onClick={() => setEditVar(editVar === r.id ? null : r.id)}>
                          <YIcon n={editVar === r.id ? 'check' : 'pencil'} style={{ width: 12, height: 12 }} /> {editVar === r.id ? 'Terminer' : 'Éditer'}
                        </button>
                        <button className="minibtn accent" onClick={() => { addVariant(r.id); setEditVar(r.id); }}><YIcon n="plus" style={{ width: 12, height: 12 }} /> Variante</button>
                      </div>
                    </div>
                    <div className="varsel-row">
                      {r.variants.map((v) => {
                      const on = v.id === activeVid(r);
                      return (
                        <div className={'varcard-wrap' + (on ? ' on' : '')} key={v.id}>
                            <button className={'varcard' + (on ? ' on' : '')} onClick={() => pickVariant(r.id, v.id, v.name)}>
                              <div className="varcard-top">
                                <span className="varcard-rd" />
                                <div className="varcard-n">{v.name}</div>
                              </div>
                              <div className="varcard-s">{v.sub}</div>
                              {v.params && Object.keys(v.params).length > 0 &&
                              <div className="varcard-params">
                                  {Object.entries(v.params).map(([k, val]) =>
                                <span key={k}><em>{k}</em>{val}</span>
                                )}
                                </div>
                              }
                              <div className="varcard-count">{v.lines.length} matériaux</div>
                            </button>
                            {editVar === r.id && r.variants.length > 1 &&
                            <button className="varcard-del" title="Supprimer la variante" onClick={() => delVariant(r.id, v.id)}><YIcon n="trash" style={{ width: 12, height: 12 }} /></button>
                            }
                          </div>);

                    })}
                    </div>
                    {editVar === r.id && (() => {
                    const av = r.variants.find((v) => v.id === activeVid(r)) || r.variants[0];
                    return (
                      <div className="varedit">
                          <div className="varedit-h">Éditer « {av.name} »</div>
                          <div className="varedit-grid">
                            <div className="fcell"><label className="lbl">Nom de la variante</label>
                              <input className="field" value={av.name} onChange={(e) => setVariantField(r.id, av.id, { name: e.target.value })} /></div>
                            <div className="fcell" style={{ flex: 1 }}><label className="lbl">Description</label>
                              <input className="field" value={av.sub || ''} onChange={(e) => setVariantField(r.id, av.id, { sub: e.target.value })} /></div>
                          </div>
                          <div className="varedit-params">
                            <label className="lbl">Paramètres</label>
                            {Object.entries(av.params || {}).map(([k, val]) =>
                          <div className="varparam-row" key={k}>
                                <input className="cell-in varparam-k" value={k} readOnly />
                                <input className="cell-in" value={val} onChange={(e) => setVariantParam(r.id, av.id, k, e.target.value)} />
                                <button className="row-x" onClick={() => setRecipes((rs) => rs.map((x) => x.id !== r.id ? x : { ...x, variants: x.variants.map((vv) => vv.id !== av.id ? vv : { ...vv, params: Object.fromEntries(Object.entries(vv.params).filter(([kk]) => kk !== k)) }) }))}><YIcon n="trash" style={{ width: 13, height: 13 }} /></button>
                              </div>
                          )}
                            <button className="addrow sm" onClick={() => { const key = prompt('Nom du paramètre (ex. Couleur)'); if (key) setVariantParam(r.id, av.id, key, '—'); }}><YIcon n="plus" /> Ajouter un paramètre</button>
                          </div>
                          <div className="varedit-note">Les matériaux de cette variante s'éditent dans le tableau ci-dessous.</div>
                        </div>);

                  })()}
                  </div>
                  }
                  <div className="wrap-flex" style={{ marginBottom: 12 }}>
                    <div className="fcell"><label className="lbl">Système de produits</label>
                      <select className="field" defaultValue={linkedSys ? linkedSys.id : ''} style={{ minWidth: 180 }}
                        onChange={(e) => onLog && onLog('recipe', `a lié la recette « ${r.name} » à un système`, e.target.value)}>
                        <option value="">— Hybride (aucun système) —</option>
                        {(ySysForMod ? ySysForMod(r.module) : []).map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
                      </select></div>
                    <div className="fcell"><label className="lbl">Marque par défaut</label>
                      <select className="field" defaultValue={r.brand} style={{ minWidth: 160 }}>{r.brands.map((b) => <option key={b}>{b}</option>)}</select></div>
                    <div className="fcell"><label className="lbl">Module pilote</label>
                      <input className="field" defaultValue={mod ? mod.name : r.module} style={{ minWidth: 180 }} readOnly /></div>
                  </div>
                  <div className="tbl-wrap">
                    <table>
                      <thead><tr>
                        <th>Composant</th>
                        <th style={{ width: 80 }}>Code</th>
                        <th style={{ width: 64 }} className="num">Qté</th>
                        <th style={{ width: 64 }} className="num">Par</th>
                        <th style={{ width: 120 }}>Base</th>
                        <th style={{ width: 90 }}>Unité vente</th>
                        <th style={{ width: 36 }}></th>
                      </tr></thead>
                      <tbody>
                        {r.lines.map((l, i) =>
                      <tr key={i}>
                            <td><input className="cell-in" value={l.item} onChange={(e) => setLine(r.id, i, { item: e.target.value })} /></td>
                            <td><input className="cell-in mono" value={l.sku} onChange={(e) => setLine(r.id, i, { sku: e.target.value })} /></td>
                            <td className="num"><input className="cell-in num" value={l.per} onChange={(e) => setLine(r.id, i, { per: Number(e.target.value) || 0 })} /></td>
                            <td className="num"><input className="cell-in num" value={l.base} onChange={(e) => setLine(r.id, i, { base: Number(e.target.value) || 0 })} /></td>
                            <td><select className="cell-in" value={l.driver} onChange={(e) => setLine(r.id, i, { driver: e.target.value })}>
                              <option value="area">{DRIVER_LBL.area}</option>
                              <option value="perim">{DRIVER_LBL.perim}</option>
                              <option value="prev">{DRIVER_LBL.prev}</option>
                            </select></td>
                            <td><input className="cell-in" value={l.unit} onChange={(e) => setLine(r.id, i, { unit: e.target.value })} /></td>
                            <td><button className="row-x" onClick={() => delLine(r.id, i)}><YIcon n="trash" /></button></td>
                          </tr>
                      )}
                      </tbody>
                    </table>
                  </div>
                  <button className="addrow" onClick={() => addLine(r.id)}><YIcon n="plus" /> Ajouter un composant</button>
                </div>
              }
            </div>);

        })}
      </div>
      </>}
    </div>);

}

// ═══════════════════════ SOUMISSION ═══════════════════════
function SoumissionModule({ measures, project, onLog, recipeChoices, onChoose, onToggleExclude }) {
  const drivers = moduleDrivers(measures);
  // exclusions = source de vérité partagée (recipeChoices) → synchro avec les widgets
  const exclOf = (pid) => (recipeChoices && recipeChoices[pid] && recipeChoices[pid].excl) || {};
  // postes = estimation modules that have a driver + at least one recipe
  const postes = EST_MODS.filter((m) => drivers[m.id] && rcpFor(m.id).length);
  const [sel, setSel] = React.useState(() => {
    const o = {};postes.forEach((p) => {
      const ch = recipeChoices && recipeChoices[p.id];
      const first = rcpFor(p.id)[0];
      o[p.id] = { on: p.status !== 'none', recipe: ch?.recipe || first.id, brand: ch?.brand || first.brand, excl: {} };
    });
    return o;
  });
  const [expanded, setExpanded] = React.useState(null); // poste déplié (composants optionnels)
  // reflète les systèmes appliqués ailleurs (modules / application globale)
  React.useEffect(() => {
    if (!recipeChoices) return;
    setSel((s) => {
      let changed = false; const next = { ...s };
      postes.forEach((p) => {
        const ch = recipeChoices[p.id];
        if (ch && s[p.id] && (s[p.id].recipe !== ch.recipe || s[p.id].brand !== ch.brand)) {
          next[p.id] = { ...s[p.id], recipe: ch.recipe, brand: ch.brand }; changed = true;
        }
      });
      return changed ? next : s;
    });
  }, [recipeChoices]);
  const setSel2 = (id, patch) => {
    setSel((s) => ({ ...s, [id]: { ...s[id], ...patch } }));
    if (onChoose && (patch.recipe || patch.brand)) {
      const cur = sel[id] || {};
      onChoose(id, patch.recipe || cur.recipe, patch.brand || cur.brand);
    }
  };
  // changer de recette remet à zéro les exclusions (les composants diffèrent)
  const pickRecipe = (id, rid, br) => { onChoose && onChoose(id, rid, br); setSel((s) => ({ ...s, [id]: { ...s[id], recipe: rid, brand: br } })); };
  // activer/désactiver un composant — écrit dans le store partagé (≡ widget)
  const toggleLine = (id, sku) => {
    if (onToggleExclude) onToggleExclude(id, sku);
    onLog && onLog('submission', 'a ajusté un composant de recette', sku);
  };

  // aggregate BOM across selected postes — composants exclus retirés
  const bom = [];
  postes.forEach((p) => {
    if (!sel[p.id].on) return;
    const recipe = rcpFor(p.id).find((r) => r.id === sel[p.id].recipe);
    if (!recipe) return;
    const excl = exclOf(p.id);
    bomForRecipe(recipe, drivers[p.id]).forEach((line) => {
      if (excl[line.sku]) return;                  // composant désactivé pour ce projet
      bom.push({ ...line, module: p.name, brand: sel[p.id].brand });
    });
  });
  // nb de composants inclus / total par poste
  const lineStats = (p) => {
    const recipe = rcpFor(p.id).find((r) => r.id === sel[p.id].recipe);
    const all = recipe ? bomForRecipe(recipe, drivers[p.id]) : [];
    const excl = exclOf(p.id);
    return { all, inc: all.filter((l) => !excl[l.sku]).length };
  };
  const totalItems = bom.reduce((s, b) => s + b.qty, 0);
  const activeCount = postes.filter((p) => sel[p.id].on).length;

  return (
    <div className="fade-in">
      <SysHeader icon="receipt" title="Soumission"
      sub="Choisissez la composition et la marque par poste, puis générez la liste de matériaux.">
        <button className="btn" onClick={() => onLog && onLog('submission', 'a exporté un aperçu PDF', project ? 'dossier ' + project.no : '', project ? project.no : '')}><YIcon n="file" /> Aperçu PDF</button>
        <button className="btn accent" onClick={() => onLog && onLog('submission', 'a généré la soumission', `${activeCount} postes · ${yFmt(totalItems, 0)} articles`, project ? project.no : '')}><YIcon n="download" /> Générer la soumission</button>
      </SysHeader>

      <div className="grid-2">
        <div className="col">
          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">1</span><span className="card-t">Postes à soumissionner</span></div>
              <span className="card-hint">{activeCount}/{postes.length} actifs</span></div>

            <div className="poste-list">
              {postes.map((p) => {
                const opts = rcpFor(p.id);
                const recipe = opts.find((r) => r.id === sel[p.id].recipe);
                const on = sel[p.id].on;
                const st = lineStats(p);
                const isOpen = expanded === p.id;
                const excl = exclOf(p.id);
                const nOff = st.all.length - st.inc;
                return (
                  <div className={'poste' + (on ? '' : ' off') + (isOpen ? ' exp' : '')} key={p.id}>
                    <div className="poste-row">
                      <button className={'poste-check' + (on ? ' on' : '')} onClick={() => setSel2(p.id, { on: !on })}>
                        {on && <YIcon n="check" />}
                      </button>
                      <span className="poste-ic"><YIcon n={p.icon} /></span>
                      <div className="poste-tx">
                        <div className="poste-name">{p.name}</div>
                        <div className="poste-driver">{drivers[p.id].label}</div>
                      </div>
                      <div className="poste-ctrls">
                        <select className="field" value={sel[p.id].recipe} disabled={!on}
                        onChange={(e) => {const rc = opts.find((r) => r.id === e.target.value);pickRecipe(p.id, e.target.value, rc.brand);}}>
                          {opts.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
                        </select>
                        <select className="field" value={sel[p.id].brand} disabled={!on}
                        onChange={(e) => setSel2(p.id, { brand: e.target.value })}>
                          {(recipe ? recipe.brands : []).map((b) => <option key={b}>{b}</option>)}
                        </select>
                      </div>
                      <button className={'poste-exp' + (isOpen ? ' on' : '')} disabled={!on}
                        onClick={() => setExpanded(isOpen ? null : p.id)}
                        title="Composants optionnels">
                        <span className="poste-exp-n">{st.inc}/{st.all.length}{nOff > 0 ? ` · ${nOff} exclu${nOff > 1 ? 's' : ''}` : ''}</span>
                        <YIcon n={isOpen ? 'chevd' : 'chev'} style={{ width: 14, height: 14 }} />
                      </button>
                    </div>
                    {isOpen && on &&
                    <div className="poste-lines">
                      <div className="poste-lines-h">Composants — décochez ceux à exclure de cette soumission</div>
                      {st.all.map((l) => {
                        const off = !!excl[l.sku];
                        return (
                          <button className={'poste-line' + (off ? ' off' : '')} key={l.sku} onClick={() => toggleLine(p.id, l.sku)}>
                            <span className={'poste-line-ck' + (off ? '' : ' on')}>{!off && <YIcon n="check" style={{ width: 11, height: 11 }} />}</span>
                            <span className="poste-line-tx">
                              <span className="poste-line-name">{l.item}</span>
                              <span className="poste-line-note">{l.note}</span>
                            </span>
                            <span className="poste-line-qty">{off ? '—' : yFmt(l.qty) + ' ' + l.unit}</span>
                          </button>);
                      })}
                    </div>}
                  </div>);

              })}
            </div>
          </div>

          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">2</span><span className="card-t">Liste de matériaux</span></div>
              <span className="card-hint">{bom.length} lignes</span></div>
            <div className="tbl-wrap">
              <table>
                <thead><tr>
                  <th>Composant</th>
                  <th style={{ width: 90 }}>Code</th>
                  <th style={{ width: 130 }}>Marque</th>
                  <th style={{ width: 130 }}>Poste</th>
                  <th className="num" style={{ width: 90 }}>Qté</th>
                  <th style={{ width: 80 }}>Unité</th>
                </tr></thead>
                <tbody>
                  {bom.map((b, i) =>
                  <tr key={i}>
                      <td style={{ fontWeight: 500 }}>{b.item}</td>
                      <td className="cell-ro" style={{ textAlign: 'left' }}>{b.sku}</td>
                      <td><span className="chip-type">{b.brand}</span></td>
                      <td className="muted" style={{ fontSize: 12 }}>{b.module}</td>
                      <td className="cell-ro">{yFmt(b.qty)}</td>
                      <td className="muted">{b.unit}</td>
                    </tr>
                  )}
                  {bom.length === 0 &&
                  <tr><td colSpan="6" style={{ textAlign: 'center', padding: '24px', color: 'var(--ink-3)' }}>Activez au moins un poste pour générer la liste.</td></tr>
                  }
                </tbody>
              </table>
            </div>
          </div>
        </div>

        <aside className="aside">
          <div className="sum tickbox">
            <div className="sum-hero">
              <div className="sum-hero-lbl">Soumission de quantités</div>
              <div className="sum-hero-val"><b>{yFmt(bom.length)}</b><span>lignes de matériaux</span></div>
              <div className="sum-hero-row"><span>Postes inclus</span><b>{activeCount}</b></div>
              <div className="sum-hero-row"><span>Unités totales</span><b>{yFmt(totalItems)}</b></div>
            </div>
            <div className="sum-body">
              <div className="sum-title">Postes inclus</div>
              {postes.filter((p) => sel[p.id].on).map((p) => {
                const st = lineStats(p);
                return (
                <div className="sum-row" key={p.id}>
                  <div><div className="sum-row-n">{p.name}</div><div className="sum-row-s">{sel[p.id].brand}</div></div>
                  <div className="sum-row-v"><b>{st.inc}{st.inc < st.all.length ? `/${st.all.length}` : ''}</b><span>composants</span></div>
                </div>);
              })}
              <div className="note" style={{ marginTop: 10 }}>Les quantités sont calculées à partir des recettes (module <b>Recettes</b>) et des relevés de chaque module.</div>
              <div className="sum-exports">
                <button className="btn ghost"><YIcon n="file" /> PDF</button>
                <button className="btn ghost"><YIcon n="download" /> Excel</button>
              </div>
            </div>
          </div>
        </aside>
      </div>
    </div>);

}

// ═══════════════════════ JOURNAL D'ACTIVITÉ ═══════════════════════
const ACT_META = {
  submission: { icon: 'receipt', label: 'Soumission' },
  edit: { icon: 'sliders', label: 'Modification' },
  measure: { icon: 'ruler', label: 'Relevé' },
  recipe: { icon: 'beaker', label: 'Recette' },
  login: { icon: 'logout', label: 'Connexion' },
  user: { icon: 'users', label: 'Utilisateur' }
};

function ActivityModule({ user, activity = [] }) {
  const { userById: aUserById, USERS: aUsers, ROLES: aRoles, Avatar: AV } = window;
  const [fType, setFType] = React.useState('all');
  const [fUser, setFUser] = React.useState('all');
  const rows = activity.filter((a) =>
  (fType === 'all' || a.type === fType) && (fUser === 'all' || a.userId === fUser));
  const isAdmin = user && user.role === 'admin';

  return (
    <div className="fade-in">
      <SysHeader icon="history" title="Journal d’activité"
      sub="Historique horodaté des actions — qui a modifié quoi, et quand.">
        {isAdmin && <button className="btn"><YIcon n="download" /> Exporter l’audit</button>}
      </SysHeader>

      <div className="wrap-flex" style={{ marginBottom: 14 }}>
        <div className="seg">
          {['all', 'submission', 'edit', 'measure', 'recipe'].map((tp) =>
          <button key={tp} className={fType === tp ? 'on' : ''} onClick={() => setFType(tp)}>
              {tp === 'all' ? 'Tout' : ACT_META[tp].label}
            </button>
          )}
        </div>
        <select className="field" style={{ width: 200 }} value={fUser} onChange={(e) => setFUser(e.target.value)}>
          <option value="all">Tous les utilisateurs</option>
          {aUsers.map((u) => <option key={u.id} value={u.id}>{u.prenom} {u.nom}</option>)}
        </select>
        {!isAdmin && <span className="muted" style={{ fontSize: 12 }}>Lecture seule · l’export est réservé aux administrateurs.</span>}
      </div>

      <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
        <div className="actlog">
          {rows.map((a) => {
            const u = aUserById(a.userId) || {};
            const meta = ACT_META[a.type] || ACT_META.edit;
            return (
              <div className="actrow" key={a.id}>
                <span className={'act-ic act-' + a.type}><YIcon n={meta.icon} /></span>
                {AV && <AV user={u} size={30} />}
                <div className="act-tx">
                  <div className="act-line"><b>{u.prenom} {u.nom}</b> {a.action} {a.cible && <span className="act-cible">{a.cible}</span>}</div>
                  <div className="act-meta">
                    <span className="act-type">{meta.label}</span>
                    {a.projet && <span>· dossier {a.projet}</span>}
                  </div>
                </div>
                <div className="act-ts">{a.ts}</div>
              </div>);

          })}
          {rows.length === 0 && <div style={{ padding: 24, textAlign: 'center', color: 'var(--ink-3)' }}>Aucune activité pour ce filtre.</div>}
        </div>
      </div>
    </div>);

}

// ═══════════════════════ UTILISATEURS & RÔLES ═══════════════════════
function TeamModule({ user, userPerms = {}, onSetPerm, onLog }) {
  const { USERS: aUsers, ROLES: aRoles, Avatar: AV, roleLabel: aRoleLabel,
    CAPABILITIES: aCaps, capFromRole: aCapFromRole, ROLE_CAPS: aRoleCaps } = window;
  const [selId, setSelId] = React.useState(null);
  const sel = aUsers.find((u) => u.id === selId) || null;

  // nb de capabilities effectives d'un utilisateur (rôle + overrides)
  const capCount = (u) => aCaps.filter((c) => aCapFromRole(u.role, c.id) || userPerms[u.id] && userPerms[u.id][c.id]).length;

  return (
    <div className="fade-in">
      <SysHeader icon="users" title="Utilisateurs & rôles"
      sub="Gérez les accès de l’équipe — rôle de base, puis permissions accordées individuellement.">
        <button className="btn accent"><YIcon n="plus" /> Inviter un utilisateur</button>
      </SysHeader>

      <div className="fgrid" style={{ gridTemplateColumns: 'repeat(2,1fr)', marginBottom: 18 }}>
        {Object.entries(aRoles).map(([k, r]) =>
        <div className="card" key={k} style={{ boxShadow: 'none', background: 'var(--surface-2)' }}>
            <div className="row-flex" style={{ justifyContent: 'space-between' }}>
              <span className={'role-badge role-' + r.cls}>
                {k === 'admin' && <YIcon n="shield" style={{ width: 11, height: 11 }} />}{r.label}
              </span>
              <span className="muted" style={{ fontSize: 12 }}>{(aRoleCaps[k] || []).length === aCaps.length ? 'toutes les permissions' : (aRoleCaps[k] || []).length + ' permissions de base'}</span>
            </div>
            <div className="muted" style={{ fontSize: 12.5, marginTop: 8, lineHeight: 1.5 }}>{r.desc}</div>
          </div>
        )}
      </div>

      <div className="team-wrap">
        <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
          <div className="tbl-wrap">
            <table>
              <thead><tr>
                <th>Membre</th><th>Rôle</th><th className="num">Accès</th><th>État</th><th style={{ width: 80 }}></th>
              </tr></thead>
              <tbody>
                {aUsers.map((u) =>
                <tr key={u.id} className={'team-row' + (u.id === selId ? ' on' : '')} onClick={() => setSelId(u.id)} style={{ cursor: 'pointer' }}>
                    <td><div className="row-flex">{AV && <AV user={u} size={32} />}
                      <div className="team-member">
                        <div className="team-member-name">{u.prenom} {u.nom} {u.id === user.id && <span className="muted">(vous)</span>}</div>
                        <div className="team-member-titre">{u.titre}</div>
                      </div></div></td>
                    <td><span className={'role-badge role-' + (aRoles[u.role] || {}).cls}>{aRoleLabel(u.role)}</span></td>
                    <td className="cell-ro">{capCount(u)}/{aCaps.length}</td>
                    <td>{u.actif ? <span className="pill done"><span className="d" />Actif</span> : <span className="pill none"><span className="d" />Inactif</span>}</td>
                    <td><button className="btn ghost" style={{ padding: '5px 10px' }} onClick={(e) => {e.stopPropagation();setSelId(u.id);}}>{u.id === selId ? 'Sélectionné' : 'Gérer'}</button></td>
                  </tr>
                )}
              </tbody>
            </table>
          </div>
        </div>

        {/* Zone grise : fiche de l'utilisateur sélectionné + tableau des permissions */}
        <div className="team-perms">
          {sel ?
          <>
              <div className="team-perms-head">
                {AV && <AV user={sel} size={44} />}
                <div className="team-perms-id">
                  <div className="team-perms-name">{sel.prenom} {sel.nom} {sel.id === user.id && <span className="muted">(vous)</span>}</div>
                  <div className="muted" style={{ fontSize: 12 }}>{sel.courriel} · {sel.titre}</div>
                </div>
                <div className="team-perms-role">
                  <label className="lbl">Rôle</label>
                  <select className="field" defaultValue={sel.role}
                    onChange={(e) => onLog && onLog('user', 'a changé le rôle', `${sel.prenom} ${sel.nom} → ${(aRoles[e.target.value] || {}).label || e.target.value}`)}>
                    {Object.entries(aRoles).map(([k, r]) => <option key={k} value={k}>{r.label}</option>)}
                  </select>
                </div>
              </div>

              <div className="team-perms-lbl">Permissions de {sel.prenom}</div>
              <div className="card" style={{ padding: 0, overflow: 'hidden' }}>
                <div className="tbl-wrap">
                  <table>
                    <thead><tr>
                      <th>Fonctionnalité</th><th>Description</th>
                      <th style={{ width: 110 }}>Source</th><th style={{ width: 90 }} className="num">Accès</th>
                    </tr></thead>
                    <tbody>
                      {aCaps.map((c) => {
                      const fromRole = aCapFromRole(sel.role, c.id);
                      const override = !!(userPerms[sel.id] && userPerms[sel.id][c.id]);
                      const on = fromRole || override;
                      return (
                        <tr key={c.id}>
                            <td><div className="row-flex">
                              <span className={'cap-ic' + (on ? ' on' : '')}><YIcon n={c.icon} /></span>
                              <b style={{ fontSize: 12.5 }}>{c.label}</b>
                            </div></td>
                            <td className="muted" style={{ fontSize: 12 }}>{c.desc}</td>
                            <td>{fromRole ?
                            <span className="src-badge src-role">Rôle</span> :
                            override ?
                            <span className="src-badge src-indiv">Individuel</span> :
                            <span className="muted" style={{ fontSize: 11 }}>—</span>}</td>
                            <td style={{ textAlign: 'right' }}>{fromRole ?
                            <span className="cap-inherit">Inclus</span> :
                            <button className={'cap-toggle' + (override ? ' on' : '')} role="switch" aria-checked={override}
                            onClick={() => onSetPerm && onSetPerm(sel.id, c.id, !override)}><span className="cap-knob" /></button>}</td>
                          </tr>);

                    })}
                    </tbody>
                  </table>
                </div>
              </div>
              <div className="note" style={{ marginTop: 12 }}>
                Accordez des fonctions précises sans changer le rôle. Les permissions <b>héritées du rôle</b> ne peuvent être retirées qu’en changeant le rôle.
              </div>
            </> :

          <div className="team-perms-empty">
              <YIcon n="users" style={{ width: 26, height: 26, opacity: .4 }} />
              <div>Sélectionnez un membre ci-dessus pour afficher et modifier ses permissions.</div>
            </div>
          }
        </div>
      </div>
    </div>);

}

// ═══════════════════════ ORGANISATION DES WIDGETS ═══════════════════════
// Admin : hiérarchie catégorie › sous-catégorie, et affectation des widgets.
function OrganisationModule({ org, onChange, onLog }) {
  // catégories pilotées par l'admin (base + créées) ; fallback robuste
  const cats = (org.cats && org.cats.length) ? org.cats : ORG_CATS;
  const baseCats = ORG_CATS;                    // catégories liées aux modules (non supprimables)
  const widgetCat = org.widgetCat || {};
  // catégorie effective d'un widget : override admin, sinon catégorie d'origine
  const catOf = (m) => widgetCat[m.id] || m.cat;
  const [cat, setCat] = React.useState(cats[0]);
  const [newSub, setNewSub] = React.useState('');
  const [newCat, setNewCat] = React.useState('');
  const [editing, setEditing] = React.useState(null);     // sous-cat {id, name}
  const [editingCat, setEditingCat] = React.useState(false); // renommage de la catégorie active
  const [catName, setCatName] = React.useState('');
  const [dragCat, setDragCat] = React.useState(null);     // catégorie en cours de glissement
  const [overCat, setOverCat] = React.useState(null);     // cible de dépôt survolée

  const subs = (org.subcats[cat] || []);
  const widgets = EST_MODS.filter((m) => catOf(m) === cat);
  const isBase = baseCats.includes(cat);

  const commit = (next) => onChange(next);

  // ── sous-catégories ──
  const addSub = () => {
    const name = newSub.trim(); if (!name) return;
    commit({ ...org, subcats: { ...org.subcats, [cat]: [...subs, { id: ORG_SLUG(cat), name }] } });
    setNewSub('');
  };
  const renameSub = (id, name) => commit({ ...org, subcats: { ...org.subcats, [cat]: subs.map((s) => s.id === id ? { ...s, name } : s) } });
  const delSub = (id) => {
    const assign = { ...org.assign };
    Object.keys(assign).forEach((k) => {if (assign[k] === id) delete assign[k];});
    commit({ ...org, assign, subcats: { ...org.subcats, [cat]: subs.filter((s) => s.id !== id) } });
  };

  // ── catégories de 1er niveau ──
  const addCat = () => {
    const name = newCat.trim(); if (!name || cats.includes(name)) { setNewCat(''); return; }
    commit({ ...org, cats: [...cats, name], subcats: { ...org.subcats, [name]: [] } });
    setNewCat(''); setCat(name);
    onLog && onLog('edit', 'a créé une catégorie', name);
  };
  const renameCat = (name) => {
    name = (name || '').trim();
    if (!name || name === cat || cats.includes(name)) { setEditingCat(false); return; }
    const nextCats = cats.map((c) => c === cat ? name : c);
    const subcats = { ...org.subcats }; subcats[name] = subcats[cat] || []; delete subcats[cat];
    const wc = { ...widgetCat }; Object.keys(wc).forEach((k) => {if (wc[k] === cat) wc[k] = name;});
    commit({ ...org, cats: nextCats, subcats, widgetCat: wc });
    setCat(name); setEditingCat(false);
  };
  // ── réordonnancement par glisser-déposer ──
  const reorderCats = (from, to) => {
    if (from === to) return;
    const order = [...cats];
    const fi = order.indexOf(from), ti = order.indexOf(to);
    if (fi < 0 || ti < 0) return;
    order.splice(fi, 1);
    order.splice(ti, 0, from);
    commit({ ...org, cats: order });
    onLog && onLog('edit', 'a réordonné les catégories', from);
  };
  const delCat = () => {
    if (isBase) return; // catégorie liée à des modules → non supprimable
    const subIds = new Set((org.subcats[cat] || []).map((s) => s.id));
    const assign = { ...org.assign }; Object.keys(assign).forEach((k) => {if (subIds.has(assign[k])) delete assign[k];});
    const wc = { ...widgetCat }; Object.keys(wc).forEach((k) => {if (wc[k] === cat) delete wc[k];}); // widgets reviennent à leur catégorie d'origine
    const subcats = { ...org.subcats }; delete subcats[cat];
    commit({ ...org, cats: cats.filter((c) => c !== cat), subcats, assign, widgetCat: wc });
    setCat(baseCats[0]);
  };

  // ── widgets ──
  const assignWidget = (mid, sid) => {
    const assign = { ...org.assign };
    if (sid) assign[mid] = sid; else delete assign[mid];
    commit({ ...org, assign });
  };
  const moveWidget = (mid, toCat) => {
    const wc = { ...widgetCat };
    const m = EST_MODS.find((x) => x.id === mid);
    if (toCat === (m && m.cat)) delete wc[mid]; else wc[mid] = toCat;
    const assign = { ...org.assign }; delete assign[mid]; // sous-cat invalide dans la nouvelle catégorie
    commit({ ...org, widgetCat: wc, assign });
  };
  const hidden = org.hidden || {};
  const toggleHidden = (mid, on) => {
    const h = { ...hidden };
    if (on) delete h[mid]; else h[mid] = true;   // on = visible
    commit({ ...org, hidden: h });
    const w = EST_MODS.find((x) => x.id === mid);
    onLog && onLog('edit', on ? 'a activé un widget' : 'a masqué un widget', w ? w.name : mid);
  };
  const hiddenCount = EST_MODS.filter((m) => hidden[m.id]).length;

  const countIn = (sid) => widgets.filter((w) => org.assign[w.id] === sid).length;
  const unclassed = widgets.filter((w) => !org.assign[w.id] || !subs.some((s) => s.id === org.assign[w.id]));

  return (
    <div className="fade-in">
      <SysHeader icon="folderPlus" title="Organisation des widgets"
        sub="Hiérarchie catégorie › sous-catégorie. Créez, organisez, déplacez et activez/désactivez les widgets visibles.">
        {hiddenCount > 0 && <span className="pill none" style={{ marginRight: 4 }}><span className="d" /> {hiddenCount} masqué{hiddenCount > 1 ? 's' : ''}</span>}
        <button className="btn" onClick={() => { commit({ cats: [...DEF_ORG.cats], widgetCat: {}, subcats: { ...DEF_ORG.subcats }, assign: { ...DEF_ORG.assign }, hidden: {} }); setCat(DEF_ORG.cats[0]); }}>
          <YIcon n="history" /> Réinitialiser
        </button>
      </SysHeader>

      <div className="org-cats">
        {cats.map((c) => {
          const n = (org.subcats[c] || []).length;
          const custom = !baseCats.includes(c);
          return (
            <button key={c}
              className={'org-cat' + (c === cat ? ' on' : '') + (dragCat === c ? ' dragging' : '') + (overCat === c && dragCat !== c ? ' dragover' : '')}
              draggable
              onDragStart={(e) => { setDragCat(c); e.dataTransfer.effectAllowed = 'move'; }}
              onDragOver={(e) => { e.preventDefault(); if (overCat !== c) setOverCat(c); }}
              onDragEnd={() => { setDragCat(null); setOverCat(null); }}
              onDrop={(e) => { e.preventDefault(); if (dragCat) reorderCats(dragCat, c); setDragCat(null); setOverCat(null); }}
              onClick={() => { setCat(c); setEditing(null); setEditingCat(false); }}>
              <YIcon n="gripV" style={{ width: 13, height: 13, opacity: .4, cursor: 'grab' }} />
              <YIcon n={custom ? 'folderPlus' : 'folders'} style={{ width: 15, height: 15 }} />
              <span>{c}</span>
              <em>{n}</em>
            </button>
          );
        })}
      </div>

      <div className="grid-2">
        <div className="col">
          <div className="card">
            <div className="card-h">
              <div className="card-h-l"><span className="badge-n">1</span>
                {editingCat ?
                  <input className="cell-in" autoFocus defaultValue={cat} style={{ fontWeight: 600, maxWidth: 220 }}
                    onBlur={(e) => renameCat(e.target.value)}
                    onKeyDown={(e) => { if (e.key === 'Enter') renameCat(e.target.value); if (e.key === 'Escape') setEditingCat(false); }} /> :
                  <span className="card-t">Catégorie « {cat} »</span>}
              </div>
              <div className="row-flex">
                <button className="row-x" title="Renommer la catégorie" onClick={() => setEditingCat(true)}><YIcon n="pencil" /></button>
                {!isBase && <button className="row-x" title="Supprimer la catégorie" onClick={delCat}><YIcon n="trash" /></button>}
              </div>
            </div>
            <div className="note" style={{ marginBottom: 14 }}>
              {isBase ?
                <>Catégorie de base (liée aux modules). Renommable ; les widgets peuvent en sortir, mais elle ne se supprime pas.</> :
                <>Catégorie créée par l’admin. Renommable et supprimable — ses widgets reviennent alors à leur catégorie d’origine.</>}
            </div>

            <div className="card-t" style={{ fontSize: 13, marginBottom: 10 }}>Sous-catégories <span className="muted" style={{ fontWeight: 400 }}>· {subs.length}</span></div>
            {subs.length === 0 &&
              <div className="note" style={{ marginBottom: 12 }}>Aucune sous-catégorie. Ajoutez-en une ci-dessous (ex. « Toiture »).</div>}
            <div className="org-sublist">
              {subs.map((s) =>
                <div className="org-sub" key={s.id}>
                  <span className="grip"><YIcon n="gripV" style={{ width: 14, height: 14 }} /></span>
                  {editing && editing.id === s.id ?
                    <input className="cell-in" autoFocus value={editing.name}
                      onChange={(e) => setEditing({ id: s.id, name: e.target.value })}
                      onBlur={() => { renameSub(s.id, editing.name.trim() || s.name); setEditing(null); }}
                      onKeyDown={(e) => { if (e.key === 'Enter') { renameSub(s.id, editing.name.trim() || s.name); setEditing(null); } }} /> :
                    <span className="org-sub-name">{s.name}</span>}
                  <span className="org-sub-ct">{countIn(s.id)} widget{countIn(s.id) > 1 ? 's' : ''}</span>
                  <button className="row-x" title="Renommer" onClick={() => setEditing({ id: s.id, name: s.name })}><YIcon n="pencil" /></button>
                  <button className="row-x" title="Supprimer" onClick={() => delSub(s.id)}><YIcon n="trash" /></button>
                </div>
              )}
            </div>
            <div className="org-add">
              <input className="field" placeholder="Nouvelle sous-catégorie…" value={newSub}
                onChange={(e) => setNewSub(e.target.value)} onKeyDown={(e) => {if (e.key === 'Enter') addSub();}} />
              <button className="btn accent" onClick={addSub}><YIcon n="folderPlus" /> Créer</button>
            </div>
          </div>

          <div className="card">
            <div className="card-h">
              <div className="card-h-l"><span className="badge-n">2</span><span className="card-t">Widgets de « {cat} »</span></div>
              <span className="card-hint">activez/masquez, déplacez, puis rangez en sous-catégorie</span>
            </div>
            <div className="tbl-wrap">
              <table>
                <thead><tr>
                  <th>Widget</th><th style={{ width: 150 }}>Catégorie</th><th style={{ width: 156 }}>Sous-catégorie</th><th style={{ width: 78 }} className="num">Visible</th>
                </tr></thead>
                <tbody>
                  {widgets.map((w) => {
                    const vis = !hidden[w.id];
                    return (
                    <tr key={w.id} className={vis ? '' : 'org-row-off'}>
                      <td><div className="org-wname"><span className="org-wic"><YIcon n={w.icon} style={{ width: 15, height: 15 }} /></span>{w.name}{!vis && <span className="org-off-tag">Masqué</span>}</div></td>
                      <td>
                        <select className="cell-in" value={cat} onChange={(e) => moveWidget(w.id, e.target.value)}>
                          {cats.map((c) => <option key={c} value={c}>{c}</option>)}
                        </select>
                      </td>
                      <td>
                        <select className="cell-in" value={org.assign[w.id] && subs.some((s) => s.id === org.assign[w.id]) ? org.assign[w.id] : ''}
                          onChange={(e) => assignWidget(w.id, e.target.value)} disabled={subs.length === 0}>
                          <option value="">— Non classé —</option>
                          {subs.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
                        </select>
                      </td>
                      <td style={{ textAlign: 'right' }}>
                        <button className={'cap-toggle' + (vis ? ' on' : '')} role="switch" aria-checked={vis}
                          title={vis ? 'Visible — cliquer pour masquer' : 'Masqué — cliquer pour activer'}
                          onClick={() => toggleHidden(w.id, !vis)}><span className="cap-knob" /></button>
                      </td>
                    </tr>);
                  })}
                </tbody>
              </table>
            </div>
            {widgets.length === 0 && <div className="note" style={{ marginTop: 10 }}>Aucun widget dans cette catégorie. Déplacez-en depuis une autre catégorie.</div>}
          </div>
        </div>

        <aside className="aside">
          <div className="card">
            <div className="sum-title">Aperçu de la hiérarchie</div>
            <div className="org-tree">
              <div className="org-tree-cat"><YIcon n={isBase ? 'folders' : 'folderPlus'} style={{ width: 14, height: 14 }} /> {cat}</div>
              {subs.map((s) =>
                <div className="org-tree-sub" key={s.id}>
                  <YIcon n="folderPlus" style={{ width: 13, height: 13 }} /> {s.name}
                  {widgets.filter((w) => org.assign[w.id] === s.id).map((w) =>
                    <div className="org-tree-w" key={w.id}><YIcon n={w.icon} style={{ width: 12, height: 12 }} /> {w.name}</div>
                  )}
                </div>
              )}
              {unclassed.length > 0 &&
                <div className="org-tree-sub muted">
                  <YIcon n="folderPlus" style={{ width: 13, height: 13 }} /> Non classé
                  {unclassed.map((w) =>
                    <div className="org-tree-w" key={w.id}><YIcon n={w.icon} style={{ width: 12, height: 12 }} /> {w.name}</div>
                  )}
                </div>}
            </div>
            <div className="note" style={{ marginTop: 12 }}>Cette hiérarchie structure le tableau de bord, la barre latérale et les vues de catégorie. Glissez les catégories pour les réordonner.</div>
          </div>

          <div className="card">
            <div className="card-t" style={{ fontSize: 13, marginBottom: 4 }}>Nouvelle catégorie</div>
            <div className="note" style={{ marginBottom: 10 }}>Créez une catégorie de 1<sup>er</sup> niveau, puis déplacez-y des widgets.</div>
            <div className="org-newcat">
              <input className="field" placeholder="Nom de la catégorie…" value={newCat}
                onChange={(e) => setNewCat(e.target.value)} onKeyDown={(e) => {if (e.key === 'Enter') addCat();}} />
              <button className="btn accent" onClick={addCat}><YIcon n="plus" /> Catégorie</button>
            </div>
          </div>
        </aside>
      </div>
    </div>
  );
}

// ═══════════════════════ SYSTÈMES DE PRODUITS ═══════════════════════
// Admin : créer/modifier un système (nom, marque, couleur), l'assigner aux
// widgets. Source de vérité unique des produits — synchronisable avec Odoo.
const SYS_COLORS = ['#1f8a5b', '#2f6bed', '#7b59d6', '#c2603a', '#d24b3e', '#1455a8', '#9a6a2c', '#6a8a3a', '#0e7c86'];
const monoFrom = (name) => (name || '?').split(/\s+/).map((w) => w[0]).join('').slice(0, 2).toUpperCase();

function SystemesModule({ systems = [], onChange, onLog }) {
  const list = systems.length ? systems : PSYS;
  const [selId, setSelId] = React.useState(list[0] && list[0].id);
  const [creating, setCreating] = React.useState(false);
  const [draft, setDraft] = React.useState({ name: '', brand: '', accent: SYS_COLORS[0], tagline: '' });
  const sel = creating ? null : (list.find((s) => s.id === selId) || list[0]);

  const commit = (next) => onChange && onChange(next);
  const update = (id, patch) => commit(list.map((s) => s.id === id ? { ...s, ...patch } : s));

  const createSystem = () => {
    const name = draft.name.trim(); if (!name) return;
    const id = 'sys-' + name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '') + '-' + Math.random().toString(36).slice(2, 5);
    const sys = { id, name, brand: draft.brand.trim() || name, mono: monoFrom(name), accent: draft.accent, tagline: draft.tagline.trim() || 'Système de produits', picks: {}, odooId: null };
    commit([...list, sys]);
    setSelId(id); setCreating(false); setDraft({ name: '', brand: '', accent: SYS_COLORS[0], tagline: '' });
    onLog && onLog('recipe', 'a créé un système de produits', name);
  };
  const delSystem = (id) => {
    const s = list.find((x) => x.id === id);
    const next = list.filter((x) => x.id !== id);
    commit(next); setSelId(next[0] && next[0].id);
    onLog && onLog('recipe', 'a supprimé un système de produits', s ? s.name : id);
  };
  // assigne / retire le système à un widget (module) avec sa recette
  const toggleWidget = (mid, on) => {
    const picks = { ...sel.picks };
    if (on) { const r = rcpFor(mid)[0]; picks[mid] = { recipe: r ? r.id : null, brand: sel.brand }; }
    else delete picks[mid];
    update(sel.id, { picks });
  };
  const setWidgetRecipe = (mid, rid) => update(sel.id, { picks: { ...sel.picks, [mid]: { ...sel.picks[mid], recipe: rid } } });

  const covered = sel ? Object.keys(sel.picks).length : 0;

  return (
    <div className="fade-in">
      <SysHeader icon="box" title="Systèmes de produits"
        sub="Créez et gérez les approches fabricant. Un système définit nom, marque et couleur, et s’assigne aux widgets — propagé aux recettes et soumissions.">
        <button className="btn accent" onClick={() => { setCreating(true); setSelId(null); }}><YIcon n="plus" /> Nouveau système</button>
      </SysHeader>

      <div className="note" style={{ marginBottom: 14, display: 'flex', alignItems: 'center', gap: 9 }}>
        <YIcon n="cloud" style={{ width: 16, height: 16, flexShrink: 0, color: '#714B67' }} />
        <span><b>Source de vérité unique.</b> Les produits et marques proviennent d’Odoo. Un système lié à Odoo hérite de sa fiche produit ; les systèmes locaux pourront y être poussés pour éviter toute duplication.</span>
      </div>

      <div className="grid-2">
        <div className="col">
          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">1</span><span className="card-t">Systèmes</span></div>
              <span className="card-hint">{list.length} systèmes</span></div>
            <div className="sys-list">
              {list.map((s) => {
                const n = Object.keys(s.picks).length;
                return (
                  <button key={s.id} className={'sys-listitem' + (s.id === selId && !creating ? ' on' : '')} style={{ '--sys': s.accent }}
                    onClick={() => { setSelId(s.id); setCreating(false); }}>
                    <span className="sysmono">{s.mono}</span>
                    <span className="systx">
                      <span className="sysname">{s.name}</span>
                      <span className="systag">{s.brand} · {n} widget{n > 1 ? 's' : ''}</span>
                    </span>
                    {s.odooId ?
                      <span className="sys-odoo linked" title={'Odoo #' + s.odooId}><YIcon n="check" style={{ width: 11, height: 11 }} /> Odoo</span> :
                      <span className="sys-odoo local">Local</span>}
                  </button>);
              })}
            </div>
          </div>
        </div>

        <aside className="aside">
          {creating ?
            <div className="card">
              <div className="card-h"><div className="card-h-l"><span className="badge-n">+</span><span className="card-t">Nouveau système</span></div></div>
              <div className="fgrid">
                <div className="fcell full"><label className="lbl">Nom du système</label>
                  <input className="field" autoFocus value={draft.name} placeholder="ex. Pro Clima" onChange={(e) => setDraft({ ...draft, name: e.target.value })} /></div>
                <div className="fcell full"><label className="lbl">Marque par défaut</label>
                  <input className="field" value={draft.brand} placeholder="ex. Pro Clima" onChange={(e) => setDraft({ ...draft, brand: e.target.value })} /></div>
                <div className="fcell full"><label className="lbl">Description courte</label>
                  <input className="field" value={draft.tagline} placeholder="ex. Étanchéité à l’air" onChange={(e) => setDraft({ ...draft, tagline: e.target.value })} /></div>
                <div className="fcell full"><label className="lbl">Couleur</label>
                  <div className="sys-swatches">
                    {SYS_COLORS.map((c) => <button key={c} className={'sys-sw' + (draft.accent === c ? ' on' : '')} style={{ background: c }} onClick={() => setDraft({ ...draft, accent: c })} />)}
                  </div></div>
              </div>
              <div className="wrap-flex" style={{ marginTop: 14 }}>
                <button className="btn accent" onClick={createSystem}><YIcon n="check" /> Créer le système</button>
                <button className="btn" onClick={() => setCreating(false)}>Annuler</button>
              </div>
            </div> :
          sel ?
            <div className="card">
              <div className="card-h"><div className="card-h-l"><span className="sysmono" style={{ '--sys': sel.accent, background: sel.accent }}>{sel.mono}</span><span className="card-t">{sel.name}</span></div>
                <button className="row-x" title="Supprimer" onClick={() => delSystem(sel.id)}><YIcon n="trash" /></button></div>
              <div className="fgrid">
                <div className="fcell"><label className="lbl">Nom</label>
                  <input className="field" value={sel.name || ''} onChange={(e) => update(sel.id, { name: e.target.value, mono: monoFrom(e.target.value) })} /></div>
                <div className="fcell"><label className="lbl">Marque</label>
                  <input className="field" value={sel.brand || ''} onChange={(e) => update(sel.id, { brand: e.target.value })} /></div>
                <div className="fcell full"><label className="lbl">Description</label>
                  <input className="field" value={sel.tagline || ''} onChange={(e) => update(sel.id, { tagline: e.target.value })} /></div>
                <div className="fcell full"><label className="lbl">Couleur</label>
                  <div className="sys-swatches">
                    {SYS_COLORS.map((c) => <button key={c} className={'sys-sw' + (sel.accent === c ? ' on' : '')} style={{ background: c }} onClick={() => update(sel.id, { accent: c })} />)}
                  </div></div>
              </div>

              <div className="sys-odoo-bar">
                <span className={'odoo-dot ' + (sel.odooId ? 'ok' : 'warn')} />
                {sel.odooId ?
                  <span>Lié à Odoo <b>#{sel.odooId}</b> — fiche produit synchronisée</span> :
                  <span>Système local — non encore dans Odoo</span>}
                {sel.odooId ?
                  <button className="btn" onClick={() => update(sel.id, { odooId: null })}><YIcon n="plug" /> Délier</button> :
                  <button className="btn accent" onClick={() => { update(sel.id, { odooId: Math.floor(7000 + Math.random() * 999) }); onLog && onLog('recipe', 'a lié un système à Odoo', sel.name); }}><YIcon n="cloud" /> Lier à Odoo</button>}
              </div>

              <div className="card-t" style={{ fontSize: 13, margin: '16px 0 8px' }}>Widgets assignés <span className="muted" style={{ fontWeight: 400 }}>· {covered}</span></div>
              <div className="sys-assign">
                {EST_MODS.map((m) => {
                  const on = !!sel.picks[m.id];
                  const recs = rcpFor(m.id);
                  return (
                    <div className={'sys-arow' + (on ? ' on' : '')} key={m.id}>
                      <button className={'cap-toggle' + (on ? ' on' : '')} role="switch" aria-checked={on} onClick={() => toggleWidget(m.id, !on)}><span className="cap-knob" /></button>
                      <span className="sys-arow-ic"><YIcon n={m.icon} style={{ width: 15, height: 15 }} /></span>
                      <span className="sys-arow-name">{m.name}</span>
                      {on && recs.length > 0 ?
                        <select className="cell-in" style={{ maxWidth: 180 }} value={sel.picks[m.id].recipe || ''} onChange={(e) => setWidgetRecipe(m.id, e.target.value)}>
                          {recs.map((r) => <option key={r.id} value={r.id}>{r.name}</option>)}
                        </select> :
                        on ? <span className="muted" style={{ fontSize: 11 }}>aucune recette</span> : null}
                    </div>);
                })}
              </div>
            </div> :
            <div className="card"><div className="empty"><YIcon n="box" /><div className="empty-t">Aucun système</div><div className="empty-s">Créez votre premier système de produits.</div></div></div>}
        </aside>
      </div>
    </div>
  );
}

// ═══════════════════════ SERVICES & INTÉGRATIONS ═══════════════════════
// Admin : tableau de bord des services (façades de données). Statut,
// santé, dernière synchro, et configuration de l'intégration Odoo.
const SVC_HEALTH = {
  ok: { cls: 'ok', label: 'Opérationnel' },
  degraded: { cls: 'warn', label: 'Dégradé' },
  down: { cls: 'bad', label: 'Hors service' },
};
const SVC_KIND = { integration: 'Intégration', data: 'Source de données' };
const fmtSync = (iso) => {
  if (!iso) return '—';
  const d = new Date(iso), now = Date.now(), diff = (now - d.getTime()) / 1000;
  if (diff < 60) return 'à l’instant';
  if (diff < 3600) return `il y a ${Math.floor(diff / 60)} min`;
  if (diff < 86400) return `il y a ${Math.floor(diff / 3600)} h`;
  return d.toLocaleDateString('fr-CA');
};

// Catalogue d'intégrations externes (centralisé). La connexion Odoo réelle
// est pilotée par le service ; les autres sont des fiches de configuration.
const INTEGRATION_DEFS = [
  { id: 'odoo', name: 'Odoo ERP', cat: 'ERP · Produits', icon: 'cloud', live: true,
    desc: 'Source de vérité des produits, marques et clients.',
    fields: [
      { k: 'Serveur', v: 'go-passif.odoo.com' },
      { k: 'Base de données', v: 'gopassif_prod' },
      { k: 'Utilisateur API', v: 'api@gopassif.app' },
      { k: 'Clé API', v: 'odoo_sk_8f3a2c91d7', secret: true },
    ] },
  { id: 'cloud', name: 'Stockage Cloud', cat: 'Cloud · Fichiers', icon: 'cloud', enabled: true,
    desc: 'Dépôt des plans téléversés et des PDF de soumission.',
    fields: [
      { k: 'Fournisseur', v: 'AWS S3' },
      { k: 'Bucket', v: 'gopassif-plans' },
      { k: 'Région', v: 'ca-central-1' },
      { k: 'Clé d’accès', v: 'AKIA4GP7XR2', secret: true },
      { k: 'Clé secrète', v: 'wJalrXUtnFEMI', secret: true },
    ] },
  { id: 'cloudflare', name: 'Cloudflare', cat: 'Réseau · DNS', icon: 'globe', enabled: true,
    desc: 'Domaine, CDN et tunnel d’accès sécurisé de la plateforme.',
    fields: [
      { k: 'Zone', v: 'gopassif.app' },
      { k: 'Account ID', v: 'a1b2c3d4e5f6' },
      { k: 'Jeton API', v: 'cf_tok_2Hk9Lp', secret: true },
      { k: 'Tunnel', v: 'estimateur-prod' },
    ] },
  { id: 'agents', name: 'Agents UX (IA)', cat: 'IA · Assistants', icon: 'robot', enabled: true,
    desc: 'Assistant de mesure sur plan et détection automatique.',
    fields: [
      { k: 'Fournisseur', v: 'Claude API' },
      { k: 'Modèle', v: 'claude-sonnet-4' },
      { k: 'Clé API', v: 'sk-ant-7Fp2Qx', secret: true },
      { k: 'Quota', v: '50 000 jetons / jour' },
    ] },
];

function ServicesModule({ onLog }) {
  const svc = window.GPServices;
  const [, force] = React.useReducer((x) => x + 1, 0);
  const [busy, setBusy] = React.useState(false);
  const [openId, setOpenId] = React.useState('odoo');
  const [reveal, setReveal] = React.useState({});       // secrets dévoilés
  const [enabled, setEnabled] = React.useState(() => {  // état activé par intégration
    const o = {}; INTEGRATION_DEFS.forEach((i) => { o[i.id] = i.enabled !== false; }); return o;
  });
  React.useEffect(() => {
    if (!svc) return;
    const off1 = svc.bus.on('odoo:status', force);
    const off2 = svc.bus.on('catalog:changed', force);
    return () => { off1(); off2(); };
  }, [svc]);

  if (!svc) return <div className="card"><div className="empty"><YIcon n="plug" /><div className="empty-t">Services indisponibles</div></div></div>;

  const services = svc.all();
  const odoo = svc.odoo.describe();
  const odooOnline = odoo.status === 'connected';

  const syncOdoo = async () => {
    setBusy(true); const ok = await svc.odoo.sync(); setBusy(false);
    onLog && onLog('edit', ok ? 'a synchronisé Odoo' : 'échec de synchro Odoo', svc.odoo.conn.server);
  };
  const toggleOdoo = () => svc.odoo.setOnline(!odooOnline);
  const setEnab = (id, v) => { setEnabled((e) => ({ ...e, [id]: v })); onLog && onLog('edit', v ? 'a activé une intégration' : 'a désactivé une intégration', id); };

  // statut affiché par intégration
  const statusOf = (id) => {
    if (id === 'odoo') return odooOnline ? 'connected' : 'offline';
    return enabled[id] ? 'connected' : 'disabled';
  };
  const STAT = { connected: { cls: 'ok', label: 'Connecté' }, offline: { cls: 'bad', label: 'Hors ligne' }, disabled: { cls: 'off', label: 'Désactivé' } };

  return (
    <div className="fade-in">
      <SysHeader icon="plug" title="Connexions & intégrations"
        sub="Centre unique des connexions techniques : Odoo, Cloud, Cloudflare, agents IA et clés d’API de la plateforme.">
        <button className="btn" onClick={() => onLog && onLog('edit', 'a exporté la config des intégrations')}><YIcon n="download" /> Exporter config</button>
      </SysHeader>

      <div className="note" style={{ marginBottom: 14, display: 'flex', alignItems: 'center', gap: 9 }}>
        <YIcon n="users" style={{ width: 16, height: 16, flexShrink: 0, color: 'var(--accent)' }} />
        <span><b>Accès réservé aux administrateurs.</b> Cette section regroupe identifiants et paramètres techniques ; elle n’est visible qu’avec la permission <b>« Connexions & intégrations »</b>.</span>
      </div>

      <div className="grid-2">
        <div className="col">
          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">1</span><span className="card-t">Intégrations externes</span></div>
              <span className="card-hint">{INTEGRATION_DEFS.length} connexions</span></div>
            <div className="intg-list">
              {INTEGRATION_DEFS.map((it) => {
                const open = openId === it.id;
                const st = STAT[statusOf(it.id)];
                return (
                  <div className={'intg' + (open ? ' open' : '')} key={it.id}>
                    <button className="intg-head" onClick={() => setOpenId(open ? null : it.id)}>
                      <span className="intg-ic"><YIcon n={it.icon} style={{ width: 18, height: 18 }} /></span>
                      <div className="intg-tx">
                        <div className="intg-name">{it.name} <span className="intg-cat">{it.cat}</span></div>
                        <div className="intg-desc">{it.desc}</div>
                      </div>
                      <span className={'intg-stat ' + st.cls}><span className="d" /> {st.label}</span>
                      <YIcon n={open ? 'chevd' : 'chev'} style={{ width: 16, height: 16, opacity: .5 }} />
                    </button>
                    {open &&
                    <div className="intg-body">
                      <div className="intg-fields">
                        {it.fields.map((f, i) => {
                          const rk = it.id + i;
                          const shown = reveal[rk];
                          return (
                            <div className="intg-field" key={i}>
                              <label className="lbl">{f.k}</label>
                              <div className="intg-input">
                                {f.secret ?
                                  <input className="field mono" value={shown ? f.v : '••••••••••••'} readOnly /> :
                                  <input className="field mono" defaultValue={f.v} />}
                                {f.secret &&
                                  <button className="intg-eye" title={shown ? 'Masquer' : 'Afficher'} onClick={() => setReveal((r) => ({ ...r, [rk]: !shown }))}><YIcon n="eye" style={{ width: 15, height: 15 }} /></button>}
                              </div>
                            </div>);
                        })}
                      </div>
                      <div className="intg-actions">
                        {it.id === 'odoo' ?
                          <>
                            <button className="btn accent" disabled={busy || !odooOnline} onClick={syncOdoo}><YIcon n="sync" /> {busy ? 'Synchronisation…' : 'Synchroniser'}</button>
                            <button className="btn" onClick={toggleOdoo}><YIcon n="plug" /> {odooOnline ? 'Simuler une panne' : 'Reconnecter'}</button>
                            <span className="intg-meta">maj {fmtSync(odoo.lastSync)}</span>
                          </> :
                          <>
                            <button className="btn" onClick={() => onLog && onLog('edit', 'a testé une connexion', it.name)}><YIcon n="sync" /> Tester la connexion</button>
                            <div className="intg-enable">
                              <span>{enabled[it.id] ? 'Activée' : 'Désactivée'}</span>
                              <button className={'cap-toggle' + (enabled[it.id] ? ' on' : '')} role="switch" aria-checked={enabled[it.id]} onClick={() => setEnab(it.id, !enabled[it.id])}><span className="cap-knob" /></button>
                            </div>
                          </>}
                      </div>
                    </div>}
                  </div>);
              })}
            </div>
            <button className="addrow" style={{ marginTop: 12 }} onClick={() => onLog && onLog('edit', 'a ouvert l’ajout d’intégration')}><YIcon n="plus" /> Ajouter une intégration</button>
          </div>

          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">2</span><span className="card-t">Flux de données</span></div></div>
            <div className="svc-flow">
              <span className="svc-flow-node odoo">Odoo<br /><em>product.template</em></span>
              <YIcon n="chev" style={{ width: 16, height: 16, opacity: .4 }} />
              <span className="svc-flow-node">catalogService<br /><em>cache + sync</em></span>
              <YIcon n="chev" style={{ width: 16, height: 16, opacity: .4 }} />
              <span className="svc-flow-node">Systèmes &amp; recettes</span>
              <YIcon n="chev" style={{ width: 16, height: 16, opacity: .4 }} />
              <span className="svc-flow-node">Widgets &amp; soumission</span>
            </div>
            <div className="note" style={{ marginTop: 12 }}>Aucun widget n’appelle une intégration directement. Le <b>catalogService</b> est l’unique point d’accès aux produits et marques — une seule source de vérité, zéro duplication.</div>
          </div>
        </div>

        <aside className="aside">
          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">3</span><span className="card-t">Santé des services</span></div></div>
            <div className="svc-list">
              {services.map((s) => {
                const d = s.describe();
                const h = SVC_HEALTH[d.health] || SVC_HEALTH.ok;
                return (
                  <div className={'svc-row ' + h.cls} key={s.id}>
                    <span className="svc-ic"><YIcon n={s.kind === 'integration' ? 'plug' : 'box'} style={{ width: 16, height: 16 }} /></span>
                    <div className="svc-tx">
                      <div className="svc-name">{s.label}</div>
                      <div className="svc-detail">{d.detail}</div>
                    </div>
                    <span className={'svc-health ' + h.cls}><span className="d" /> {h.label}</span>
                  </div>);
              })}
            </div>
            <div className="note" style={{ marginTop: 12 }}>Identifiants et jetons chiffrés côté serveur dans la version connectée (OAuth / coffre de secrets).</div>
          </div>
        </aside>
      </div>
    </div>
  );
}

// ═══════════════════════ COURRIELS (super-admin) ═══════════════════════
// Éditeur « champs guidés » du courriel d'onboarding. Charge/sauvegarde via
// l'API Worker (super-admin only) ; aperçu live rendu par le Worker.
const COURRIEL_FIELDS = [
  { key: 'subject', label: 'Objet du courriel', type: 'text' },
  { key: 'brandName', label: 'Bandeau (nom de marque)', type: 'text' },
  { key: 'heading', label: 'Titre', type: 'text' },
  { key: 'intro', label: 'Texte d\'introduction', type: 'area' },
  { key: 'buttonLabel', label: 'Libellé du bouton', type: 'text' },
  { key: 'accent', label: 'Couleur du bouton', type: 'color' },
  { key: 'footer', label: 'Pied de page', type: 'text' },
];
function CourrielsModule({ user, onLog }) {
  const AUTH = window.GPTOAuth;
  const [fields, setFields] = React.useState(null);
  const [defaults, setDefaults] = React.useState({});
  const [html, setHtml] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [msg, setMsg] = React.useState(null);
  const isSuper = user && user.role === 'super_admin';

  // Chargement initial
  React.useEffect(() => {
    if (!AUTH || !AUTH.enabled || !isSuper) return;
    AUTH.call('/api/auth/email-template', null, 'GET').then(({ ok, data }) => {
      if (ok) { setFields(data.fields); setDefaults(data.defaults); }
      else setMsg({ type: 'error', text: data.error || 'Chargement impossible.' });
    });
  }, []);

  // Aperçu live (débounce) — rendu par le Worker pour être fidèle à l'envoi réel
  React.useEffect(() => {
    if (!fields) return;
    const t = setTimeout(() => {
      AUTH.call('/api/auth/email-template/preview', { kind: 'verify', fields }).then(({ ok, data }) => { if (ok) setHtml(data.html); });
    }, 400);
    return () => clearTimeout(t);
  }, [fields]);

  if (!AUTH || !AUTH.enabled)
    return <div className="fade-in"><SysHeader icon="mail" title="Courriels" sub="Personnalisation des courriels transactionnels." />
      <div className="note">Disponible en production (nécessite l'API d'authentification). En mode démo local, cet écran est inactif.</div></div>;
  if (!isSuper)
    return <div className="fade-in"><SysHeader icon="mail" title="Courriels" />
      <div className="empty tickbox" style={{ maxWidth: 460, margin: '40px auto' }}><div className="empty-t">Réservé au super-administrateur</div><div className="empty-s">Seul le compte fondateur peut modifier les gabarits de courriel.</div></div></div>;

  const set = (k) => (e) => setFields((f) => ({ ...f, [k]: e.target.value }));
  const save = () => {
    setBusy(true); setMsg(null);
    AUTH.call('/api/auth/email-template/save', { kind: 'verify', fields }).then(({ ok, data }) => {
      if (ok) { setFields(data.fields); setMsg({ type: 'info', text: 'Gabarit enregistré — appliqué aux prochains envois.' }); onLog && onLog('recipe', 'a modifié le gabarit de courriel'); }
      else setMsg({ type: 'error', text: data.error || 'Enregistrement impossible.' });
    }).finally(() => setBusy(false));
  };
  const reset = () => setFields({ ...defaults });

  return (
    <div className="fade-in">
      <SysHeader icon="mail" title="Courriels" sub="Personnalisez l'apparence du courriel d'activation envoyé aux nouveaux comptes.">
        <button className="btn" onClick={reset}><YIcon n="history" /> Réinitialiser</button>
        <button className="btn accent" onClick={save} disabled={busy || !fields}><YIcon n="check" /> {busy ? 'Enregistrement…' : 'Enregistrer'}</button>
      </SysHeader>
      {msg && <div className={'login-msg ' + msg.type} style={{ marginBottom: 14 }}>{msg.text}</div>}

      <div className="grid-2">
        <div className="col">
          <div className="card">
            <div className="card-h"><div className="card-h-l"><span className="badge-n">1</span><span className="card-t">Contenu du courriel</span></div>
              <span className="card-hint">courriel de vérification d'adresse</span></div>
            {!fields && <div className="note">Chargement…</div>}
            {fields && <div className="fgrid">
              {COURRIEL_FIELDS.map((f) => (
                <div className={'fcell' + (f.type === 'area' ? ' full' : '')} key={f.key}>
                  <label className="lbl">{f.label}</label>
                  {f.type === 'area'
                    ? <textarea className="field" rows={3} value={fields[f.key] || ''} onChange={set(f.key)} />
                    : f.type === 'color'
                      ? <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                          <input type="color" value={fields[f.key] || '#1f6feb'} onChange={set(f.key)} style={{ width: 44, height: 34, padding: 2, border: '1px solid var(--line)', borderRadius: 6, background: 'var(--surface)' }} />
                          <input className="field mono" value={fields[f.key] || ''} onChange={set(f.key)} style={{ flex: 1 }} />
                        </div>
                      : <input className="field" value={fields[f.key] || ''} onChange={set(f.key)} />}
                </div>
              ))}
            </div>}
            <div className="note" style={{ marginTop: 12 }}>Le lien d'activation, l'expéditeur et la mention légale d'expiration sont ajoutés automatiquement.</div>
          </div>
        </div>

        <aside className="aside">
          <div className="card">
            <div className="sum-title">Aperçu</div>
            <div style={{ fontSize: 11, color: 'var(--ink-3)', margin: '0 0 8px' }}>Objet : <b style={{ color: 'var(--ink)' }}>{fields ? fields.subject : ''}</b></div>
            <iframe title="Aperçu du courriel" srcDoc={html} style={{ width: '100%', height: 480, border: '1px solid var(--line)', borderRadius: 8, background: '#fff' }} />
          </div>
        </aside>
      </div>
    </div>
  );
}

// ═══════════════════════ UTILISATEURS & RÔLES (réel — onglets) ═══════════════════════
// Écran unique branché sur la base : Membres (par magasin) · En attente · Magasins.
const STATUT_META = {
  pending:  { label: 'Courriel non vérifié', cls: 'none' },
  awaiting: { label: 'En attente d’autorisation', cls: 'prog' },
  active:   { label: 'Actif', cls: 'done' },
  disabled: { label: 'Désactivé', cls: 'none' },
};
function UtilisateursModule({ user, onLog }) {
  const AUTH = window.GPTOAuth;
  const Avatar = window.Avatar, yRoleLabel = window.roleLabel || ((r) => r);
  const CAPS = window.CAPABILITIES || [];
  const capFromRole = window.capFromRole || (() => false);
  const EST = window.ESTIMATION_MODULES || [];
  const [tab, setTab] = React.useState('membres');
  const [users, setUsers] = React.useState(null);
  const [stores, setStores] = React.useState([]);
  const [msg, setMsg] = React.useState(null);
  const [busyId, setBusyId] = React.useState(null);
  const [selId, setSelId] = React.useState(null);
  const [sForm, setSForm] = React.useState({ name: '', address: '', phone: '' });
  const [sEdit, setSEdit] = React.useState(null);       // magasin en édition
  const isSuper = user && user.role === 'super_admin';
  const canManage = isSuper || (user && user.role === 'store_manager');

  const load = React.useCallback(() => {
    if (!AUTH || !AUTH.enabled || !canManage) return;
    AUTH.call('/api/auth/users', null, 'GET').then(({ ok, data }) => { if (ok) setUsers(data.users); else setMsg({ type: 'error', text: data.error || 'Chargement impossible.' }); });
    AUTH.call('/api/auth/stores', null, 'GET').then(({ ok, data }) => { if (ok) setStores(data.stores); });
  }, []);
  React.useEffect(() => { load(); }, [load]);

  if (!AUTH || !AUTH.enabled)
    return <div className="fade-in"><SysHeader icon="users" title="Utilisateurs & rôles" sub="Magasins, comptes, rôles et accès." />
      <div className="note">Disponible en production (nécessite l'API d'authentification). En mode démo local, cet écran est inactif.</div></div>;
  if (!canManage)
    return <div className="fade-in"><SysHeader icon="users" title="Utilisateurs & rôles" />
      <div className="empty tickbox" style={{ maxWidth: 460, margin: '40px auto' }}><div className="empty-t">Accès réservé</div><div className="empty-s">Réservé aux gérants et au super-administrateur.</div></div></div>;

  const permsOf = (u) => { try { return JSON.parse(u.perms_json || '{}'); } catch (_) { return {}; } };
  const modsOf = (u) => { try { return JSON.parse(u.modules_json || '{}'); } catch (_) { return {}; } };
  const capOn = (u, id) => capFromRole(u.role, id) || !!permsOf(u)[id];
  const storeName = (id) => (stores.find((s) => s.id === id) || {}).name || '—';
  const allMods = (u) => u.role !== 'user';               // gérant/admin/super voient tous les modules
  const accessTxt = (u) => allMods(u) ? 'tous' : `${Object.keys(modsOf(u)).length}/${EST.length}`;

  const call = (path, body, verb) => {
    setBusyId(body.id || 'store'); setMsg(null);
    return AUTH.call('/api/auth/' + path, body).then(({ ok, data }) => {
      if (ok) { onLog && onLog('user', verb); load(); return true; }
      setMsg({ type: 'error', text: data.error || (data.errors && Object.values(data.errors)[0]) || 'Action impossible.' }); return false;
    }).finally(() => setBusyId(null));
  };
  const setStatus = (id, p, v) => call('users/' + p, { id }, v);
  const changeRole = (u, role) => call('users/role', { id: u.id, role }, 'a changé un rôle');
  const setStore = (u, storeId) => call('users/store', { id: u.id, storeId: storeId || null }, 'a affecté un magasin');
  const togglePerm = (u, id) => { if (capFromRole(u.role, id)) return; const p = { ...permsOf(u) }; p[id] ? delete p[id] : (p[id] = true); call('users/perms', { id: u.id, perms: p }, 'a modifié des permissions'); };
  const toggleMod = (u, id) => { const m = { ...modsOf(u) }; m[id] ? delete m[id] : (m[id] = true); call('users/modules', { id: u.id, modules: m }, 'a modifié les modules autorisés'); };
  const toggleAllMods = (u, on) => { const m = {}; if (on) EST.forEach((x) => { m[x.id] = true; }); call('users/modules', { id: u.id, modules: m }, 'a modifié les modules autorisés'); };

  const createStore = () => { if (!sForm.name.trim()) return setMsg({ type: 'error', text: 'Nom du magasin requis.' }); call('stores/create', { ...sForm }, 'a créé un magasin').then((ok) => ok && setSForm({ name: '', address: '', phone: '' })); };
  const saveStore = () => call('stores/update', { ...sEdit }, 'a modifié un magasin').then((ok) => ok && setSEdit(null));
  const delStore = (id) => { if (!window.confirm('Supprimer ce magasin ?')) return; call('stores/delete', { id }, 'a supprimé un magasin'); };

  const awaiting = (users || []).filter((u) => u.status === 'awaiting');
  const sel = (users || []).find((u) => u.id === selId) || null;

  const MemberRow = (u) => {
    const meta = STATUT_META[u.status] || { label: u.status, cls: 'none' };
    const isSelf = u.id === user.id, isSuperRow = u.role === 'super_admin';
    return (
      <tr key={u.id} style={selId === u.id ? { background: 'var(--surface-2)' } : null}>
        <td><div className="org-wname"><Avatar user={{ role: u.role, prenom: u.prenom, nom: u.nom }} size={30} />
          <div><div style={{ fontWeight: 500 }}>{u.prenom} {u.nom}</div><div className="muted" style={{ fontSize: 11 }}>{u.email}</div></div></div></td>
        <td>{isSuperRow ? <span className="muted">—</span> :
          <select className="cell-in" value={u.store_id || ''} disabled={!isSuper || busyId === u.id} onChange={(e) => setStore(u, e.target.value)} style={{ width: 150 }}>
            <option value="">— Aucun —</option>{stores.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
          </select>}</td>
        <td>{isSuperRow ? <span className="role-badge role-admin"><YIcon n="shield" style={{ width: 11, height: 11 }} /> {yRoleLabel(u.role)}</span> :
          <select className="cell-in" value={u.role} disabled={!isSuper || busyId === u.id} onChange={(e) => changeRole(u, e.target.value)} style={{ width: 150 }}>
            <option value="user">Estimateur</option><option value="store_manager">Gérant de magasin</option><option value="admin">Administrateur</option></select>}</td>
        <td className="muted" style={{ fontSize: 12 }}>{accessTxt(u)}</td>
        <td><span className={'pill ' + meta.cls}><span className="d" /> {meta.label}</span></td>
        <td style={{ textAlign: 'right' }}>
          {isSuperRow ? <span className="muted" style={{ fontSize: 11 }}>—</span> : <div className="row-flex" style={{ justifyContent: 'flex-end' }}>
            {u.status !== 'active' && <button className="btn accent" disabled={busyId === u.id} onClick={() => setStatus(u.id, 'approve', 'a autorisé un compte')}><YIcon n="check" /> Autoriser</button>}
            <button className={'btn' + (selId === u.id ? ' accent' : '')} onClick={() => setSelId(selId === u.id ? null : u.id)}><YIcon n="settings" /> Gérer</button>
            {u.status !== 'disabled' && !isSelf && <button className="btn" disabled={busyId === u.id} onClick={() => setStatus(u.id, 'reject', 'a désactivé un compte')}><YIcon n="trash" /></button>}
          </div>}
        </td>
      </tr>
    );
  };

  const TABS = [{ id: 'membres', label: 'Membres' }, { id: 'attente', label: `En attente${awaiting.length ? ' (' + awaiting.length + ')' : ''}` }, { id: 'magasins', label: 'Magasins' }];

  return (
    <div className="fade-in">
      <SysHeader icon="users" title="Utilisateurs & rôles" sub="Magasins Go Passif, comptes, rôles, et modules autorisés par utilisateur.">
        <button className="btn" onClick={load}><YIcon n="sync" /> Actualiser</button>
      </SysHeader>
      <div className="steps" style={{ marginBottom: 16 }}>
        {TABS.map((t) => <button key={t.id} className={'step' + (tab === t.id ? ' on' : '')} onClick={() => setTab(t.id)}>{t.label}</button>)}
      </div>
      {msg && <div className={'login-msg ' + msg.type} style={{ marginBottom: 14 }}>{msg.text}</div>}
      {!users && <div className="note">Chargement…</div>}

      {/* ── Onglet Membres ── */}
      {users && tab === 'membres' && <>
        <div className="card">
          <div className="card-h"><div className="card-h-l"><span className="badge-n">1</span><span className="card-t">Tous les comptes</span></div>
            <span className="card-hint">{users.length} compte{users.length > 1 ? 's' : ''}</span></div>
          <div className="tbl-wrap"><table><thead><tr><th>Membre</th><th style={{ width: 160 }}>Magasin</th><th style={{ width: 160 }}>Rôle</th><th style={{ width: 60 }}>Accès</th><th style={{ width: 160 }}>État</th><th style={{ width: 200 }}></th></tr></thead>
            <tbody>{users.map(MemberRow)}</tbody></table></div>
        </div>

        {sel && <div className="card">
          <div className="card-h"><div className="card-h-l"><span className="badge-n">2</span><span className="card-t">Accès — {sel.prenom} {sel.nom}</span></div>
            <button className="row-x" onClick={() => setSelId(null)} title="Fermer"><YIcon n="chevd" /></button></div>

          <div style={{ display: 'flex', alignItems: 'center', gap: 10, margin: '4px 0 10px' }}>
            <div className="card-t" style={{ fontSize: 13, flex: 1 }}>Modules d’estimation autorisés <span className="muted" style={{ fontWeight: 400 }}>· accès conditionné à la formation</span></div>
            {!allMods(sel) && <div className="row-flex">
              <button className="minibtn" disabled={busyId === sel.id} onClick={() => toggleAllMods(sel, true)}><YIcon n="check" style={{ width: 12, height: 12 }} /> Tout autoriser</button>
              <button className="minibtn" disabled={busyId === sel.id} onClick={() => toggleAllMods(sel, false)}>Tout retirer</button>
            </div>}
          </div>
          {allMods(sel) ? <div className="note">Ce rôle voit <b>tous les modules</b> automatiquement.</div> :
            <div className="cap-grid">
              {EST.map((m) => { const on = !!modsOf(sel)[m.id]; return (
                <label key={m.id} className={'cap-item' + (on ? ' on' : '')}>
                  <span className="cap-ic"><YIcon n={m.icon} style={{ width: 15, height: 15 }} /></span>
                  <span className="cap-tx"><b>{m.name}</b><span>{m.cat}</span></span>
                  <button className={'cap-toggle' + (on ? ' on' : '')} role="switch" aria-checked={on} disabled={busyId === sel.id} onClick={() => toggleMod(sel, m.id)}><span className="cap-knob" /></button>
                </label>); })}
            </div>}

          {isSuper && <>
            <div className="card-t" style={{ fontSize: 13, margin: '18px 0 8px' }}>Permissions administratives</div>
            <div className="cap-grid">
              {CAPS.map((c) => { const inh = capFromRole(sel.role, c.id), on = capOn(sel, c.id); return (
                <label key={c.id} className={'cap-item' + (on ? ' on' : '') + (inh ? ' locked' : '')} title={inh ? 'Accordé par le rôle' : ''}>
                  <span className="cap-ic"><YIcon n={c.icon} style={{ width: 15, height: 15 }} /></span>
                  <span className="cap-tx"><b>{c.label}</b><span>{c.desc}</span></span>
                  <button className={'cap-toggle' + (on ? ' on' : '')} role="switch" aria-checked={on} disabled={inh || busyId === sel.id} onClick={() => togglePerm(sel, c.id)}><span className="cap-knob" /></button>
                </label>); })}
            </div>
          </>}
        </div>}
      </>}

      {/* ── Onglet En attente ── */}
      {users && tab === 'attente' && <div className="card">
        <div className="card-h"><div className="card-h-l"><span className="badge-n">!</span><span className="card-t">Demandes d’autorisation</span></div>
          <span className="card-hint">{awaiting.length} en attente</span></div>
        {awaiting.length === 0 ? <div className="note">Aucune demande en attente.</div> :
          <div className="tbl-wrap"><table><thead><tr><th>Membre</th><th style={{ width: 160 }}>Magasin</th><th style={{ width: 160 }}>Rôle</th><th style={{ width: 60 }}>Accès</th><th style={{ width: 160 }}>État</th><th style={{ width: 200 }}></th></tr></thead>
            <tbody>{awaiting.map(MemberRow)}</tbody></table></div>}
      </div>}

      {/* ── Onglet Magasins ── */}
      {users && tab === 'magasins' && <>
        <div className="card">
          <div className="card-h"><div className="card-h-l"><span className="badge-n">1</span><span className="card-t">Magasins Go Passif</span></div>
            <span className="card-hint">{stores.length} magasin{stores.length > 1 ? 's' : ''}</span></div>
          {stores.length === 0 ? <div className="note">Aucun magasin. Créez-en un ci-dessous.</div> :
            <div className="tbl-wrap"><table><thead><tr><th>Nom</th><th>Adresse</th><th style={{ width: 150 }}>Téléphone</th><th style={{ width: 90 }}>Membres</th><th style={{ width: 110 }}></th></tr></thead>
              <tbody>{stores.map((s) => {
                const n = (users || []).filter((u) => u.store_id === s.id).length;
                const ed = sEdit && sEdit.id === s.id;
                return <tr key={s.id}>
                  <td>{ed ? <input className="cell-in" value={sEdit.name} onChange={(e) => setSEdit({ ...sEdit, name: e.target.value })} /> : <b>{s.name}</b>}</td>
                  <td>{ed ? <input className="cell-in" value={sEdit.address || ''} onChange={(e) => setSEdit({ ...sEdit, address: e.target.value })} /> : (s.address || <span className="muted">—</span>)}</td>
                  <td>{ed ? <input className="cell-in" value={sEdit.phone || ''} onChange={(e) => setSEdit({ ...sEdit, phone: e.target.value })} /> : (s.phone || <span className="muted">—</span>)}</td>
                  <td className="muted">{n}</td>
                  <td style={{ textAlign: 'right' }}>{isSuper && (ed
                    ? <div className="row-flex" style={{ justifyContent: 'flex-end' }}><button className="btn accent" onClick={saveStore}><YIcon n="check" /></button><button className="btn" onClick={() => setSEdit(null)}>Annuler</button></div>
                    : <div className="row-flex" style={{ justifyContent: 'flex-end' }}><button className="row-x" onClick={() => setSEdit({ id: s.id, name: s.name, address: s.address, phone: s.phone })}><YIcon n="pencil" /></button><button className="row-x" onClick={() => delStore(s.id)}><YIcon n="trash" /></button></div>)}</td>
                </tr>;
              })}</tbody></table></div>}
        </div>

        {isSuper && <div className="card">
          <div className="card-h"><div className="card-h-l"><span className="badge-n">2</span><span className="card-t">Nouveau magasin</span></div></div>
          <div className="fgrid">
            <div className="fcell"><label className="lbl">Nom</label><input className="field" value={sForm.name} onChange={(e) => setSForm({ ...sForm, name: e.target.value })} placeholder="Go Passif Québec" /></div>
            <div className="fcell"><label className="lbl">Téléphone</label><input className="field" value={sForm.phone} onChange={(e) => setSForm({ ...sForm, phone: e.target.value })} placeholder="418-555-0100" /></div>
            <div className="fcell full"><label className="lbl">Adresse</label><input className="field" value={sForm.address} onChange={(e) => setSForm({ ...sForm, address: e.target.value })} placeholder="123 rue Principale, Québec" /></div>
          </div>
          <button className="btn accent" style={{ marginTop: 12 }} onClick={createStore}><YIcon n="plus" /> Créer le magasin</button>
        </div>}
      </>}
    </div>
  );
}

// ═══════════════════════ FORMULAIRE DE PROJET (super-admin) ═══════════════════════
// Configure les champs du formulaire « Nouveau projet » : activer, obligatoire,
// libellé, ordre, et champs personnalisés.
const FIELD_TYPES = [{ value: 'text', label: 'Texte' }, { value: 'number', label: 'Nombre' }, { value: 'textarea', label: 'Zone de texte' }];
function ProjetFormModule({ user, onLog }) {
  const AUTH = window.GPTOAuth;
  const [fields, setFields] = React.useState(null);
  const [msg, setMsg] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const isSuper = user && user.role === 'super_admin';

  React.useEffect(() => {
    if (!AUTH || !AUTH.enabled || !isSuper) return;
    AUTH.call('/api/auth/form', null, 'GET').then(({ ok, data }) => { if (ok) setFields(data.config.fields); else setMsg({ type: 'error', text: data.error || 'Chargement impossible.' }); });
  }, []);

  if (!AUTH || !AUTH.enabled)
    return <div className="fade-in"><SysHeader icon="clipboard" title="Formulaire de projet" sub="Champs du formulaire de création de projet." />
      <div className="note">Disponible en production. En mode démo local, cet écran est inactif.</div></div>;
  if (!isSuper)
    return <div className="fade-in"><SysHeader icon="clipboard" title="Formulaire de projet" />
      <div className="empty tickbox" style={{ maxWidth: 460, margin: '40px auto' }}><div className="empty-t">Réservé au super-administrateur</div></div></div>;

  const setF = (i, patch) => setFields((fs) => fs.map((f, idx) => idx === i ? { ...f, ...patch } : f));
  const move = (i, d) => setFields((fs) => { const a = [...fs]; const j = i + d; if (j < 0 || j >= a.length) return fs; const t = a[i]; a[i] = a[j]; a[j] = t; return a; });
  const del = (i) => setFields((fs) => fs.filter((_, idx) => idx !== i));
  const addCustom = () => setFields((fs) => [...fs, { key: 'c_' + Math.random().toString(36).slice(2, 7), label: 'Nouveau champ', type: 'text', enabled: true, required: false, full: false, builtin: false }]);
  const save = () => {
    setBusy(true); setMsg(null);
    AUTH.call('/api/auth/form/save', { fields }).then(({ ok, data }) => {
      if (ok) { setFields(data.config.fields); setMsg({ type: 'info', text: 'Formulaire enregistré — appliqué aux prochaines créations.' }); onLog && onLog('edit', 'a modifié le formulaire de projet'); }
      else setMsg({ type: 'error', text: data.error || 'Enregistrement impossible.' });
    }).finally(() => setBusy(false));
  };

  return (
    <div className="fade-in">
      <SysHeader icon="clipboard" title="Formulaire de projet" sub="Choisissez les champs affichés à la création d'un projet, leur ordre, et ceux qui sont obligatoires.">
        <button className="btn accent" onClick={save} disabled={busy || !fields}><YIcon n="check" /> {busy ? 'Enregistrement…' : 'Enregistrer'}</button>
      </SysHeader>
      {msg && <div className={'login-msg ' + msg.type} style={{ marginBottom: 14 }}>{msg.text}</div>}
      {!fields && <div className="note">Chargement…</div>}

      {fields && <div className="card">
        <div className="tbl-wrap"><table><thead><tr>
          <th style={{ width: 60 }}>Ordre</th><th>Libellé</th><th style={{ width: 120 }}>Type</th>
          <th style={{ width: 80 }} className="num">Affiché</th><th style={{ width: 90 }} className="num">Obligatoire</th><th style={{ width: 80 }} className="num">Large</th><th style={{ width: 40 }}></th>
        </tr></thead>
        <tbody>{fields.map((f, i) => (
          <tr key={f.key}>
            <td><div className="row-flex"><button className="row-x" disabled={i === 0} onClick={() => move(i, -1)}><YIcon n="chev" style={{ width: 13, height: 13, transform: 'rotate(-90deg)' }} /></button><button className="row-x" disabled={i === fields.length - 1} onClick={() => move(i, 1)}><YIcon n="chevd" style={{ width: 13, height: 13 }} /></button></div></td>
            <td><input className="cell-in" value={f.label} onChange={(e) => setF(i, { label: e.target.value })} />{f.builtin && <span className="muted" style={{ fontSize: 10, marginLeft: 6 }}>intégré</span>}</td>
            <td>{f.builtin ? <span className="muted" style={{ fontSize: 12 }}>{(FIELD_TYPES.find((t) => t.value === f.type) || {}).label || f.type}</span> :
              <select className="cell-in" value={f.type} onChange={(e) => setF(i, { type: e.target.value })}>{FIELD_TYPES.map((t) => <option key={t.value} value={t.value}>{t.label}</option>)}</select>}</td>
            <td style={{ textAlign: 'right' }}><button className={'cap-toggle' + (f.enabled ? ' on' : '')} role="switch" aria-checked={f.enabled} disabled={f.locked} onClick={() => setF(i, { enabled: !f.enabled })}><span className="cap-knob" /></button></td>
            <td style={{ textAlign: 'right' }}><button className={'cap-toggle' + (f.required ? ' on' : '')} role="switch" aria-checked={f.required} disabled={f.locked || !f.enabled} onClick={() => setF(i, { required: !f.required })}><span className="cap-knob" /></button></td>
            <td style={{ textAlign: 'right' }}><button className={'cap-toggle' + (f.full ? ' on' : '')} role="switch" aria-checked={f.full} onClick={() => setF(i, { full: !f.full })}><span className="cap-knob" /></button></td>
            <td style={{ textAlign: 'right' }}>{!f.builtin && <button className="row-x" onClick={() => del(i)}><YIcon n="trash" /></button>}</td>
          </tr>
        ))}</tbody></table></div>
        <button className="addrow" onClick={addCustom}><YIcon n="plus" /> Ajouter un champ personnalisé</button>
        <div className="note" style={{ marginTop: 12 }}>Le champ « Nom du projet » est toujours affiché et obligatoire. Les champs intégrés sont conservés en base ; les champs personnalisés sont stockés avec le projet.</div>
      </div>}
    </div>
  );
}

Object.assign(window, { ProjetModule, ConfigModule, RecettesModule, SoumissionModule, ActivityModule, TeamModule, OrganisationModule, SystemesModule, ServicesModule, SysHeader, CourrielsModule, UtilisateursModule, ProjetFormModule });