/* global React, Icon, PROJECTS, sectorById, sectorLabel, projectName, projectCountries, projectPhase, getPortDakarData, getPlanFor, PDPlanning */
const { useState: useStatePD, useMemo: useMemoPD } = React;

// Sector-specific narratives for header subtitle + per-sector talking points
const SECTOR_NARRATIVES = {
  sante:       { fr: "Renforcement des soins de santé primaires — maternité, vaccination, chaîne du froid.", en: "Primary care strengthening — maternity, vaccination, cold chain." },
  nutrition:   { fr: "Lutte contre la malnutrition aiguë et chronique — dépistage, supplémentation, cantines.", en: "Tackling acute and chronic malnutrition — screening, supplementation, canteens." },
  vih:         { fr: "Prévention, dépistage et accompagnement TARV des jeunes urbains et populations clés.", en: "Prevention, testing and ART support for urban youth and key populations." },
  education:   { fr: "Qualité des apprentissages, équipement numérique des collèges et formation enseignants.", en: "Learning quality, digital equipment in middle schools and teacher training." },
  peche:       { fr: "Modernisation pêche artisanale, gestion des stocks halieutiques et valorisation littorale.", en: "Artisanal fisheries modernization, fish-stock management and coastal value addition." },
  port:        { fr: "Modernisation portuaire, terminaux à conteneurs, dragage, performance opérationnelle et recettes douanières.", en: "Port modernization, container terminals, dredging, operational performance and customs revenue." },
  agriculture: { fr: "Filières agricoles, irrigation, vulgarisation et résilience climatique des exploitations.", en: "Agricultural value chains, irrigation, extension services and climate resilience." },
  finances:    { fr: "Modernisation de l'administration fiscale et budgétaire — mobilisation des recettes.", en: "Fiscal and budget administration modernization — revenue mobilization." },
  gouvernance: { fr: "Décentralisation, redevabilité sociale et performance des services publics locaux.", en: "Decentralization, social accountability and local public service performance." },
  eau:         { fr: "Accès à l'eau potable, assainissement et hygiène dans les centres de services publics.", en: "Access to safe water, sanitation and hygiene in public facility centers." },
  energie:     { fr: "Mini-réseaux solaires, électrification rurale et accès aux services énergétiques.", en: "Solar mini-grids, rural electrification and access to energy services." },
  genre:       { fr: "Autonomisation économique des femmes, lutte contre les violences et inclusion.", en: "Women economic empowerment, gender-based violence reduction and inclusion." },
};

// ==================== PROJECT DETAIL (drill-down) ====================
function ProjectDetail({ t, lang, projectId, selectedProject, onOpen, onBack, isSuperAdmin, isAdmin, canEditIndicators, hasPerm }) {
  const pid = projectId || selectedProject || "P-001";
  const { currency } = window.melr.useCurrency();
  // fmtM accepts an optional source currency (defaults to EUR for fixture projects).
  const fmtM = (v, src) => window.melr.formatAmount(v, src || "EUR", currency, lang);
  // All React hooks must be called unconditionally before any early
  // return (rules of hooks). Previously useStatePD(tab) and usePlan(...)
  // came AFTER the supaLoading early return, which made the number of
  // hooks vary between renders and crashed the component.
  const { project: supaProject, loading: supaLoading, refresh: refreshSupa } = window.melr.useProjectDetail(pid);
  const { programmes: livePrograms } = window.melr.usePrograms();
  const [tab, setTab] = useStatePD("overview");
  const [progEditing, setProgEditing] = useStatePD(false);
  const [progBusy, setProgBusy] = useStatePD(false);
  const { plan: livePlan, refresh: refreshPlan } = window.melr.usePlan(supaProject && supaProject.uuid);
  if (supaLoading) {
    return <div className="page"><div className="page-body" style={{ padding: 40 }}>{lang === "fr" ? "Chargement du projet…" : "Loading project…"}</div></div>;
  }

  // Click handler for the progress edit button (live projects only).
  const onEditProgress = async () => {
    if (!supaProject || !supaProject.uuid) return;
    const raw = window.prompt(
      lang === "fr" ? "Nouveau pourcentage d'avancement (0-100) :" : "New progress (0-100):",
      String(supaProject.progress || 0),
    );
    if (raw === null) return;
    const v = Math.max(0, Math.min(100, parseInt(raw, 10) || 0));
    try {
      await window.melr.updateProject(supaProject.uuid, { progress: v });
      await refreshSupa();
    } catch (e) {
      window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message);
    }
  };
  // Use database row when available; fall back to legacy fixture for
  // projects that aren't seeded yet (e.g. Port de Dakar demo).
  const base = supaProject || (PROJECTS && PROJECTS.find((x) => x.id === pid)) || (PROJECTS && PROJECTS[0]);
  const sectorObj = base ? sectorById(base.sector) : null;
  const sectorNote = (base && SECTOR_NARRATIVES[base.sector]) || SECTOR_NARRATIVES.sante;
  const isPortDakar = pid === "P-612" && typeof getPortDakarData === "function";
  const PORT = isPortDakar ? getPortDakarData(lang) : null;

  const PROJECT = {
    id: base ? base.id : "P-241",
    name: base ? projectName(base, lang) : "Renforcement santé primaire Sahel",
    short: lang === "fr" ? sectorNote.fr : sectorNote.en,
    phase: base ? projectPhase(base, lang) : "Mise en œuvre",
    phaseStep: base && base.status === "appraisal" ? 0 : base && base.status === "closing" ? 3 : base && base.progress < 25 ? 1 : 2,
    countries: base ? projectCountries(base, lang) : "Mali · Burkina Faso · Niger",
    sector: sectorObj ? sectorLabel(sectorObj, lang) : "—",
    sectorObj,
    donor: base ? base.donor : "AFD · Union européenne · Gavi",
    startDate: "01/03/2024",
    endDate: "28/02/2027",
    sites: base ? base.sites : 42,
    beneficiaries: base ? (base.sites * 9750).toLocaleString("fr-FR") : "412 800",
    staff: 18,
    progress: base ? base.progress : 64,
    budget: base ? base.budget : 3.36,
    disbursed: base ? base.disbursed : 2.15,
    committed: base ? (base.disbursed + base.budget * 0.18).toFixed(2) * 1 : 2.84,
    burn: base ? base.progress : 64,
    plan: base ? Math.min(95, base.progress + 6) : 58,
    irr: 14.2, npv: 1.27, dscr: 1.86,
    nativeCurrency: (base && base.nativeCurrency) || "EUR",
    risk: base ? base.risk : "warn",
    lead: base ? base.lead : "Aïssata Diallo",
    leadRole: lang === "fr" ? "Chef de projet" : "Project lead",
    meLead: "Souleymane Touré",
    programmeId:    (base && base.programmeId) || null,
    programmeCode:  (base && base.programmeCode) || null,
    programmeName:  (base && base.programmeName) || null,
  };

  // Live-edit handler for the project's programme link. Only available
  // when this is a database row (supaProject.uuid present).
  const onChangeProgramme = async (newProgrammeId) => {
    if (!supaProject || !supaProject.uuid) return;
    setProgBusy(true);
    try {
      await window.melr.updateProject(supaProject.uuid, {
        programme_id: newProgrammeId || null,
      });
      await refreshSupa();
      setProgEditing(false);
    } catch (e) {
      window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message);
    } finally {
      setProgBusy(false);
    }
  };

  // Outcome / output indicators with sparkline data
  const INDICATORS = PORT ? PORT.indicators : [
    { code: "OC-01", level: "outcome", name: lang === "fr" ? "Couverture vaccinale Penta-3 (12–23 mois)" : "Penta-3 immunization coverage (12–23 m)", base: 65.1, target: 90, cur: 78.4, unit: "%", trend: [65,67,68,70,71,73,74,75,76,77,78,78.4], status: "ok", dir: "up" },
    { code: "OC-02", level: "outcome", name: lang === "fr" ? "Accouchements assistés par personnel qualifié" : "Births attended by skilled personnel", base: 48.2, target: 75, cur: 67.8, unit: "%", trend: [48,52,55,57,60,62,63,65,66,67,67.5,67.8], status: "ok", dir: "up" },
    { code: "OC-03", level: "outcome", name: lang === "fr" ? "Mortalité maternelle (pour 100 000 NV)" : "Maternal mortality (per 100k live births)", base: 642, target: 380, cur: 487, unit: "‰", trend: [642,624,610,580,560,540,525,510,500,495,490,487], status: "warn", dir: "down" },
    { code: "OP-04", level: "output", name: lang === "fr" ? "CSCom dotés en chaîne du froid fonctionnelle" : "CSCom with functioning cold chain", base: 12, target: 42, cur: 38, unit: "", trend: [12,15,19,22,26,29,32,34,36,37,38,38], status: "ok", dir: "up" },
    { code: "OP-05", level: "output", name: lang === "fr" ? "Agents de santé formés (cumul)" : "Health workers trained (cumulative)", base: 0, target: 420, cur: 287, unit: "", trend: [0,28,62,98,134,168,201,224,247,268,279,287], status: "ok", dir: "up" },
    { code: "OP-06", level: "output", name: lang === "fr" ? "Référencements obstétricaux d'urgence" : "Emergency obstetric referrals", base: 184, target: 600, cur: 312, unit: "", trend: [184,202,221,238,251,266,281,290,298,304,309,312], status: "warn", dir: "up" },
    { code: "OP-07", level: "output", name: lang === "fr" ? "Stock de rupture en MEG (jours/an)" : "MEG stockout (days/year)", base: 71, target: 14, cur: 38, unit: "j", trend: [71,68,62,58,53,49,45,42,40,39,38,38], status: "warn", dir: "down" },
  ];

  const MILESTONES = PORT ? PORT.milestones : [
    { d: "2024-03-01", t: lang === "fr" ? "Démarrage projet" : "Project kick-off", state: "done" },
    { d: "2024-05-15", t: lang === "fr" ? "Baseline finalisée — 42 sites" : "Baseline complete — 42 sites", state: "done" },
    { d: "2024-09-30", t: lang === "fr" ? "Cohorte 1 formation (140 agents)" : "Training cohort 1 (140 staff)", state: "done" },
    { d: "2025-01-15", t: lang === "fr" ? "Installation chaîne du froid 30 sites" : "Cold chain deployed 30 sites", state: "done" },
    { d: "2025-06-30", t: lang === "fr" ? "Évaluation mi-parcours" : "Mid-term evaluation", state: "done" },
    { d: "2026-03-15", t: lang === "fr" ? "Cohorte 2 formation — EN COURS" : "Training cohort 2 — IN PROGRESS", state: "cur" },
    { d: "2026-09-30", t: lang === "fr" ? "Audit DQA externe" : "External DQA audit", state: "todo" },
    { d: "2027-02-28", t: lang === "fr" ? "Clôture & évaluation finale" : "Closure & final evaluation", state: "todo" },
  ];

  const ACTIVITY = PORT ? PORT.activity : [
    { who: "Souleymane Touré", role: "S&E", a: lang === "fr" ? "a validé 11 mises à jour d'indicateurs" : "validated 11 indicator updates", w: lang === "fr" ? "il y a 2 h" : "2 h ago", t: "check", tone: "green" },
    { who: "Aïssata Diallo", role: lang === "fr" ? "Chef projet" : "Project lead", a: lang === "fr" ? "a soumis le rapport Q1 2026 pour validation" : "submitted Q1 2026 report for validation", w: lang === "fr" ? "hier" : "yesterday", t: "send", tone: "accent" },
    { who: "Karim Bensaad", role: lang === "fr" ? "Audit qualité" : "Quality audit", a: lang === "fr" ? "a lancé un échantillonnage DQA sur 4 sites" : "launched DQA sampling on 4 sites", w: "2 j", t: "shieldCheck", tone: "violet" },
    { who: "Bintou Tall", role: lang === "fr" ? "Saisie terrain" : "Field", a: lang === "fr" ? "a synchronisé 87 fiches (Tombouctou)" : "synced 87 forms (Timbuktu)", w: "3 j", t: "refresh", tone: "accent" },
    { who: "Modou Sarr", role: lang === "fr" ? "Bailleur" : "Donor", a: lang === "fr" ? "a téléchargé le dashboard portefeuille" : "downloaded portfolio dashboard", w: "5 j", t: "download", tone: "amber" },
  ];

  const RISKS = PORT ? PORT.risks : [
    { l: "H", c: lang === "fr" ? "Insécurité Nord Mali — 4 sites" : "Insecurity North Mali — 4 sites", m: lang === "fr" ? "Rotation des équipes · escortes UN" : "Team rotation · UN escorts", p: 4, i: 5 },
    { l: "M", c: lang === "fr" ? "Rupture MEG saison pluies" : "MEG stockout rainy season", m: lang === "fr" ? "Stocks tampons préfectoraux" : "Buffer stocks at district level", p: 3, i: 4 },
    { l: "M", c: lang === "fr" ? "Volatilité FCFA / EUR" : "XOF / EUR volatility", m: lang === "fr" ? "Achats groupés trimestriels" : "Quarterly bulk procurement", p: 4, i: 3 },
    { l: "L", c: lang === "fr" ? "Rotation personnel formé" : "Trained staff rotation", m: lang === "fr" ? "Primes rétention 18 mois" : "18-month retention bonus", p: 3, i: 2 },
  ];

  const FILES = PORT ? PORT.files : [
    { n: "MPR-P241-revised-v3.xlsx", s: "284 KB", w: "Karim B.", d: lang === "fr" ? "il y a 3 j" : "3 d ago", t: "spreadsheet" },
    { n: "Baseline-rapport-final.pdf", s: "4.2 MB", w: "Souleymane T.", d: "12 j", t: "fileText" },
    { n: "Cartographie-42-sites.geojson", s: "892 KB", w: "Bintou T.", d: "1 m", t: "map" },
    { n: "Manuel-procedures-DQA.pdf", s: "1.8 MB", w: "Aïssata D.", d: "2 m", t: "fileText" },
    { n: "Budget-2026-detail.xlsx", s: "156 KB", w: "Aïssata D.", d: "2 m", t: "spreadsheet" },
  ];

  // Live plan from database if the project is in DB AND has phases seeded;
  // otherwise fall back to the in-memory fixture (data-plans.jsx).
  const fixturePlan = typeof getPlanFor === "function" ? getPlanFor(pid, base) : null;
  let plan = fixturePlan;
  if (livePlan && livePlan.phases && livePlan.phases.length > 0) {
    const dates = livePlan.actions
      .flatMap((a) => [a.start_date, a.end_date])
      .filter(Boolean)
      .sort();
    plan = {
      start: dates[0] || "2024-01-01",
      end:   dates[dates.length - 1] || "2027-12-31",
      phases: livePlan.phases.map((ph) => ({
        uuid: ph.id,                                                 // database UUID for createPlanAction
        id: ph.code,
        code: ph.code,
        name: { fr: ph.name_fr, en: ph.name_en || ph.name_fr },
        color: ph.color || "oklch(0.55 0.13 230)",
      })),
      actions: livePlan.actions.map((a) => ({
        uuid: a.id,                                                 // database UUID, needed for updatePlanAction
        phase_id: a.phase_id,                                        // for EditActionModal
        id: a.wbs || a.id,
        wbs: a.wbs || "",
        phase: (livePlan.phases.find((ph) => ph.id === a.phase_id) || {}).code,
        name: { fr: a.name_fr, en: a.name_en || a.name_fr },
        name_fr: a.name_fr,
        name_en: a.name_en,
        owner: base && base.lead ? base.lead : "—",
        start: a.start_date,
        end: a.end_date,
        progress: a.progress || 0,
        status: a.status || "planned",
        milestone: !!a.milestone,
        dep: [],
      })),
    };
  }
  const TABS = [
    { k: "overview", l: lang === "fr" ? "Vue d'ensemble" : "Overview" },
    { k: "planning", l: lang === "fr" ? "Planification" : "Planning", c: plan ? plan.actions.length : 0 },
    { k: "indicators", l: PORT ? (lang === "fr" ? "Indicateurs CNUCED" : "UNCTAD indicators") : (lang === "fr" ? "Indicateurs" : "Indicators"), c: INDICATORS.length },
    { k: "sites", l: PORT ? (lang === "fr" ? "Postes à quai" : "Berths") : (lang === "fr" ? "Sites" : "Sites"), c: PORT ? PORT.sites.length : 42 },
    { k: "budget", l: lang === "fr" ? "Budget" : "Budget" },
    { k: "team", l: lang === "fr" ? "Équipe" : "Team", c: PORT ? PORT.team.length : 18 },
    { k: "files", l: lang === "fr" ? "Documents" : "Documents", c: FILES.length },
    { k: "risks", l: lang === "fr" ? "Risques" : "Risks", c: RISKS.length },
    { k: "activity", l: lang === "fr" ? "Activité" : "Activity" },
  ];

  return (
    <div className="page pd-page">
      {/* Header */}
      <div className="pd-header">
        <button className="pd-back" onClick={onBack}>
          <Icon.chevronLeft />
          <span>{lang === "fr" ? "Portefeuille" : "Portfolio"}</span>
        </button>
        <div className="pd-title-row">
          <div style={{ flex: 1, minWidth: 0 }}>
            <div className="row gap-sm" style={{ marginBottom: 6 }}>
              <span className="mono text-faint" style={{ fontSize: 11.5 }}>{PROJECT.id}</span>
              <span className="dotsep"></span>
              {PROJECT.sectorObj && (
                <span className="sector-chip" style={{ background: PROJECT.sectorObj.bg, color: PROJECT.sectorObj.color, borderColor: PROJECT.sectorObj.color }}>{PROJECT.sector}</span>
              )}
              <span className="dotsep"></span>
              {PROJECT.risk === "ok" && <span className="pill green dot">OK</span>}
              {PROJECT.risk === "warn" && <span className="pill amber dot">{lang === "fr" ? "Vigilance" : "Watch"}</span>}
              {PROJECT.risk === "bad" && <span className="pill red dot">{lang === "fr" ? "Élevé" : "High"}</span>}
              <span className="dotsep"></span>
              <span className="muted" style={{ fontSize: 11.5 }}>{PROJECT.phase}</span>
              <span className="dotsep"></span>
              {/* Programme parent chip — clickable when set, with inline edit dropdown for live projects */}
              {PROJECT.programmeId ? (
                <span className="row gap-xs" style={{ alignItems: "center" }}>
                  <span
                    onClick={() => onOpen && onOpen("prog:" + PROJECT.programmeId)}
                    title={lang === "fr" ? "Ouvrir le programme parent" : "Open parent programme"}
                    style={{
                      padding: "2px 8px", borderRadius: 999, background: "var(--bg-sunken)",
                      color: "var(--text)", fontSize: 11, fontWeight: 500, cursor: "pointer",
                      border: "1px solid var(--line-faint)",
                    }}>
                    ◇ {PROJECT.programmeCode}{PROJECT.programmeName ? " — " + PROJECT.programmeName : ""}
                  </span>
                </span>
              ) : (
                <span className="text-faint" style={{ fontSize: 11 }}>
                  ◇ {lang === "fr" ? "Sans programme" : "No programme"}
                </span>
              )}
              {/* Inline edit: only for live (database) projects with a uuid */}
              {supaProject && supaProject.uuid && (
                <>
                  {!progEditing ? (
                    <button className="btn xs ghost" onClick={() => setProgEditing(true)}
                      title={lang === "fr" ? "Changer le programme parent" : "Change parent programme"}
                      style={{ fontSize: 10, padding: "1px 6px" }}>
                      <Icon.edit />
                    </button>
                  ) : (
                    <span className="row gap-xs" style={{ alignItems: "center" }}>
                      <select
                        autoFocus
                        defaultValue={PROJECT.programmeId || ""}
                        disabled={progBusy}
                        onChange={(e) => onChangeProgramme(e.target.value)}
                        style={{ fontSize: 11, padding: "2px 6px", borderRadius: 4, border: "1px solid var(--line)" }}>
                        <option value="">— {lang === "fr" ? "aucun programme" : "no programme"} —</option>
                        {(livePrograms || []).map((pg) => (
                          <option key={pg.id} value={pg.id}>
                            {pg.code} — {lang === "en" ? (pg.name_en || pg.name_fr) : pg.name_fr}
                          </option>
                        ))}
                      </select>
                      <button className="btn xs ghost" onClick={() => setProgEditing(false)}
                        disabled={progBusy} style={{ fontSize: 10, padding: "1px 6px" }}>
                        {lang === "fr" ? "Annuler" : "Cancel"}
                      </button>
                    </span>
                  )}
                </>
              )}
            </div>
            <h1 className="page-title" style={{ marginBottom: 4 }}>
              {PROJECT.name}
              {PROJECT.nativeCurrency && PROJECT.nativeCurrency !== currency && (
                <span style={{
                  marginLeft: 10, display: "inline-block", padding: "2px 8px", borderRadius: 999,
                  background: "var(--bg-sunken)", color: "var(--text-faint)",
                  border: "1px solid var(--line-faint)",
                  fontSize: 11, fontWeight: 500, verticalAlign: "middle",
                }} title={lang === "fr" ? "Devise native du projet (les montants sont convertis pour l'affichage)" : "Project's native currency (amounts are converted for display)"}>
                  {lang === "fr" ? "Native : " : "Native: "}{PROJECT.nativeCurrency}
                </span>
              )}
            </h1>
            <div className="page-sub" style={{ maxWidth: 760 }}>{PROJECT.short}</div>
          </div>
          <div className="pd-header-actions">
            {/* "Suivre" / "Discussion" removed — placeholders with no
                backing feature confused beginners. "Exporter" now
                produces a CSV with the project's headline data so the
                button does something concrete. "Modifier" deep-links
                to the Projects screen where the existing edit flow
                lives (transfer org, set lead, rename, etc.). */}
            <button className="btn sm"
              onClick={() => {
                if (!window.melr || !window.melr.exportCSV) return;
                const date = new Date().toISOString().slice(0, 10);
                window.melr.exportCSV("project-" + (PROJECT.id || "x") + "-" + date + ".csv", [{
                  code:        PROJECT.id,
                  name:        projectName(PROJECT, lang),
                  sector:      (() => { const s = sectorById(PROJECT.sector); return s ? (lang === "en" ? s.en : s.fr) : ""; })(),
                  countries:   projectCountries(PROJECT, lang),
                  lead:        PROJECT.lead,
                  budget:      PROJECT.budget,
                  disbursed:   PROJECT.disbursed,
                  progress:    Math.round(PROJECT.progress || 0),
                  phase:       lang === "en" ? PROJECT.phaseEn : PROJECT.phaseFr,
                  risk:        PROJECT.risk,
                  sites:       PROJECT.sites,
                  indicators:  PROJECT.indic,
                  donors:      PROJECT.donor || "",
                }], [
                  { key: "code",       label: "Code" },
                  { key: "name",       label: lang === "fr" ? "Nom" : "Name" },
                  { key: "sector",     label: lang === "fr" ? "Secteur" : "Sector" },
                  { key: "countries",  label: lang === "fr" ? "Pays" : "Countries" },
                  { key: "lead",       label: lang === "fr" ? "Responsable" : "Lead" },
                  { key: "budget",     label: lang === "fr" ? "Budget (M)" : "Budget (M)" },
                  { key: "disbursed",  label: lang === "fr" ? "Décaissé (M)" : "Disbursed (M)" },
                  { key: "progress",   label: lang === "fr" ? "Avancement %" : "Progress %" },
                  { key: "phase",      label: lang === "fr" ? "Phase" : "Phase" },
                  { key: "risk",       label: lang === "fr" ? "Risque" : "Risk" },
                  { key: "sites",      label: "Sites" },
                  { key: "indicators", label: lang === "fr" ? "Indicateurs" : "Indicators" },
                  { key: "donors",     label: lang === "fr" ? "Bailleurs" : "Donors" },
                ]);
              }}>
              <Icon.download /> {lang === "fr" ? "Exporter" : "Export"}
            </button>
            {supaProject && supaProject.uuid && (
              <button className="btn sm" onClick={onEditProgress} title={lang === "fr" ? "Modifier l'avancement" : "Update progress"}>
                <Icon.edit /> {lang === "fr" ? "Avancement" : "Progress"}
              </button>
            )}
            <button className="btn sm primary"
              onClick={() => { if (typeof window !== "undefined" && window.melr) window.alert(lang === "fr"
                ? "Édition complète du projet : utilisez l'écran « Projets » → bouton « Définir le responsable » ou « Transférer ». Édition libre des champs (code, nom, budget…) à venir."
                : "Full project edit: use the 'Projects' screen → 'Set lead' or 'Transfer' buttons. Free-field edit (code, name, budget…) coming soon."); }}>
              <Icon.edit /> {lang === "fr" ? "Modifier" : "Edit"}
            </button>
          </div>
        </div>

        {/* Phase progress */}
        <div className="pd-phases">
          {[
            { k: "appraisal", l: lang === "fr" ? "Évaluation" : "Appraisal" },
            { k: "inception", l: lang === "fr" ? "Démarrage" : "Inception" },
            { k: "implementation", l: lang === "fr" ? "Mise en œuvre" : "Implementation" },
            { k: "closing", l: lang === "fr" ? "Clôture" : "Closing" },
            { k: "evaluation", l: lang === "fr" ? "Évaluation finale" : "Final evaluation" },
          ].map((p, i) => (
            <div key={p.k} className={"pd-phase " + (i < PROJECT.phaseStep ? "done" : i === PROJECT.phaseStep ? "cur" : "todo")}>
              <div className="pd-phase-bar"></div>
              <div className="pd-phase-label">{p.l}</div>
            </div>
          ))}
        </div>

        {/* Tabs */}
        <div className="pd-tabs">
          {TABS.map((tb) => (
            <button key={tb.k} className={"pd-tab" + (tab === tb.k ? " active" : "")} onClick={() => setTab(tb.k)}>
              {tb.l} {tb.c !== undefined && <span className="pd-tab-c">{tb.c}</span>}
            </button>
          ))}
        </div>
      </div>

      {tab === "overview" && <PDOverview P={PROJECT} INDICATORS={INDICATORS} MILESTONES={MILESTONES} ACTIVITY={ACTIVITY} lang={lang} kpisOverride={PORT && PORT.kpis} fmtM={fmtM} />}
      {tab === "planning" && <PDPlanning project={base} plan={plan} lang={lang} projectUuid={supaProject && supaProject.uuid} onPlanChanged={refreshPlan} />}
      {tab === "indicators" && (PORT
        ? <PDIndicatorsCNUCED INDICATORS={INDICATORS} lang={lang} projectCode={base && base.id} onChanged={() => refreshSupa && refreshSupa()} />
        : <PDIndicators INDICATORS={INDICATORS} lang={lang} projectCode={base && base.id}
            onChanged={() => refreshSupa && refreshSupa()}
            isSuperAdmin={isSuperAdmin} isAdmin={isAdmin}
            canEditIndicators={canEditIndicators} hasPerm={hasPerm} />)}
      {tab === "sites" && <PDSites lang={lang} SITES={PORT && PORT.sites} />}
      {tab === "budget" && <PDBudget P={PROJECT} lang={lang} LINES={PORT && PORT.budgetLines} fmtM={fmtM} projectUuid={supaProject && supaProject.uuid} />}
      {tab === "team" && <PDTeam lang={lang} TEAM={PORT && PORT.team} projectUuid={supaProject && supaProject.uuid} projectOrgId={supaProject && supaProject.organizationId} />}
      {tab === "files" && <PDFiles FILES={FILES} lang={lang} projectUuid={supaProject && supaProject.uuid} />}
      {tab === "risks" && <PDRisks RISKS={RISKS} lang={lang} projectUuid={supaProject && supaProject.uuid} />}
      {tab === "activity" && <PDActivity ACTIVITY={ACTIVITY} lang={lang} projectUuid={supaProject && supaProject.uuid} />}
    </div>
  );
}

function PDOverview({ P, INDICATORS, MILESTONES, ACTIVITY, lang, kpisOverride, fmtM }) {
  // Fallback for fixture / standalone use: if no fmtM is passed, format
  // amounts as plain "X.XX M€" (legacy behaviour).
  if (!fmtM) fmtM = (v) => (v != null ? (Number(v).toFixed(2) + " M€") : "—");
  const DEFAULT_KPIS = [
    { l: lang === "fr" ? "Avancement" : "Progress", v: P.progress + "%", s: lang === "fr" ? "vs plan " + P.plan + "%" : "vs plan " + P.plan + "%", tone: "accent" },
    { l: lang === "fr" ? "Décaissé" : "Disbursed", v: fmtM(P.disbursed, P.nativeCurrency), s: P.budget > 0 ? Math.round(P.disbursed / P.budget * 100) + "% " + (lang === "fr" ? "du budget" : "of budget") : "—", tone: "green" },
    { l: lang === "fr" ? "Bénéficiaires" : "Beneficiaries", v: P.beneficiaries, s: lang === "fr" ? "cumul atteint" : "cumulative reached" },
    { l: lang === "fr" ? "Sites actifs" : "Active sites", v: P.sites, s: "3 " + (lang === "fr" ? "pays" : "countries") },
    { l: "VAN", v: "+" + fmtM(P.npv), s: "TRI " + P.irr + "%", tone: "green" },
    { l: "DSCR", v: P.dscr, s: lang === "fr" ? "couverture dette" : "debt coverage" },
  ];
  const KPIS = kpisOverride || DEFAULT_KPIS;

  return (
    <>
      <div className="grid cols-6" style={{ marginBottom: 16 }}>
        {KPIS.map((k, i) => (
          <div key={i} className="kpi">
            <div className="kpi-label">{k.l}</div>
            <div className="kpi-value" style={k.tone ? { color: `var(--${k.tone === "accent" ? "accent" : k.tone})` } : {}}>{k.v}</div>
            <div className="kpi-sub">{k.s}</div>
          </div>
        ))}
      </div>

      <div className="grid cols-3" style={{ gridTemplateColumns: "1.6fr 1fr", gap: 14 }}>
        {/* Indicators summary */}
        <div className="card">
          <div className="card-head">
            <div className="card-title">{lang === "fr" ? "Indicateurs clés" : "Key indicators"}</div>
            <button className="btn xs ghost">{lang === "fr" ? "Tout voir" : "View all"} <Icon.chevronRight /></button>
          </div>
          <div className="card-body flush">
            {INDICATORS.filter((ind) => !ind.textual && ind.trend && ind.trend.length).slice(0, 5).map((ind) => (
              <div key={ind.code} className="pd-ind-row">
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="row gap-xs" style={{ marginBottom: 3 }}>
                    <span className="mono text-faint" style={{ fontSize: 10 }}>{ind.code}</span>
                    <span className={"pill xs " + (ind.level === "outcome" ? "violet" : "")} style={{ fontSize: 9 }}>{ind.level === "outcome" ? (lang === "fr" ? "Effet" : "Outcome") : (lang === "fr" ? "Produit" : "Output")}</span>
                  </div>
                  <div className="strong" style={{ fontSize: 12.5, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{ind.name}</div>
                </div>
                <PDSpark data={ind.trend} tone={ind.status} />
                <div style={{ textAlign: "right", minWidth: 110 }}>
                  <div className="mono strong" style={{ fontSize: 13 }}>{ind.cur}{ind.unit}</div>
                  <div className="text-faint" style={{ fontSize: 10.5 }}>{lang === "fr" ? "cible" : "target"} {ind.target}{ind.unit}</div>
                </div>
                <div style={{ width: 100 }}>
                  <div className="bar"><div className="bar-fill" style={{ width: Math.min(100, Math.round((ind.cur - ind.base) / (ind.target - ind.base) * 100)) + "%", background: ind.status === "ok" ? "var(--green)" : ind.status === "warn" ? "var(--amber)" : "var(--red)" }}></div></div>
                </div>
              </div>
            ))}
          </div>
        </div>

        {/* Metadata card */}
        <div className="card">
          <div className="card-head"><div className="card-title">{lang === "fr" ? "Fiche projet" : "Project info"}</div></div>
          <div className="card-body" style={{ display: "flex", flexDirection: "column", gap: 10 }}>
            {[
              { l: lang === "fr" ? "Pays" : "Countries", v: P.countries },
              { l: lang === "fr" ? "Secteur" : "Sector", v: P.sector },
              { l: lang === "fr" ? "Bailleurs" : "Donors", v: P.donor },
              { l: lang === "fr" ? "Période" : "Period", v: P.startDate + " → " + P.endDate },
              { l: lang === "fr" ? "Chef de projet" : "Project lead", v: P.lead, av: true },
              { l: lang === "fr" ? "Responsable S&E" : "M&E lead", v: P.meLead, av: true },
            ].map((row, i) => (
              <div key={i} className="pd-meta-row">
                <div className="text-faint" style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: 0.4 }}>{row.l}</div>
                <div className="row gap-xs" style={{ fontSize: 12.5 }}>
                  {row.av && <span className="avatar xxs" style={{ background: avColorPD(row.v) }}>{initialsPD(row.v)}</span>}
                  <span className="strong">{row.v}</span>
                </div>
              </div>
            ))}
          </div>
        </div>
      </div>

      {/* Timeline + Activity */}
      <div className="grid cols-2" style={{ gap: 14, gridTemplateColumns: "1.6fr 1fr", marginTop: 14 }}>
        <div className="card">
          <div className="card-head"><div className="card-title">{lang === "fr" ? "Jalons" : "Milestones"}</div></div>
          <div className="card-body">
            <div className="pd-milestones">
              {MILESTONES.map((m, i) => (
                <div key={i} className={"pd-ms " + m.state}>
                  <div className="pd-ms-dot">{m.state === "done" ? <Icon.check /> : <span></span>}</div>
                  <div style={{ flex: 1 }}>
                    <div className="strong" style={{ fontSize: 12.5 }}>{m.t}</div>
                    <div className="text-faint mono" style={{ fontSize: 11 }}>{m.d}</div>
                  </div>
                </div>
              ))}
            </div>
          </div>
        </div>

        <div className="card">
          <div className="card-head">
            <div className="card-title">{lang === "fr" ? "Activité récente" : "Recent activity"}</div>
            <button className="btn xs ghost">{lang === "fr" ? "Tout" : "All"} <Icon.chevronRight /></button>
          </div>
          <div className="card-body">
            {ACTIVITY.map((a, i) => {
              const Ic = Icon[a.t] || Icon.info;
              return (
                <div key={i} className="pd-act">
                  <div className={"pd-act-icon tone-" + a.tone}><Ic /></div>
                  <div style={{ flex: 1, fontSize: 12 }}>
                    <div><span className="strong">{a.who}</span> <span className="muted">{a.a}</span></div>
                    <div className="text-faint" style={{ fontSize: 10.5 }}>{a.role} · {a.w}</div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </>
  );
}

function PDIndicators({ INDICATORS, lang, projectCode, onChanged, isSuperAdmin, isAdmin, canEditIndicators, hasPerm }) {
  const { useState: useStatePDI } = React;
  // Tri-state filter for the Niveau (level) column. Defaults to "all".
  // Values: 'all' | 'output' | 'outcome' | 'impact' | 'context'.
  const [levelFilter, setLevelFilter] = useStatePDI("all");
  const [addOpen, setAddOpen] = useStatePDI(false);
  const visible = levelFilter === "all"
    ? INDICATORS
    : INDICATORS.filter((i) => i.level === levelFilter);
  // The "Ajouter" button only makes sense on a real (DB-backed) project
  // AND for users who have the indicators.edit permission (or admin /
  // super-admin). Falling back to canEditIndicators which is set by
  // ProjectDetail/App.jsx from screenProps; older invocation sites
  // without the prop are treated as "no permission" — fail closed.
  const mayEdit = !!isSuperAdmin || !!isAdmin || !!canEditIndicators
    || (typeof hasPerm === "function" && (hasPerm("indicators.edit") || hasPerm("users.manage")));
  const canAdd = !!projectCode && !!window.CreateIndicatorModal && mayEdit;
  return (
    <div className="card">
      <div className="card-head"><div className="card-title">{lang === "fr" ? "Cadre logique — Indicateurs" : "Logframe — Indicators"}</div>
        <div className="row gap-sm" style={{ alignItems: "center" }}>
          {/* Inline filter dropdown — replaces the previous placeholder
              <button>Filtrer</button>. Compact + accessible without a
              modal layer. */}
          <Icon.filter />
          <select value={levelFilter} onChange={(e) => setLevelFilter(e.target.value)}
            style={{ padding: "3px 6px", fontSize: 11.5, borderRadius: 4, border: "1px solid var(--line)", background: "var(--bg)", color: "var(--text)" }}>
            <option value="all">{lang === "fr" ? "Tous niveaux" : "All levels"}</option>
            <option value="output">{lang === "fr" ? "Produit" : "Output"}</option>
            <option value="outcome">{lang === "fr" ? "Effet" : "Outcome"}</option>
            <option value="impact">Impact</option>
            <option value="context">Context</option>
          </select>
          {/* Render the Ajouter button only when the user has the
              indicators.edit permission. For non-editors we hide it
              entirely rather than show a disabled stub — they're not
              expected to create indicators at all, and a greyed-out
              button on every page would just be visual noise. */}
          {mayEdit && (
            <button className="btn xs" onClick={() => setAddOpen(true)} disabled={!canAdd}
              title={canAdd ? "" : (lang === "fr" ? "Démo — ouvrez un vrai projet pour ajouter" : "Demo — open a real project to add")}>
              <Icon.plus /> {lang === "fr" ? "Ajouter" : "Add"}
            </button>
          )}
        </div>
      </div>
      {addOpen && window.CreateIndicatorModal && (() => {
        const CIM = window.CreateIndicatorModal;
        return (
          <CIM
            lang={lang}
            defaultProject={projectCode}
            onClose={() => setAddOpen(false)}
            onCreated={async () => { if (onChanged) await onChanged(); setAddOpen(false); }}
          />
        );
      })()}
      <div className="card-body flush">
        <table className="tbl">
          <thead><tr>
            <th style={{ width: 60 }}>Code</th>
            <th style={{ width: 70 }}>{lang === "fr" ? "Niveau" : "Level"}</th>
            <th>{lang === "fr" ? "Libellé" : "Name"}</th>
            <th className="num">{lang === "fr" ? "Base" : "Base"}</th>
            <th className="num">{lang === "fr" ? "Actuel" : "Current"}</th>
            <th className="num">{lang === "fr" ? "Cible" : "Target"}</th>
            <th>{lang === "fr" ? "Tendance" : "Trend"}</th>
            <th>{lang === "fr" ? "Progrès" : "Progress"}</th>
            <th>Statut</th>
          </tr></thead>
          <tbody>
            {visible.length === 0 && (
              <tr><td colSpan={9} style={{ padding: 18, textAlign: "center", color: "var(--text-faint)", fontSize: 12 }}>
                {lang === "fr" ? "Aucun indicateur à ce niveau." : "No indicator at this level."}
              </td></tr>
            )}
            {visible.map((ind) => {
              const pct = Math.min(100, Math.round((ind.cur - ind.base) / (ind.target - ind.base) * 100));
              return (
                <tr key={ind.code}>
                  <td className="mono text-faint">{ind.code}</td>
                  <td><span className={"pill xs " + (ind.level === "outcome" ? "violet" : "")} style={{ fontSize: 9.5 }}>{ind.level === "outcome" ? (lang === "fr" ? "Effet" : "Outcome") : (lang === "fr" ? "Produit" : "Output")}</span></td>
                  <td className="strong">{ind.name}</td>
                  <td className="num mono muted">{ind.base}{ind.unit}</td>
                  <td className="num mono strong">{ind.cur}{ind.unit}</td>
                  <td className="num mono">{ind.target}{ind.unit}</td>
                  <td><PDSpark data={ind.trend} tone={ind.status} /></td>
                  <td><div className="row gap-sm"><div className="bar" style={{ width: 80 }}><div className="bar-fill" style={{ width: pct + "%", background: ind.status === "ok" ? "var(--green)" : "var(--amber)" }}></div></div><span className="mono num-sm">{pct}%</span></div></td>
                  <td>{ind.status === "ok" ? <span className="pill green dot">OK</span> : <span className="pill amber dot">{lang === "fr" ? "Vigilance" : "Watch"}</span>}</td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

function PDSites({ lang, SITES: SITES_OVERRIDE }) {
  const { useState: useStatePDS } = React;
  const DEFAULT_SITES = [
    { c: "ML", n: "CSCom Tombouctou", t: "Centre", reg: "Tombouctou", b: "8 412", s: "ok", ind: 11 },
    { c: "ML", n: "CSCom Gao Centre", t: "Centre", reg: "Gao", b: "12 207", s: "warn", ind: 11 },
    { c: "ML", n: "CSCom Mopti Sud", t: "Centre", reg: "Mopti", b: "9 845", s: "ok", ind: 11 },
    { c: "BF", n: "CSPS Dori", t: "Centre", reg: "Sahel", b: "6 102", s: "bad", ind: 11 },
    { c: "BF", n: "CSPS Djibo", t: "Centre", reg: "Sahel", b: "7 538", s: "warn", ind: 11 },
    { c: "BF", n: "CSPS Ouahigouya", t: "Centre", reg: "Nord", b: "11 247", s: "ok", ind: 11 },
    { c: "NE", n: "CSI Tillabéri", t: "Centre", reg: "Tillabéri", b: "10 822", s: "ok", ind: 11 },
    { c: "NE", n: "CSI Tahoua", t: "Centre", reg: "Tahoua", b: "13 506", s: "ok", ind: 11 },
    { c: "NE", n: "CSI Maradi-1", t: "Centre", reg: "Maradi", b: "15 213", s: "warn", ind: 11 },
    { c: "ML", n: "CSCom Ségou", t: "Centre", reg: "Ségou", b: "8 117", s: "ok", ind: 11 },
    { c: "ML", n: "CSCom Kayes", t: "Centre", reg: "Kayes", b: "9 388", s: "ok", ind: 11 },
    { c: "BF", n: "CSPS Kaya", t: "Centre", reg: "Centre-Nord", b: "10 042", s: "ok", ind: 11 },
  ];
  const SITES = SITES_OVERRIDE || DEFAULT_SITES;
  const isPort = !!SITES_OVERRIDE;
  const countryName = (c) => ({ ML: "Mali", BF: "Burkina Faso", NE: "Niger", SN: "Sénégal" }[c] || c);
  // 3-way view toggle: 'map' (regional groups) / 'cards' (grid of tiles) /
  // 'table' (default). Stored locally — no persistence needed.
  const [siteView, setSiteView] = useStatePDS("table");
  const stateLabel = (s) => s === "ok" ? (lang === "fr" ? "OK" : "OK")
                          : s === "warn" ? (lang === "fr" ? "Vigilance" : "Watch")
                          : (lang === "fr" ? "Risque" : "Risk");
  const statePillClass = (s) => s === "ok" ? "pill green dot"
                              : s === "warn" ? "pill amber dot"
                              : "pill red dot";
  return (
    <div className="card">
      <div className="card-head"><div className="card-title">{isPort ? (lang === "fr" ? "Postes à quai & terminaux" : "Berths & terminals") : (lang === "fr" ? "Sites couverts" : "Covered sites")} <span className="muted">· {SITES.length}</span></div>
        <div className="seg sm">
          <button className={"seg-btn" + (siteView === "map" ? " active" : "")}
                  onClick={() => setSiteView("map")}
                  title={lang === "fr" ? "Vue carte (regroupement par région)" : "Map view (grouped by region)"}>
            <Icon.map />
          </button>
          <button className={"seg-btn" + (siteView === "cards" ? " active" : "")}
                  onClick={() => setSiteView("cards")}
                  title={lang === "fr" ? "Vue vignettes" : "Card view"}>
            <Icon.layout />
          </button>
          <button className={"seg-btn" + (siteView === "table" ? " active" : "")}
                  onClick={() => setSiteView("table")}
                  title={lang === "fr" ? "Vue tableau" : "Table view"}>
            <Icon.spreadsheet />
          </button>
        </div>
      </div>
      <div className={"card-body" + (siteView === "table" ? " flush" : "")} style={siteView !== "table" ? { padding: 14 } : null}>
        {siteView === "table" && (
          <>
            <table className="tbl">
              <thead><tr>
                <th>{isPort ? (lang === "fr" ? "Poste / Terminal" : "Berth / Terminal") : (lang === "fr" ? "Site" : "Site")}</th>
                <th>{isPort ? (lang === "fr" ? "Type" : "Type") : (lang === "fr" ? "Pays" : "Country")}</th>
                <th>{isPort ? (lang === "fr" ? "Zone" : "Zone") : (lang === "fr" ? "Région" : "Region")}</th>
                <th className="num">{isPort ? (lang === "fr" ? "Volume / trafic" : "Volume / traffic") : (lang === "fr" ? "Bénéficiaires" : "Beneficiaries")}</th>
                <th className="num">{lang === "fr" ? "Indicateurs" : "Indicators"}</th>
                <th>{lang === "fr" ? "État" : "State"}</th>
              </tr></thead>
              <tbody>
                {SITES.map((s, i) => (
                  <tr key={i}>
                    <td><span className="row gap-xs">{!isPort && <span className="mono" style={{ fontSize: 11, background: "var(--bg-sunken)", padding: "1px 5px", borderRadius: 3, color: "var(--text-faint)" }}>{s.c}</span>}<span className="strong">{s.n}</span></span></td>
                    <td className="muted">{isPort ? s.t : countryName(s.c)}</td>
                    <td className="muted">{s.reg}</td>
                    <td className="num mono">{s.b}</td>
                    <td className="num mono">{s.ind}</td>
                    <td><span className={statePillClass(s.s)}>{stateLabel(s.s)}</span></td>
                  </tr>
                ))}
              </tbody>
            </table>
            {!isPort && (
              <div className="text-faint" style={{ padding: "10px 12px", fontSize: 11.5, borderTop: "1px solid var(--line-faint)" }}>
                {lang === "fr" ? "30 sites supplémentaires non affichés — utilisez la recherche ou les filtres." : "30 more sites not shown — use search or filters."}
              </div>
            )}
          </>
        )}
        {siteView === "cards" && (
          <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(220px, 1fr))", gap: 10 }}>
            {SITES.map((s, i) => (
              <div key={i} className="card" style={{ padding: 12, margin: 0 }}>
                <div style={{ display: "flex", alignItems: "flex-start", gap: 8, marginBottom: 8 }}>
                  {!isPort && (
                    <span className="mono" style={{ fontSize: 10, background: "var(--bg-sunken)", padding: "2px 6px", borderRadius: 3, color: "var(--text-faint)" }}>
                      {s.c}
                    </span>
                  )}
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="strong" style={{ fontSize: 13, lineHeight: 1.3 }}>{s.n}</div>
                    <div className="text-faint" style={{ fontSize: 11, marginTop: 2 }}>
                      {isPort ? s.t : countryName(s.c)} · {s.reg}
                    </div>
                  </div>
                </div>
                <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 11 }}>
                  <div>
                    <span className="mono">{s.b}</span>{" "}
                    <span className="text-faint">{isPort ? (lang === "fr" ? "trafic" : "traffic") : (lang === "fr" ? "bénéf." : "benef.")}</span>
                  </div>
                  <span className={statePillClass(s.s)} style={{ fontSize: 9 }}>{stateLabel(s.s)}</span>
                </div>
                <div className="text-faint" style={{ fontSize: 10.5, marginTop: 4 }}>
                  {s.ind} {lang === "fr" ? "indicateurs" : "indicators"}
                </div>
              </div>
            ))}
          </div>
        )}
        {siteView === "map" && (() => {
          // Group sites by region (since real coords aren't on the schema).
          // Each region renders as a "bubble" sized by site count, coloured
          // by the worst-state site in the group. Gives a quick visual scan
          // of where the project's footprint is + where attention is needed.
          const regions = {};
          SITES.forEach((s) => {
            const key = (isPort ? s.t : countryName(s.c)) + " · " + s.reg;
            if (!regions[key]) regions[key] = { country: isPort ? s.t : countryName(s.c), reg: s.reg, sites: [], worst: "ok" };
            regions[key].sites.push(s);
            if (s.s === "bad" || (s.s === "warn" && regions[key].worst === "ok")) regions[key].worst = s.s;
          });
          const groups = Object.values(regions);
          return (
            <div>
              <div className="text-faint" style={{ fontSize: 11.5, marginBottom: 10 }}>
                {lang === "fr"
                  ? "Vue carte simplifiée — regroupement par région (coordonnées GPS non disponibles pour les sites de ce projet)."
                  : "Simplified map view — grouped by region (GPS coordinates not available on this project's sites)."}
              </div>
              <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))", gap: 10 }}>
                {groups.map((g, i) => {
                  const bg = g.worst === "bad" ? "#fee2e2" : g.worst === "warn" ? "#fef3c7" : "#dcfce7";
                  const border = g.worst === "bad" ? "#dc2626" : g.worst === "warn" ? "#d97706" : "#059669";
                  return (
                    <div key={i} style={{
                      padding: 12, borderRadius: 8, background: bg,
                      border: "2px solid " + border, position: "relative",
                    }}>
                      <div style={{ position: "absolute", top: 8, right: 8, fontSize: 18, fontWeight: 700, color: border }}>
                        {g.sites.length}
                      </div>
                      <div style={{ fontSize: 11, color: "var(--text-faint)", marginBottom: 2 }}>{g.country}</div>
                      <div className="strong" style={{ fontSize: 13 }}>{g.reg}</div>
                      <div style={{ fontSize: 10.5, marginTop: 6 }}>
                        {g.sites.filter((s) => s.s === "ok").length}{" "}
                        <span className="text-faint">{lang === "fr" ? "OK" : "OK"}</span>
                        {g.sites.filter((s) => s.s === "warn").length > 0 && (
                          <> · {g.sites.filter((s) => s.s === "warn").length}{" "}
                            <span className="text-faint">{lang === "fr" ? "veille" : "watch"}</span></>
                        )}
                        {g.sites.filter((s) => s.s === "bad").length > 0 && (
                          <> · {g.sites.filter((s) => s.s === "bad").length}{" "}
                            <span className="text-faint">{lang === "fr" ? "risque" : "risk"}</span></>
                        )}
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          );
        })()}
      </div>
    </div>
  );
}

function PDBudget({ P, lang, LINES: LINES_OVERRIDE, fmtM, projectUuid }) {
  const { useState } = React;
  if (!fmtM) fmtM = (v) => (v != null ? (Number(v).toFixed(2) + " M€") : "—");
  const DEFAULT_LINES = [
    { c: lang === "fr" ? "Équipements médicaux" : "Medical equipment", b: 0.92, d: 0.78, e: 0.04 },
    { c: lang === "fr" ? "Chaîne du froid" : "Cold chain", b: 0.58, d: 0.51, e: 0.01 },
    { c: lang === "fr" ? "Formation agents" : "Staff training", b: 0.42, d: 0.31, e: 0.06 },
    { c: lang === "fr" ? "Médicaments essentiels" : "Essential medicines", b: 0.68, d: 0.34, e: 0.18 },
    { c: lang === "fr" ? "Réhabilitation CSCom" : "CSCom rehabilitation", b: 0.38, d: 0.14, e: 0.20 },
    { c: lang === "fr" ? "Personnel projet" : "Project staff", b: 0.22, d: 0.11, e: 0.00 },
    { c: lang === "fr" ? "Suivi & évaluation" : "M&E", b: 0.10, d: 0.05, e: 0.01 },
    { c: lang === "fr" ? "Frais de gestion" : "Management overhead", b: 0.06, d: 0.03, e: 0.00 },
  ];

  // Live data from budget_lines kicks in when projectUuid is present.
  // For demo / standalone pages we keep the fixture so the burn chart
  // remains representative.
  const live = window.melr && window.melr.useProjectBudget
    ? window.melr.useProjectBudget(projectUuid || null)
    : { data: [], loading: false, refresh: () => {} };
  const useLive = !!projectUuid;
  const [editing, setEditing] = useState(null); // null | 'new' | row object
  const [busy, setBusy] = useState(false);

  // Live amounts are stored as raw <native currency> in budget_lines.
  // The fixture is already expressed in millions. Convert before
  // showing so both code paths can share the same renderer.
  const liveLines = (live.data || []).map((r) => ({
    id: r.id,
    c:  r.category + (r.label ? " — " + r.label : ""),
    b:  (Number(r.budget)    || 0) / 1_000_000,
    d:  (Number(r.disbursed) || 0) / 1_000_000,
    e:  Math.max(0, ((Number(r.committed) || 0) - (Number(r.disbursed) || 0)) / 1_000_000),
    _raw: r,
  }));
  const fixtureLines = (LINES_OVERRIDE || DEFAULT_LINES).map((l, i) => ({ id: "demo-" + i, ...l, _raw: null }));
  const LINES = useLive ? liveLines : fixtureLines;

  // Roll up live lines into totals so the footer matches what's in the
  // table. For demo we keep the project-level numbers (P.budget / P.disbursed
  // / P.committed) since those drive the burn chart on the right.
  const totals = useLive
    ? {
        budget:    LINES.reduce((s, l) => s + (l.b || 0), 0),
        disbursed: LINES.reduce((s, l) => s + (l.d || 0), 0),
        committed: LINES.reduce((s, l) => s + (l.d || 0) + (l.e || 0), 0),
      }
    : { budget: P.budget, disbursed: P.disbursed, committed: P.committed };

  const onDelete = async (row) => {
    if (!row._raw) return;
    if (!confirm(lang === "fr" ? `Supprimer la ligne « ${row.c} » ?` : `Delete line "${row.c}"?`)) return;
    setBusy(true);
    try { await window.melr.budgetLinesCrud.remove(row._raw.id); await live.refresh(); }
    catch (e) { alert(e.message); }
    finally { setBusy(false); }
  };

  return (
    <>
    <div className="grid cols-3" style={{ gridTemplateColumns: "1.5fr 1fr", gap: 14 }}>
      <div className="card">
        <div className="card-head">
          <div className="card-title">
            {lang === "fr" ? "Budget par catégorie" : "Budget by category"}
            <span className="muted"> · {LINES.length}</span>
            <span className="pill" style={{
              marginLeft: 8, fontSize: 10, padding: "2px 8px",
              background: "var(--brand-tint, #eff6ff)", color: "var(--brand, #1d4ed8)",
            }} title={lang === "fr" ? "Devise des montants" : "Amount currency"}>
              {P.nativeCurrency || "EUR"}
            </span>
            {!useLive && (
              <span className="pill" style={{ marginLeft: 8, fontSize: 10, background: "var(--bg-sunken)", color: "var(--text-faint)" }}>
                {lang === "fr" ? "Démo" : "Demo"}
              </span>
            )}
          </div>
          {useLive && (
            <button className="btn xs primary" onClick={() => setEditing("new")} disabled={busy}>
              <Icon.plus /> {lang === "fr" ? "Ajouter" : "Add"}
            </button>
          )}
        </div>
        <div className="card-body flush">
          {useLive && live.loading && (
            <div style={{ padding: 18, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
              {lang === "fr" ? "Chargement…" : "Loading…"}
            </div>
          )}
          {useLive && !live.loading && LINES.length === 0 && (
            <div style={{ padding: 22, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
              {lang === "fr"
                ? "Aucune ligne budgétaire. Cliquez « Ajouter » pour démarrer."
                : "No budget line. Click 'Add' to start."}
            </div>
          )}
          {LINES.length > 0 && (
            <table className="tbl">
              <thead><tr>
                <th>{lang === "fr" ? "Poste" : "Line"}</th>
                <th className="num">{lang === "fr" ? "Budget" : "Budget"} <span className="text-faint" style={{ fontSize: 10 }}>(M{P.nativeCurrency || "EUR"})</span></th>
                <th className="num">{lang === "fr" ? "Décaissé" : "Disbursed"} <span className="text-faint" style={{ fontSize: 10 }}>(M{P.nativeCurrency || "EUR"})</span></th>
                <th className="num">{lang === "fr" ? "Engagé" : "Committed"} <span className="text-faint" style={{ fontSize: 10 }}>(M{P.nativeCurrency || "EUR"})</span></th>
                <th>{lang === "fr" ? "Exécution" : "Burn"}</th>
                {useLive && <th style={{ width: 70 }}></th>}
              </tr></thead>
              <tbody>
                {LINES.map((l) => {
                  const pct = l.b > 0 ? Math.round((l.d / l.b) * 100) : 0;
                  return (
                    <tr key={l.id}>
                      <td className="strong">{l.c}</td>
                      <td className="num mono">{fmtM(l.b)}</td>
                      <td className="num mono">{fmtM(l.d)}</td>
                      <td className="num mono">{fmtM(l.d + l.e)}</td>
                      <td><div className="row gap-sm"><div className="bar" style={{ width: 100 }}><div className="bar-fill" style={{ width: Math.min(100, pct) + "%", background: pct > 70 ? "var(--green)" : pct > 35 ? "var(--accent)" : "var(--amber)" }}></div></div><span className="mono num-sm" style={{ minWidth: 30 }}>{pct}%</span></div></td>
                      {useLive && (
                        <td className="num">
                          <button className="iconbtn" disabled={!l._raw || busy}
                            title={lang === "fr" ? "Modifier" : "Edit"}
                            onClick={() => setEditing(l._raw)}>
                            <Icon.edit />
                          </button>
                          <button className="iconbtn" disabled={!l._raw || busy}
                            title={lang === "fr" ? "Supprimer" : "Delete"}
                            onClick={() => onDelete(l)}>
                            <Icon.trash />
                          </button>
                        </td>
                      )}
                    </tr>
                  );
                })}
                <tr className="totals">
                  <td className="strong">{lang === "fr" ? "Total" : "Total"}</td>
                  <td className="num mono strong">{fmtM(totals.budget, P.nativeCurrency)}</td>
                  <td className="num mono strong">{fmtM(totals.disbursed, P.nativeCurrency)}</td>
                  <td className="num mono strong">{fmtM(totals.committed, P.nativeCurrency)}</td>
                  <td className="mono">{totals.budget > 0 ? Math.round(totals.disbursed / totals.budget * 100) : 0}%</td>
                  {useLive && <td></td>}
                </tr>
              </tbody>
            </table>
          )}
        </div>
      </div>

      <div className="card">
        <div className="card-head"><div className="card-title">{lang === "fr" ? "Décaissement vs plan" : "Disbursement vs plan"}</div></div>
        <div className="card-body">
          <div className="pd-burn">
            <svg viewBox="0 0 320 180" style={{ width: "100%", height: 180 }}>
              <defs>
                <linearGradient id="burnG" x1="0" x2="0" y1="0" y2="1">
                  <stop offset="0" stopColor="var(--accent)" stopOpacity="0.35" />
                  <stop offset="1" stopColor="var(--accent)" stopOpacity="0" />
                </linearGradient>
              </defs>
              <line x1="0" y1="150" x2="320" y2="150" stroke="var(--line)" />
              <polyline points="0,150 30,138 60,124 90,108 120,90 150,75 180,62 210,52 240,42 270,35 300,30 320,28" fill="none" stroke="var(--text-faint)" strokeDasharray="4 3" strokeWidth="1.5" />
              <polyline points="0,150 30,140 60,128 90,112 120,98 150,84 180,72 210,62" fill="none" stroke="var(--accent)" strokeWidth="2" />
              <path d="M0,150 L30,140 60,128 90,112 120,98 150,84 180,72 210,62 L210,150 L0,150 Z" fill="url(#burnG)" />
              <circle cx="210" cy="62" r="3.5" fill="var(--accent)" />
              <text x="216" y="58" fontSize="10" fill="var(--accent)" fontWeight="600">{fmtM(P.disbursed, P.nativeCurrency)}</text>
            </svg>
            <div className="row gap-md" style={{ fontSize: 11.5, marginTop: 10 }}>
              <span className="row gap-xs"><span style={{ width: 12, height: 2, background: "var(--accent)" }}></span>{lang === "fr" ? "Réalisé" : "Actual"}</span>
              <span className="row gap-xs"><span style={{ width: 12, height: 2, background: "var(--text-faint)", borderTop: "1px dashed" }}></span>{lang === "fr" ? "Plan" : "Plan"}</span>
            </div>
          </div>
        </div>
      </div>
    </div>

    {editing && (
      <PDBudgetLineModal
        lang={lang}
        projectUuid={projectUuid}
        row={editing === "new" ? null : editing}
        currency={P.nativeCurrency || "EUR"}
        onClose={() => setEditing(null)}
        onSaved={async () => { await live.refresh(); setEditing(null); }}
      />
    )}
    </>
  );
}

// =====================================================================
// Budget category catalog — francophone NGO / development-bank
// accounting buckets. Two top-level groups (CAPEX = investment / OPEX
// = recurring) with French-first sub-categories. Stored as a flat
// "CAPEX › Immobilisations corporelles › Équipements" string in
// project_team.role_label-style, so we keep the existing single text
// column and can recover the breadcrumb client-side.
//
// Tweak / extend without breaking existing rows: any value not in the
// catalogue is treated as a free-text custom category.
// =====================================================================
const BUDGET_CATEGORY_CATALOG = [
  {
    group: "CAPEX",
    fr: "CAPEX — Investissements",
    en: "CAPEX — Investments",
    items: [
      { id: "capex_intangible_studies",    fr: "Immo. incorporelles · frais d'études",        en: "Intangibles · study fees" },
      { id: "capex_intangible_notary",     fr: "Immo. incorporelles · frais de notaire",      en: "Intangibles · notary fees" },
      { id: "capex_intangible_licenses",   fr: "Immo. incorporelles · licences / logiciels",  en: "Intangibles · licenses / software" },
      { id: "capex_intangible_other",      fr: "Immo. incorporelles · autres",                en: "Intangibles · other" },
      { id: "capex_construction",          fr: "Immo. corporelles · construction / BTP",      en: "Tangibles · construction / civil works" },
      { id: "capex_rehabilitation",        fr: "Immo. corporelles · réhabilitation",          en: "Tangibles · rehabilitation" },
      { id: "capex_equipment",             fr: "Immo. corporelles · équipements",             en: "Tangibles · equipment" },
      { id: "capex_furniture",             fr: "Immo. corporelles · mobilier / aménagement",  en: "Tangibles · furniture / fit-out" },
      { id: "capex_vehicles",              fr: "Immo. corporelles · matériel roulant",        en: "Tangibles · vehicles / rolling stock" },
      { id: "capex_it",                    fr: "Immo. corporelles · informatique / IT",       en: "Tangibles · IT hardware" },
      { id: "capex_financial",             fr: "Immo. financières · titres / cautions",       en: "Financial · securities / deposits" },
    ],
  },
  {
    group: "OPEX",
    fr: "OPEX — Charges d'exploitation",
    en: "OPEX — Operating expenses",
    items: [
      { id: "opex_staff",                  fr: "Personnel projet · salaires & charges",       en: "Project staff · salaries & charges" },
      { id: "opex_consultants",            fr: "Consultants / expertise externe",             en: "Consultants / external expertise" },
      { id: "opex_training",               fr: "Formation & coaching",                        en: "Training & coaching" },
      { id: "opex_supplies",               fr: "Fournitures & consommables",                  en: "Supplies & consumables" },
      { id: "opex_medical_supplies",       fr: "Médicaments & intrants médicaux",             en: "Medicines & medical inputs" },
      { id: "opex_rent",                   fr: "Loyers & charges immobilières",               en: "Rent & property charges" },
      { id: "opex_utilities",              fr: "Eau, électricité, télécoms",                  en: "Utilities (water/elec/telecom)" },
      { id: "opex_transport",              fr: "Transport, déplacements, missions",           en: "Transport, travel, missions" },
      { id: "opex_per_diem",               fr: "Per diems & frais de séjour",                 en: "Per diems & subsistence" },
      { id: "opex_services",               fr: "Services externes (audit, légal, banque)",    en: "External services (audit, legal, banking)" },
      { id: "opex_maintenance",            fr: "Entretien & maintenance",                     en: "Maintenance & upkeep" },
      { id: "opex_communication",          fr: "Communication & reporting",                   en: "Communication & reporting" },
      { id: "opex_me",                     fr: "Suivi & évaluation (S&E)",                    en: "M&E activities" },
      { id: "opex_overhead",               fr: "Frais de gestion / overhead",                 en: "Management overhead" },
      { id: "opex_contingency",            fr: "Provisions / imprévus",                       en: "Contingency / provisions" },
      { id: "opex_other",                  fr: "Autres charges",                              en: "Other expenses" },
    ],
  },
];

// Resolve a stored category string to a catalogue entry (when it
// matches one of our ids or labels). Returns null for free-text values.
function resolveBudgetCategory(stored) {
  if (!stored) return null;
  const s = String(stored).trim();
  for (const g of BUDGET_CATEGORY_CATALOG) {
    for (const it of g.items) {
      if (it.id === s || it.fr === s || it.en === s) return { group: g, item: it };
    }
  }
  return null;
}

// =====================================================================
// PDBudgetLineModal — create / edit a row in public.budget_lines.
// Amounts are entered in the project's native currency (NOT millions),
// matching how the DB stores them. The PDBudget table displays them in
// millions via fmtM(). Category is a 2-level dropdown sourced from
// BUDGET_CATEGORY_CATALOG with a free-text fallback for legacy /
// custom entries.
// =====================================================================
function PDBudgetLineModal({ lang, projectUuid, row, currency, onClose, onSaved }) {
  const { useState, useMemo } = React;
  const isEdit = !!row;

  // Decompose the stored category: if it matches a catalogue entry,
  // pre-fill both the group + sub-category. Otherwise drop into
  // free-text mode so the user can still edit legacy / one-off rows.
  const initialResolved = useMemo(() => resolveBudgetCategory(row && row.category), [row]);
  const initialMode = initialResolved ? "catalog" : (row && row.category ? "custom" : "catalog");

  const [mode, setMode] = useState(initialMode);
  const [groupCode, setGroupCode] = useState(initialResolved ? initialResolved.group.group : "");
  const [itemId, setItemId]       = useState(initialResolved ? initialResolved.item.id : "");
  const [customCategory, setCustomCategory] = useState(
    !initialResolved && row && row.category ? row.category : ""
  );

  const [label, setLabel]         = useState(row ? (row.label || "") : "");
  const [budget, setBudget]       = useState(row ? (row.budget != null ? String(row.budget) : "") : "");
  const [disbursed, setDisbursed] = useState(row ? (row.disbursed != null ? String(row.disbursed) : "") : "");
  const [committed, setCommitted] = useState(row ? (row.committed != null ? String(row.committed) : "") : "");
  const [position, setPosition]   = useState(row ? (row.position != null ? String(row.position) : "0") : "0");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  // Build the category string we'll persist. Catalog mode stores the
  // localised label (e.g. "Immo. corporelles · équipements") which is
  // what we want to display in tables and exports. Free-text mode
  // stores whatever the user typed.
  const resolvedCategory = useMemo(() => {
    if (mode === "custom") return customCategory.trim();
    const g = BUDGET_CATEGORY_CATALOG.find((x) => x.group === groupCode);
    if (!g) return "";
    const it = g.items.find((x) => x.id === itemId);
    if (!it) return "";
    return lang === "fr" ? it.fr : it.en;
  }, [mode, groupCode, itemId, customCategory, lang]);

  // Sub-items list for the picked group.
  const groupItems = useMemo(() => {
    const g = BUDGET_CATEGORY_CATALOG.find((x) => x.group === groupCode);
    return g ? g.items : [];
  }, [groupCode]);

  const onSubmit = async () => {
    setErr(null);
    if (!resolvedCategory) { setErr(lang === "fr" ? "Catégorie requise." : "Category required."); return; }
    const b = budget === "" ? 0 : Number(budget);
    const d = disbursed === "" ? 0 : Number(disbursed);
    const c = committed === "" ? 0 : Number(committed);
    if (!isFinite(b) || !isFinite(d) || !isFinite(c)) { setErr(lang === "fr" ? "Montants invalides." : "Invalid amounts."); return; }
    if (d > c && c > 0) { setErr(lang === "fr" ? "Décaissé ne peut dépasser l'engagé." : "Disbursed cannot exceed committed."); return; }
    setBusy(true);
    try {
      const payload = {
        category:  resolvedCategory,
        label:     label.trim() || resolvedCategory,
        budget:    b,
        disbursed: d,
        committed: c || d,
        currency:  (currency || "EUR").slice(0, 3),
        position:  parseInt(position, 10) || 0,
      };
      if (isEdit) await window.melr.budgetLinesCrud.update(row.id, payload);
      else        await window.melr.budgetLinesCrud.create(projectUuid, payload);
      await onSaved();
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const lbl = { display: "block", fontSize: 10.5, color: "var(--text-faint)", marginBottom: 3, textTransform: "uppercase", letterSpacing: "0.04em", marginTop: 10 };
  const inp = { width: "100%", padding: "6px 8px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 12.5, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };

  const Modal = window.Modal;
  return (
    <Modal
      title={isEdit
        ? (lang === "fr" ? "Modifier la ligne" : "Edit line")
        : (lang === "fr" ? "Nouvelle ligne budgétaire" : "New budget line")}
      onClose={busy ? null : onClose}
      size="md"
      footer={
        <>
          <button className="btn sm" onClick={onClose} disabled={busy}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button className="btn sm primary" onClick={onSubmit} disabled={busy}>
            {busy ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
          </button>
        </>
      }
    >
      <label style={{ ...lbl, marginTop: 0 }}>{lang === "fr" ? "Catégorie" : "Category"} *</label>
      <div className="seg" style={{ marginBottom: 6 }}>
        <button type="button" className={"seg-btn" + (mode === "catalog" ? " active" : "")} onClick={() => setMode("catalog")}>
          {lang === "fr" ? "Catalogue" : "Catalog"}
        </button>
        <button type="button" className={"seg-btn" + (mode === "custom" ? " active" : "")} onClick={() => setMode("custom")}>
          {lang === "fr" ? "Personnalisé" : "Custom"}
        </button>
      </div>
      {mode === "catalog" ? (
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1.6fr", gap: 8 }}>
          <select style={inp} value={groupCode}
            onChange={(e) => { setGroupCode(e.target.value); setItemId(""); }}>
            <option value="">— {lang === "fr" ? "Groupe" : "Group"} —</option>
            {BUDGET_CATEGORY_CATALOG.map((g) => (
              <option key={g.group} value={g.group}>
                {lang === "fr" ? g.fr : g.en}
              </option>
            ))}
          </select>
          <select style={inp} value={itemId} onChange={(e) => setItemId(e.target.value)}
            disabled={!groupCode}>
            <option value="">— {lang === "fr" ? "Poste" : "Line item"} —</option>
            {groupItems.map((it) => (
              <option key={it.id} value={it.id}>{lang === "fr" ? it.fr : it.en}</option>
            ))}
          </select>
        </div>
      ) : (
        <input style={inp} value={customCategory}
          onChange={(e) => setCustomCategory(e.target.value)}
          placeholder={lang === "fr" ? "Saisissez une catégorie sur mesure" : "Enter a custom category"} />
      )}

      <label style={lbl}>{lang === "fr" ? "Libellé (optionnel)" : "Label (optional)"}</label>
      <input style={inp} value={label} onChange={(e) => setLabel(e.target.value)}
        placeholder={lang === "fr"
          ? "Détail (ex : « Antibiotiques de 1re ligne »)"
          : "Detail (e.g. 'First-line antibiotics')"} />

      <div style={{
        display: "flex", alignItems: "center", gap: 6,
        background: "var(--bg-sunken)", padding: "6px 10px", borderRadius: 6,
        fontSize: 11.5, color: "var(--text-faint)", marginTop: 10,
      }}>
        <Icon.info />
        <span>
          {lang === "fr"
            ? `Tous les montants sont en ${currency || "EUR"} brut (pas en millions). La conversion en M${(currency || "EUR")} est faite à l'affichage.`
            : `All amounts are in raw ${currency || "EUR"} (not millions). Display values are converted to M${(currency || "EUR")} automatically.`}
        </span>
      </div>

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr 80px", gap: 10 }}>
        <div>
          <label style={lbl}>{lang === "fr" ? "Budget" : "Budget"}</label>
          <input type="number" step="0.01" style={inp} value={budget}
            onChange={(e) => setBudget(e.target.value)} placeholder="0" />
        </div>
        <div>
          <label style={lbl}>{lang === "fr" ? "Engagé" : "Committed"}</label>
          <input type="number" step="0.01" style={inp} value={committed}
            onChange={(e) => setCommitted(e.target.value)} placeholder="0" />
        </div>
        <div>
          <label style={lbl}>{lang === "fr" ? "Décaissé" : "Disbursed"}</label>
          <input type="number" step="0.01" style={inp} value={disbursed}
            onChange={(e) => setDisbursed(e.target.value)} placeholder="0" />
        </div>
        <div>
          <label style={lbl}>{lang === "fr" ? "Ordre" : "Order"}</label>
          <input type="number" style={inp} value={position}
            onChange={(e) => setPosition(e.target.value)} />
        </div>
      </div>

      {err && <div style={{ color: "#b91c1c", fontSize: 12, marginTop: 10 }}>{err}</div>}
    </Modal>
  );
}

function PDTeam({ lang, TEAM: TEAM_OVERRIDE, projectUuid, projectOrgId }) {
  const { useState } = React;
  const DEFAULT_TEAM = [
    { n: "Aïssata Diallo", r: lang === "fr" ? "Chef de projet" : "Project lead", l: "Bamako", c: "ML" },
    { n: "Souleymane Touré", r: lang === "fr" ? "Responsable S&E" : "M&E officer", l: "Bamako", c: "ML" },
    { n: "Bintou Tall", r: lang === "fr" ? "Coordinatrice terrain" : "Field coordinator", l: "Tombouctou", c: "ML" },
    { n: "Yacouba Ouédraogo", r: lang === "fr" ? "Chef pays BF" : "Country lead BF", l: "Ouagadougou", c: "BF" },
    { n: "Halima Issoufou", r: lang === "fr" ? "Chef pays NE" : "Country lead NE", l: "Niamey", c: "NE" },
    { n: "Karim Bensaad", r: lang === "fr" ? "Référent qualité" : "Quality officer", l: "Bamako", c: "ML" },
    { n: "Aminata Coulibaly", r: lang === "fr" ? "Formation & coaching" : "Training & coaching", l: "Bamako", c: "ML" },
    { n: "Modou Sarr", r: lang === "fr" ? "Logistique régionale" : "Regional logistics", l: "Dakar", c: "SN" },
    { n: "Khadija Diabaté", r: lang === "fr" ? "Communication & reporting" : "Communication & reporting", l: "Bamako", c: "ML" },
    { n: "Fatima Ould Cheikh", r: lang === "fr" ? "Référente santé maternelle" : "Maternal health lead", l: "Nouakchott", c: "MR" },
  ];
  // When we have a real project, prefer live data from project_team
  // joined to profiles. Otherwise fall back to the demo team passed
  // in by PORT or the hardcoded DEFAULT_TEAM.
  const live = window.melr && window.melr.useProjectTeam
    ? window.melr.useProjectTeam(projectUuid || null)
    : { data: [], loading: false, refresh: () => {} };
  const useLive = !!projectUuid;
  const [picking, setPicking] = useState(false);
  const [busyUser, setBusyUser] = useState(null);

  const liveCards = (live.data || []).map((m) => {
    const name = (m.profile && m.profile.full_name) || (m.profile && m.profile.email) || "—";
    return {
      n: name,
      r: m.role_label || (lang === "fr" ? "Membre" : "Member"),
      l: m.profile && m.profile.email || "",
      c: "",
      _userId: m.user_id,
    };
  });
  const TEAM = useLive ? liveCards : (TEAM_OVERRIDE || DEFAULT_TEAM);

  const onRemove = async (userId, name) => {
    if (!useLive || !userId) return;
    if (!confirm(lang === "fr" ? `Retirer ${name} de l'équipe ?` : `Remove ${name} from the team?`)) return;
    setBusyUser(userId);
    try {
      await window.melr.projectTeamCrud.remove(projectUuid, userId);
      await live.refresh();
    } catch (e) {
      alert((lang === "fr" ? "Échec : " : "Failed: ") + e.message);
    } finally { setBusyUser(null); }
  };

  return (
    <>
      {useLive && (
        <div style={{ display: "flex", alignItems: "center", marginBottom: 10, gap: 8 }}>
          <span className="muted" style={{ fontSize: 12.5 }}>
            {live.loading
              ? (lang === "fr" ? "Chargement…" : "Loading…")
              : (lang === "fr" ? `${TEAM.length} membre${TEAM.length > 1 ? "s" : ""}` : `${TEAM.length} member${TEAM.length === 1 ? "" : "s"}`)}
          </span>
        </div>
      )}
      <div className="grid" style={{ gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))", gap: 12 }}>
        {TEAM.map((p, i) => (
          <div key={p._userId || i} className="card" style={{ padding: 14, display: "flex", alignItems: "center", gap: 12 }}>
            <div className="avatar" style={{ background: avColorPD(p.n), width: 42, height: 42, fontSize: 14 }}>{initialsPD(p.n)}</div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div className="strong">{p.n}</div>
              <div className="text-faint" style={{ fontSize: 11.5 }}>{p.r}</div>
              {(p.l || p.c) && (
                <div className="text-faint mono" style={{ fontSize: 10.5, marginTop: 2 }}>
                  {[p.l, p.c].filter(Boolean).join(" · ")}
                </div>
              )}
            </div>
            {useLive && p._userId ? (
              <button className="iconbtn" title={lang === "fr" ? "Retirer" : "Remove"}
                disabled={busyUser === p._userId}
                onClick={() => onRemove(p._userId, p.n)}>
                <Icon.trash />
              </button>
            ) : (
              <button className="iconbtn"><Icon.message /></button>
            )}
          </div>
        ))}
        <div
          className="card"
          style={{
            padding: 14, display: "flex", alignItems: "center", justifyContent: "center",
            border: "1px dashed var(--line-strong)", color: "var(--text-faint)",
            cursor: useLive ? "pointer" : "not-allowed",
          }}
          onClick={() => {
            if (!useLive) {
              alert(lang === "fr"
                ? "Cette page est en démo. Ouvrez un vrai projet pour gérer l'équipe."
                : "This page is a demo. Open a real project to manage the team.");
              return;
            }
            setPicking(true);
          }}
          title={useLive ? "" : (lang === "fr" ? "Démo" : "Demo")}
        >
          <Icon.plus /> &nbsp;<span style={{ fontSize: 12 }}>{lang === "fr" ? "Ajouter membre" : "Add member"}</span>
        </div>
      </div>

      {picking && (
        <PDTeamPickerModal
          lang={lang}
          projectUuid={projectUuid}
          projectOrgId={projectOrgId}
          existingUserIds={new Set((live.data || []).map((m) => m.user_id))}
          onClose={() => setPicking(false)}
          onAdded={async () => { await live.refresh(); setPicking(false); }}
        />
      )}
    </>
  );
}

// Catalogue of functional project roles. Different from the org-level
// roles table (admin / super_admin / member …): those gate tenant
// access, these describe what someone DOES on a specific project.
// The user picks 1+ via checkboxes — we persist as a comma-joined
// string in project_team.role_label (single text column).
const PROJECT_ROLE_CATALOG = [
  { id: "project_lead",      fr: "Chef de projet",           en: "Project lead" },
  { id: "me_lead",           fr: "Responsable S&E",          en: "M&E lead" },
  { id: "me_officer",        fr: "Chargé S&E",               en: "M&E officer" },
  { id: "field_coordinator", fr: "Coordinateur terrain",     en: "Field coordinator" },
  { id: "country_lead",      fr: "Chef pays",                en: "Country lead" },
  { id: "quality_officer",   fr: "Référent qualité",         en: "Quality officer" },
  { id: "training",          fr: "Formation & coaching",     en: "Training & coaching" },
  { id: "logistics",         fr: "Logistique",               en: "Logistics" },
  { id: "communication",     fr: "Communication & reporting", en: "Communication & reporting" },
  { id: "finance",           fr: "Finance",                  en: "Finance" },
  { id: "technical_expert",  fr: "Expert technique",         en: "Technical expert" },
  { id: "data_analyst",      fr: "Analyste de données",      en: "Data analyst" },
];

// Member picker: lists every member of the project's organization
// (via organization_memberships, so users with a "home" elsewhere
// who joined this org also appear) and lets the user pick one to
// add to project_team. Role is a multi-select from
// PROJECT_ROLE_CATALOG, persisted as a comma-joined string.
// Filters out anyone already on the team so we don't violate the
// (project_id, user_id) primary key.
function PDTeamPickerModal({ lang, projectUuid, projectOrgId, existingUserIds, onClose, onAdded }) {
  const { useState } = React;
  // useOrgMembershipsList queries organization_memberships joined to
  // profiles for the given org. Catches multi-org members that
  // useOrgMembers (which filters on profiles.organization_id, i.e.
  // the user's "home" org only) would miss — that was the reason
  // super-admins acting in a different org saw an empty list.
  const memListHook = window.melr && window.melr.useOrgMembershipsList;
  const { data: memberships, loading } = memListHook
    ? memListHook(projectOrgId || null)
    : { data: [], loading: false };

  const [q, setQ] = useState("");
  const [selected, setSelected] = useState(null); // profile id
  const [rolesPicked, setRolesPicked] = useState([]); // array of catalog ids
  const [extraRole, setExtraRole] = useState(""); // free text fallback
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  // De-dupe by user_id and keep the joined profile fields.
  const rosterByUser = new Map();
  for (const m of (memberships || [])) {
    if (!m.user_id || rosterByUser.has(m.user_id)) continue;
    rosterByUser.set(m.user_id, {
      id:        m.user_id,
      full_name: m.fullName || (m.profiles && m.profiles.full_name) || null,
      email:     m.email    || (m.profiles && m.profiles.email)    || null,
    });
  }
  const available = Array.from(rosterByUser.values()).filter((p) => !existingUserIds.has(p.id));

  const ql = q.trim().toLowerCase();
  const visible = ql
    ? available.filter((p) =>
        (p.full_name || "").toLowerCase().includes(ql) ||
        (p.email || "").toLowerCase().includes(ql))
    : available;

  const toggleRole = (id) => {
    setRolesPicked((cur) => {
      if (cur.includes(id)) return cur.filter((x) => x !== id);
      // Keep the saved order matching the catalog order rather than
      // click order so badges always render the same.
      const order = PROJECT_ROLE_CATALOG.map((c) => c.id);
      return [...cur, id].sort((a, b) => order.indexOf(a) - order.indexOf(b));
    });
  };

  const onSave = async () => {
    if (!selected) { setErr(lang === "fr" ? "Sélectionnez un membre." : "Pick a member."); return; }
    // Build the role_label string: catalog labels (in catalog order),
    // then any free-text extras the user typed.
    const catLabels = rolesPicked.map((id) => {
      const c = PROJECT_ROLE_CATALOG.find((x) => x.id === id);
      return c ? (lang === "fr" ? c.fr : c.en) : null;
    }).filter(Boolean);
    const extras = (extraRole || "").split(",").map((s) => s.trim()).filter(Boolean);
    const merged = [...catLabels, ...extras].join(", ");

    setBusy(true); setErr(null);
    try {
      await window.melr.projectTeamCrud.add(projectUuid, selected, merged || null);
      await onAdded();
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const Modal = window.Modal;
  return (
    <Modal
      title={lang === "fr" ? "Ajouter un membre à l'équipe" : "Add a team member"}
      onClose={busy ? null : onClose}
      size="md"
      footer={
        <>
          <button className="btn sm" onClick={onClose} disabled={busy}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button className="btn sm primary" onClick={onSave} disabled={busy || !selected}>
            {busy ? "…" : (lang === "fr" ? "Ajouter" : "Add")}
          </button>
        </>
      }
    >
      <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        {!projectOrgId && (
          <div style={{ background: "var(--bg-sunken)", padding: 8, borderRadius: 6, fontSize: 11.5, color: "var(--text-faint)" }}>
            {lang === "fr"
              ? "Organisation du projet inconnue — la liste peut être incomplète."
              : "Project organization unknown — list may be incomplete."}
          </div>
        )}

        <input
          style={{ padding: "6px 8px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 12.5 }}
          placeholder={lang === "fr" ? "Rechercher par nom ou e-mail…" : "Search by name or email…"}
          value={q} onChange={(e) => setQ(e.target.value)}
          autoFocus
        />

        <div style={{ maxHeight: 240, overflowY: "auto", border: "1px solid var(--line)", borderRadius: 6 }}>
          {loading && <div style={{ padding: 14, color: "var(--text-faint)", fontSize: 12.5, textAlign: "center" }}>{lang === "fr" ? "Chargement…" : "Loading…"}</div>}
          {!loading && visible.length === 0 && (
            <div style={{ padding: 14, color: "var(--text-faint)", fontSize: 12.5, textAlign: "center" }}>
              {available.length === 0
                ? (lang === "fr" ? "Tous les membres de l'organisation sont déjà dans l'équipe." : "All organization members are already on the team.")
                : (lang === "fr" ? "Aucun résultat." : "No results.")}
            </div>
          )}
          {!loading && visible.map((p) => {
            const sel = selected === p.id;
            return (
              <div key={p.id}
                onClick={() => setSelected(p.id)}
                style={{
                  display: "flex", alignItems: "center", gap: 10,
                  padding: "8px 10px", cursor: "pointer",
                  background: sel ? "var(--brand-tint, #eff6ff)" : "transparent",
                  borderBottom: "1px solid var(--line-faint)",
                }}>
                <input type="radio" checked={sel} readOnly />
                <div className="avatar" style={{ background: avColorPD(p.full_name || p.email || "?"), width: 28, height: 28, fontSize: 11 }}>
                  {initialsPD(p.full_name || p.email || "?")}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="strong" style={{ fontSize: 12.5 }}>{p.full_name || (lang === "fr" ? "(Sans nom)" : "(No name)")}</div>
                  <div className="text-faint mono" style={{ fontSize: 10.5 }}>{p.email}</div>
                </div>
              </div>
            );
          })}
        </div>

        <div>
          <label style={{ display: "block", fontSize: 10.5, color: "var(--text-faint)", marginBottom: 6, textTransform: "uppercase", letterSpacing: "0.04em" }}>
            {lang === "fr" ? "Rôles dans le projet (optionnel)" : "Roles on this project (optional)"}
            <span style={{ marginLeft: 6, textTransform: "none", letterSpacing: 0 }}>
              ({lang === "fr" ? "cochez plusieurs" : "tick several"})
            </span>
          </label>
          <div style={{
            display: "grid",
            gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))",
            gap: 6,
          }}>
            {PROJECT_ROLE_CATALOG.map((c) => {
              const checked = rolesPicked.includes(c.id);
              return (
                <label key={c.id} style={{
                  display: "flex", alignItems: "center", gap: 6,
                  padding: "5px 8px", border: "1px solid var(--line)", borderRadius: 6,
                  background: checked ? "var(--brand-tint, #eff6ff)" : "transparent",
                  cursor: "pointer", fontSize: 12,
                }}>
                  <input type="checkbox" checked={checked} onChange={() => toggleRole(c.id)} />
                  <span>{lang === "fr" ? c.fr : c.en}</span>
                </label>
              );
            })}
          </div>
          <input
            style={{ width: "100%", padding: "6px 8px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 12.5, boxSizing: "border-box", marginTop: 6 }}
            placeholder={lang === "fr" ? "Autres rôles (séparés par des virgules)…" : "Other roles (comma-separated)…"}
            value={extraRole} onChange={(e) => setExtraRole(e.target.value)}
          />
        </div>

        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}
      </div>
    </Modal>
  );
}

function PDFiles({ FILES, lang, projectUuid }) {
  const { useState, useRef } = React;
  // Live documents take over as soon as the project is a real DB row.
  // For fixture-only projects we keep the demo FILES list.
  const live = window.melr && window.melr.useProjectDocuments
    ? window.melr.useProjectDocuments(projectUuid || null)
    : { data: [], loading: false, refresh: () => {} };
  const useLive = !!projectUuid;
  const fileInputRef = useRef(null);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  const fmtSize = (b) => {
    if (b == null) return "—";
    if (b < 1024) return b + " B";
    if (b < 1024 * 1024) return (b / 1024).toFixed(1) + " KB";
    return (b / 1024 / 1024).toFixed(2) + " MB";
  };
  const fmtDate = (iso) => {
    if (!iso) return "—";
    try { return new Date(iso).toLocaleString(lang === "fr" ? "fr-FR" : "en-US"); }
    catch { return iso; }
  };
  // Map a mime type to the closest icon name in our Icon set.
  const iconForMime = (mime) => {
    if (!mime) return "fileText";
    if (mime.startsWith("image/")) return "image";
    if (mime.includes("pdf")) return "fileText";
    if (mime.includes("spreadsheet") || mime.includes("excel") || mime.includes("csv")) return "table";
    if (mime.includes("word") || mime.includes("document")) return "fileText";
    if (mime.includes("zip") || mime.includes("compressed")) return "archive";
    return "fileText";
  };

  const onPickFile = () => {
    if (!useLive) {
      alert(lang === "fr"
        ? "Le téléversement nécessite un projet connecté à la base. Ce projet est une démo."
        : "Uploads require a database-backed project. This one is a demo.");
      return;
    }
    fileInputRef.current && fileInputRef.current.click();
  };

  const onUpload = async (e) => {
    const file = e.target.files && e.target.files[0];
    e.target.value = "";
    if (!file || !projectUuid) return;
    setBusy(true); setErr(null);
    try {
      await window.melr.uploadProjectDocument(projectUuid, file);
      await live.refresh();
    } catch (ex) {
      setErr(ex.message);
      alert((lang === "fr" ? "Échec : " : "Failed: ") + ex.message);
    } finally { setBusy(false); }
  };

  const onDownload = async (doc) => {
    try {
      const url = await window.melr.getProjectDocumentUrl(doc.storage_path);
      if (url) window.open(url, "_blank", "noopener");
    } catch (ex) {
      alert((lang === "fr" ? "Échec : " : "Failed: ") + ex.message);
    }
  };

  const onDelete = async (doc) => {
    if (!confirm(lang === "fr" ? `Supprimer « ${doc.name} » ?` : `Delete "${doc.name}"?`)) return;
    setBusy(true);
    try {
      await window.melr.removeProjectDocument(doc.id, doc.storage_path);
      await live.refresh();
    } catch (ex) {
      alert((lang === "fr" ? "Échec : " : "Failed: ") + ex.message);
    } finally { setBusy(false); }
  };

  // Rows in display shape — either live docs or fixture entries.
  const liveRows = (live.data || []).map((d) => ({
    id:   d.id,
    name: d.name,
    size: fmtSize(d.size_bytes),
    who:  (d.profile && d.profile.full_name) || "—",
    when: fmtDate(d.uploaded_at),
    icon: iconForMime(d.mime),
    _raw: d,
  }));
  const fixtureRows = (FILES || []).map((f, i) => ({
    id:   "demo-" + i,
    name: f.n,
    size: f.s,
    who:  f.w,
    when: f.d,
    icon: f.t || "fileText",
    _raw: null,
  }));
  const rows = useLive ? liveRows : fixtureRows;

  return (
    <div className="card">
      <div className="card-head">
        <div className="card-title">
          {lang === "fr" ? "Documents" : "Documents"}
          <span className="muted"> · {rows.length}</span>
          {!useLive && (
            <span className="pill" style={{ marginLeft: 8, fontSize: 10, background: "var(--bg-sunken)", color: "var(--text-faint)" }}>
              {lang === "fr" ? "Démo" : "Demo"}
            </span>
          )}
        </div>
        <input ref={fileInputRef} type="file" style={{ display: "none" }} onChange={onUpload} />
        <button className="btn xs primary" onClick={onPickFile} disabled={busy}>
          <Icon.upload /> {busy ? (lang === "fr" ? "Envoi…" : "Uploading…") : (lang === "fr" ? "Téléverser" : "Upload")}
        </button>
      </div>
      {err && <div style={{ padding: "8px 14px", color: "#b91c1c", fontSize: 12 }}>{err}</div>}
      <div className="card-body flush">
        {useLive && live.loading && (
          <div style={{ padding: 18, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
            {lang === "fr" ? "Chargement…" : "Loading…"}
          </div>
        )}
        {useLive && !live.loading && rows.length === 0 && (
          <div style={{ padding: 22, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
            {lang === "fr"
              ? "Aucun document pour ce projet. Cliquez « Téléverser » pour ajouter une pièce jointe."
              : "No document for this project. Click 'Upload' to attach a file."}
          </div>
        )}
        {rows.length > 0 && (
          <table className="tbl">
            <thead><tr>
              <th>{lang === "fr" ? "Nom" : "Name"}</th>
              <th>{lang === "fr" ? "Taille" : "Size"}</th>
              <th>{lang === "fr" ? "Auteur" : "Owner"}</th>
              <th>{lang === "fr" ? "Modifié" : "Modified"}</th>
              <th></th>
            </tr></thead>
            <tbody>
              {rows.map((f) => {
                const Ic = Icon[f.icon] || Icon.fileText;
                return (
                  <tr key={f.id}>
                    <td><span className="row gap-xs"><Ic className="sm muted" /><span className="strong">{f.name}</span></span></td>
                    <td className="mono muted">{f.size}</td>
                    <td>{f.who}</td>
                    <td className="text-faint">{f.when}</td>
                    <td className="num">
                      {f._raw ? (
                        <>
                          <button className="iconbtn" title={lang === "fr" ? "Télécharger" : "Download"}
                            onClick={() => onDownload(f._raw)}>
                            <Icon.download />
                          </button>
                          <button className="iconbtn" title={lang === "fr" ? "Supprimer" : "Delete"}
                            onClick={() => onDelete(f._raw)} disabled={busy}>
                            <Icon.trash />
                          </button>
                        </>
                      ) : (
                        <button className="iconbtn" disabled
                          title={lang === "fr" ? "Démo — téléversez un vrai document" : "Demo — upload a real document"}>
                          <Icon.download />
                        </button>
                      )}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        )}
      </div>
    </div>
  );
}

function PDRisks({ RISKS, lang, projectUuid }) {
  const { useState } = React;
  // Live risks override the fixture as soon as we know which DB
  // project we're looking at. Demo / standalone pages keep RISKS.
  const live = window.melr && window.melr.useProjectRisks
    ? window.melr.useProjectRisks(projectUuid || null)
    : { data: [], loading: false, refresh: () => {} };
  const useLive = !!projectUuid;
  const [editing, setEditing] = useState(null); // null | 'new' | row object
  const [busy, setBusy] = useState(false);

  // Map both shapes (fixture {l,c,m,p,i} vs DB {level,title,mitigation,
  // probability,impact}) to one display shape so the table + matrix
  // can be shared between demo and live.
  const rows = useLive
    ? (live.data || []).map((r) => ({
        id: r.id,
        l:  r.level,
        c:  r.title,
        m:  r.mitigation || "",
        p:  r.probability,
        i:  r.impact,
        status: r.status,
        owner:  r.owner && r.owner.full_name,
        _raw: r,
      }))
    : (RISKS || []).map((r, i) => ({ id: "demo-" + i, l: r.l, c: r.c, m: r.m, p: r.p, i: r.i, _raw: null }));

  const onRemove = async (row) => {
    if (!row._raw) return;
    if (!confirm(lang === "fr" ? `Supprimer le risque « ${row.c} » ?` : `Delete risk "${row.c}"?`)) return;
    setBusy(true);
    try { await window.melr.risksCrud.remove(row._raw.id); await live.refresh(); }
    catch (e) { alert(e.message); }
    finally { setBusy(false); }
  };

  const levelLabel = (l) =>
    l === "H" ? (lang === "fr" ? "Élevé" : "High")
    : l === "M" ? (lang === "fr" ? "Moyen" : "Med")
    : (lang === "fr" ? "Faible" : "Low");

  return (
    <>
      <div className="grid cols-2" style={{ gridTemplateColumns: "1.4fr 1fr", gap: 14 }}>
        <div className="card">
          <div className="card-head">
            <div className="card-title">
              {lang === "fr" ? "Registre des risques" : "Risk register"}
              <span className="muted"> · {rows.length}</span>
              {!useLive && (
                <span className="pill" style={{ marginLeft: 8, fontSize: 10, background: "var(--bg-sunken)", color: "var(--text-faint)" }}>
                  {lang === "fr" ? "Démo" : "Demo"}
                </span>
              )}
            </div>
            {useLive && (
              <button className="btn xs primary" onClick={() => setEditing("new")} disabled={busy}>
                <Icon.plus /> {lang === "fr" ? "Ajouter" : "Add"}
              </button>
            )}
          </div>
          <div className="card-body flush">
            {useLive && live.loading && (
              <div style={{ padding: 18, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
                {lang === "fr" ? "Chargement…" : "Loading…"}
              </div>
            )}
            {useLive && !live.loading && rows.length === 0 && (
              <div style={{ padding: 22, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
                {lang === "fr"
                  ? "Aucun risque enregistré. Cliquez « Ajouter » pour créer le premier."
                  : "No risk recorded. Click 'Add' to create one."}
              </div>
            )}
            {rows.length > 0 && (
              <table className="tbl">
                <thead><tr>
                  <th>{lang === "fr" ? "Niveau" : "Level"}</th>
                  <th>{lang === "fr" ? "Risque" : "Risk"}</th>
                  <th>{lang === "fr" ? "Mitigation" : "Mitigation"}</th>
                  <th className="num">P</th>
                  <th className="num">I</th>
                  {useLive && <th style={{ width: 70 }}></th>}
                </tr></thead>
                <tbody>
                  {rows.map((r) => (
                    <tr key={r.id}>
                      <td><span className={"pill xs " + (r.l === "H" ? "red" : r.l === "M" ? "amber" : "green") + " dot"}>{levelLabel(r.l)}</span></td>
                      <td className="strong">{r.c}</td>
                      <td className="muted">{r.m}</td>
                      <td className="num mono">{r.p}</td>
                      <td className="num mono">{r.i}</td>
                      {useLive && (
                        <td className="num">
                          <button className="iconbtn" disabled={!r._raw || busy}
                            title={lang === "fr" ? "Modifier" : "Edit"}
                            onClick={() => setEditing(r._raw)}>
                            <Icon.edit />
                          </button>
                          <button className="iconbtn" disabled={!r._raw || busy}
                            title={lang === "fr" ? "Supprimer" : "Delete"}
                            onClick={() => onRemove(r)}>
                            <Icon.trash />
                          </button>
                        </td>
                      )}
                    </tr>
                  ))}
                </tbody>
              </table>
            )}
          </div>
        </div>
        <div className="card">
          <div className="card-head"><div className="card-title">{lang === "fr" ? "Matrice probabilité × impact" : "Probability × impact"}</div></div>
          <div className="card-body">
            <div className="pd-risk-grid">
              {[5,4,3,2,1].map((row) => (
                <React.Fragment key={row}>
                  <div className="pd-risk-axis">{row}</div>
                  {[1,2,3,4,5].map((col) => {
                    const tone = row + col >= 8 ? "red" : row + col >= 6 ? "amber" : "green";
                    const here = rows.filter((r) => r.p === col && r.i === row);
                    return (
                      <div key={col} className={"pd-risk-cell " + tone}>
                        {here.map((r, j) => <div key={j} className="pd-risk-bub">{r.l}</div>)}
                      </div>
                    );
                  })}
                </React.Fragment>
              ))}
              <div></div>
              {[1,2,3,4,5].map((c) => <div key={c} className="pd-risk-axis">{c}</div>)}
            </div>
            <div className="row" style={{ justifyContent: "space-between", marginTop: 8, fontSize: 10.5, color: "var(--text-faint)" }}>
              <span>{lang === "fr" ? "Probabilité →" : "Probability →"}</span>
              <span>↑ {lang === "fr" ? "Impact" : "Impact"}</span>
            </div>
          </div>
        </div>
      </div>

      {editing && (
        <PDRiskModal
          lang={lang}
          projectUuid={projectUuid}
          row={editing === "new" ? null : editing}
          onClose={() => setEditing(null)}
          onSaved={async () => { await live.refresh(); setEditing(null); }}
        />
      )}
    </>
  );
}

// =====================================================================
// PDRiskModal — create / edit a row in public.risks. Uses the unified
// <Modal> shell. Level (L/M/H) auto-derives from probability+impact
// (sum < 6 = Low, 6–7 = Med, ≥ 8 = High) but the user can override.
// =====================================================================
function PDRiskModal({ lang, projectUuid, row, onClose, onSaved }) {
  const { useState, useMemo } = React;
  const isEdit = !!row;

  const [title, setTitle] = useState(row ? (row.title || "") : "");
  const [mitigation, setMitigation] = useState(row ? (row.mitigation || "") : "");
  const [probability, setProbability] = useState(row ? (row.probability || 3) : 3);
  const [impact, setImpact] = useState(row ? (row.impact || 3) : 3);
  const [status, setStatus] = useState(row ? (row.status || "open") : "open");
  const [levelOverride, setLevelOverride] = useState(row ? (row.level || null) : null);
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  // Compute the auto-level the same way the matrix colours its cells:
  // sum of P+I bands the heatmap into Low/Med/High.
  const autoLevel = useMemo(() => {
    const s = (Number(probability) || 0) + (Number(impact) || 0);
    if (s >= 8) return "H";
    if (s >= 6) return "M";
    return "L";
  }, [probability, impact]);
  const level = levelOverride || autoLevel;

  const onSubmit = async () => {
    setErr(null);
    if (!title.trim()) { setErr(lang === "fr" ? "Titre requis." : "Title required."); return; }
    const p = Number(probability), i = Number(impact);
    if (!(p >= 1 && p <= 5)) { setErr(lang === "fr" ? "Probabilité 1–5." : "Probability 1-5."); return; }
    if (!(i >= 1 && i <= 5)) { setErr(lang === "fr" ? "Impact 1–5." : "Impact 1-5."); return; }
    setBusy(true);
    try {
      const payload = {
        title:       title.trim(),
        mitigation:  mitigation.trim() || null,
        probability: p,
        impact:      i,
        level:       level,
        status:      status,
      };
      if (isEdit) await window.melr.risksCrud.update(row.id, payload);
      else        await window.melr.risksCrud.create(projectUuid, payload);
      await onSaved();
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const lbl = { display: "block", fontSize: 10.5, color: "var(--text-faint)", marginBottom: 3, textTransform: "uppercase", letterSpacing: "0.04em", marginTop: 10 };
  const inp = { width: "100%", padding: "6px 8px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 12.5, background: "var(--bg, white)", color: "var(--text)", boxSizing: "border-box", fontFamily: "inherit" };

  const Modal = window.Modal;
  return (
    <Modal
      title={isEdit
        ? (lang === "fr" ? "Modifier le risque" : "Edit risk")
        : (lang === "fr" ? "Nouveau risque" : "New risk")}
      onClose={busy ? null : onClose}
      size="md"
      footer={
        <>
          <button className="btn sm" onClick={onClose} disabled={busy}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button className="btn sm primary" onClick={onSubmit} disabled={busy}>
            {busy ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
          </button>
        </>
      }
    >
      <label style={{ ...lbl, marginTop: 0 }}>{lang === "fr" ? "Titre" : "Title"} *</label>
      <input style={inp} value={title} onChange={(e) => setTitle(e.target.value)}
        placeholder={lang === "fr" ? "ex : Rupture stock vaccin Penta-3" : "e.g. Penta-3 vaccine stockout"} />

      <label style={lbl}>{lang === "fr" ? "Mitigation" : "Mitigation"}</label>
      <textarea style={{ ...inp, minHeight: 70, fontFamily: "inherit" }}
        value={mitigation} onChange={(e) => setMitigation(e.target.value)}
        placeholder={lang === "fr" ? "Actions prévues pour réduire le risque…" : "Planned actions to reduce the risk…"} />

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
        <div>
          <label style={lbl}>{lang === "fr" ? "Probabilité" : "Probability"} (1–5)</label>
          <input type="number" min={1} max={5} style={inp} value={probability}
            onChange={(e) => setProbability(e.target.value)} />
        </div>
        <div>
          <label style={lbl}>{lang === "fr" ? "Impact" : "Impact"} (1–5)</label>
          <input type="number" min={1} max={5} style={inp} value={impact}
            onChange={(e) => setImpact(e.target.value)} />
        </div>
        <div>
          <label style={lbl}>{lang === "fr" ? "Statut" : "Status"}</label>
          <select style={inp} value={status} onChange={(e) => setStatus(e.target.value)}>
            <option value="open">{lang === "fr" ? "Ouvert" : "Open"}</option>
            <option value="mitigated">{lang === "fr" ? "Atténué" : "Mitigated"}</option>
            <option value="closed">{lang === "fr" ? "Fermé" : "Closed"}</option>
          </select>
        </div>
      </div>

      <label style={lbl}>{lang === "fr" ? "Niveau" : "Level"}</label>
      <div className="seg">
        {[
          { v: null, label: (lang === "fr" ? "Auto" : "Auto") + " (" + autoLevel + ")" },
          { v: "L",  label: lang === "fr" ? "Faible" : "Low" },
          { v: "M",  label: lang === "fr" ? "Moyen"  : "Med" },
          { v: "H",  label: lang === "fr" ? "Élevé"  : "High" },
        ].map((o) => (
          <button key={String(o.v)} type="button"
            className={"seg-btn" + ((levelOverride === o.v || (!levelOverride && o.v === null)) ? " active" : "")}
            onClick={() => setLevelOverride(o.v)}>
            {o.label}
          </button>
        ))}
      </div>

      {err && <div style={{ color: "#b91c1c", fontSize: 12, marginTop: 10 }}>{err}</div>}
    </Modal>
  );
}

function PDActivity({ ACTIVITY, lang, projectUuid }) {
  const live = window.melr && window.melr.useProjectActivity
    ? window.melr.useProjectActivity(projectUuid || null, 100)
    : { data: [], loading: false, refresh: () => {} };
  const useLive = !!projectUuid;

  // Map action+entity to an icon + tone. Action verbs we expect from
  // the SECURITY DEFINER helpers: 'created', 'updated', 'deleted',
  // 'submitted', 'approved', 'rejected', 'commented', 'uploaded'.
  const ICON_FOR_ACTION = {
    created:   { t: "plus",       tone: "accent" },
    updated:   { t: "edit",       tone: "amber" },
    deleted:   { t: "trash",      tone: "red" },
    submitted: { t: "send",       tone: "accent" },
    approved:  { t: "check",      tone: "green" },
    rejected:  { t: "x",          tone: "red" },
    commented: { t: "message",    tone: "accent" },
    uploaded:  { t: "upload",     tone: "violet" },
    assigned:  { t: "user",       tone: "accent" },
  };
  const ICON_FOR_ENTITY = {
    indicator_value: "trending",
    indicator:       "trending",
    report:          "fileText",
    document:        "fileText",
    validation_item: "shieldCheck",
    plan_action:     "calendar",
    risk:            "alert",
    site:            "globe",
    project:         "folder",
    note:            "message",
  };

  // Relative time formatting — keeps the row compact.
  const fmtRel = (iso) => {
    if (!iso) return "—";
    const d = new Date(iso).getTime();
    if (isNaN(d)) return iso;
    const s = Math.floor((Date.now() - d) / 1000);
    if (s < 60)         return lang === "fr" ? "il y a quelques s" : "just now";
    if (s < 3600)       return (lang === "fr" ? "il y a " : "") + Math.floor(s / 60) + (lang === "fr" ? " min" : " min ago");
    if (s < 86400)      return (lang === "fr" ? "il y a " : "") + Math.floor(s / 3600) + (lang === "fr" ? " h" : " h ago");
    if (s < 86400 * 7)  return (lang === "fr" ? "il y a " : "") + Math.floor(s / 86400) + (lang === "fr" ? " j" : " d ago");
    return new Date(iso).toLocaleDateString(lang === "fr" ? "fr-FR" : "en-US");
  };

  // Render a single activity_logs row to the existing pd-act shape.
  const mapLive = (a) => {
    const aa = ICON_FOR_ACTION[a.action] || { t: "info", tone: "accent" };
    const icon = ICON_FOR_ENTITY[a.entity] || aa.t;
    // Verb phrasing from action + entity (best effort, falls back to
    // the raw verb so unknown events still read).
    const verbFr = {
      created: "a créé", updated: "a modifié", deleted: "a supprimé",
      submitted: "a soumis", approved: "a validé", rejected: "a rejeté",
      commented: "a commenté", uploaded: "a téléversé", assigned: "a assigné",
    }[a.action] || a.action;
    const verbEn = {
      created: "created", updated: "updated", deleted: "deleted",
      submitted: "submitted", approved: "approved", rejected: "rejected",
      commented: "commented on", uploaded: "uploaded", assigned: "assigned",
    }[a.action] || a.action;
    const entLabel = {
      indicator_value: lang === "fr" ? "une valeur d'indicateur" : "an indicator value",
      indicator:       lang === "fr" ? "un indicateur"           : "an indicator",
      report:          lang === "fr" ? "un rapport"              : "a report",
      document:        lang === "fr" ? "un document"             : "a document",
      validation_item: lang === "fr" ? "une demande de validation" : "a validation item",
      plan_action:     lang === "fr" ? "une action du plan"      : "a plan action",
      risk:            lang === "fr" ? "un risque"               : "a risk",
      site:            lang === "fr" ? "un site"                 : "a site",
      project:         lang === "fr" ? "le projet"               : "the project",
      note:            lang === "fr" ? "une note"                : "a note",
    }[a.entity] || a.entity;
    const sentence = (lang === "fr" ? verbFr : verbEn) + " " + entLabel;
    return {
      id:   a.id,
      who:  (a.actor && a.actor.full_name) || (lang === "fr" ? "Quelqu'un" : "Someone"),
      role: "",
      a:    sentence,
      w:    fmtRel(a.occurred_at),
      t:    icon,
      tone: aa.tone,
    };
  };

  const liveRows = (live.data || []).map(mapLive);
  // For demo/standalone we keep the colourful ACTIVITY fixture — but
  // we only render it when there's nothing else to show, otherwise
  // we'd mix real history with fake events.
  const fixtureRows = ACTIVITY || [];
  const rows = useLive ? liveRows : fixtureRows;

  return (
    <div className="card">
      <div className="card-head">
        <div className="card-title">
          {lang === "fr" ? "Journal d'activité" : "Activity log"}
          <span className="muted"> · {rows.length}</span>
          {!useLive && (
            <span className="pill" style={{ marginLeft: 8, fontSize: 10, background: "var(--bg-sunken)", color: "var(--text-faint)" }}>
              {lang === "fr" ? "Démo" : "Demo"}
            </span>
          )}
        </div>
        {useLive && (
          <button className="btn xs ghost" onClick={() => live.refresh()} disabled={live.loading}
            title={lang === "fr" ? "Rafraîchir" : "Refresh"}>
            <Icon.refresh /> {lang === "fr" ? "Rafraîchir" : "Refresh"}
          </button>
        )}
      </div>
      <div className="card-body">
        {useLive && live.loading && (
          <div style={{ padding: 18, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
            {lang === "fr" ? "Chargement…" : "Loading…"}
          </div>
        )}
        {useLive && !live.loading && rows.length === 0 && (
          <div style={{ padding: 22, textAlign: "center", color: "var(--text-faint)", fontSize: 12.5 }}>
            {lang === "fr"
              ? "Aucune activité enregistrée pour ce projet pour le moment."
              : "No activity recorded for this project yet."}
          </div>
        )}
        {rows.map((a) => {
          const Ic = Icon[a.t] || Icon.info;
          return (
            <div key={a.id} className="pd-act">
              <div className={"pd-act-icon tone-" + a.tone}><Ic /></div>
              <div style={{ flex: 1, fontSize: 12.5 }}>
                <div><span className="strong">{a.who}</span> <span className="muted">{a.a}</span></div>
                <div className="text-faint" style={{ fontSize: 11 }}>{a.role ? a.role + " · " : ""}{a.w}</div>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

function PDSpark({ data, tone }) {
  if (!data || !data.length) return <span className="text-faint mono" style={{ fontSize: 10.5 }}>—</span>;
  const w = 90, h = 28, max = Math.max(...data), min = Math.min(...data);
  const pts = data.map((v, i) => `${(i / (data.length - 1)) * w},${h - ((v - min) / (max - min || 1)) * (h - 4) - 2}`).join(" ");
  const color = tone === "warn" ? "var(--amber)" : tone === "bad" ? "var(--red)" : "var(--green)";
  return (
    <svg width={w} height={h} style={{ display: "block" }}>
      <polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
      <circle cx={w} cy={h - ((data[data.length - 1] - min) / (max - min || 1)) * (h - 4) - 2} r="2" fill={color} />
    </svg>
  );
}

function initialsPD(name) { return name.split(/\s+/).slice(0, 2).map((s) => s[0]).join("").toUpperCase(); }
function avColorPD(name) {
  let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) | 0;
  return `oklch(0.78 0.08 ${Math.abs(h) % 360})`;
}

window.ProjectDetail = ProjectDetail;

// ==================== CNUCED INDICATORS — Port management ====================
function PDIndicatorsCNUCED({ INDICATORS, lang }) {
  const groups = [...new Set(INDICATORS.map((i) => i.group))];

  // KPI mini-cards per group: count + average score (0–100)
  const groupStats = groups.map((g) => {
    const items = INDICATORS.filter((i) => i.group === g);
    const numericItems = items.filter((i) => !i.textual && typeof i.cur === "number" && typeof i.base === "number" && typeof i.target === "number");
    const avg = numericItems.length
      ? Math.round(numericItems.reduce((sum, i) => {
          const denom = (i.invert ? (i.base - i.target) : (i.target - i.base)) || 1;
          const num = (i.invert ? (i.base - i.cur) : (i.cur - i.base));
          return sum + Math.max(0, Math.min(1, num / denom));
        }, 0) / numericItems.length * 100)
      : null;
    return { name: g, count: items.length, avg };
  });

  return (
    <div>
      <div className="cnuced-banner">
        <div className="cnuced-banner-icon"><Icon.shieldHalf /></div>
        <div style={{ flex: 1 }}>
          <div className="strong" style={{ fontSize: 13 }}>{lang === "fr" ? "Cadre CNUCED de performance portuaire — Port Performance Scorecard (PPS)" : "UNCTAD Port Performance Scorecard (PPS)"}</div>
          <div className="muted" style={{ fontSize: 11.5 }}>
            {lang === "fr" ? "7 groupes · 94 indicateurs de référence · " + INDICATORS.length + " indicateurs suivis ce trimestre · alignement ODD 3, 5, 7, 8, 9, 11, 12, 13, 14, 16" : "7 groups · 94 reference indicators · " + INDICATORS.length + " tracked this quarter · aligned to SDGs 3, 5, 7, 8, 9, 11, 12, 13, 14, 16"}
          </div>
        </div>
        <div className="row gap-sm">
          <button className="btn xs"><Icon.download /> {lang === "fr" ? "Export CNUCED" : "Export UNCTAD"}</button>
          <button className="btn xs primary"><Icon.send /> {lang === "fr" ? "Soumettre rapport" : "Submit report"}</button>
        </div>
      </div>

      <div className="cnuced-summary">
        {groupStats.map((g) => (
          <div key={g.name} className="cnuced-kpi">
            <div className="cnuced-kpi-name">{g.name}</div>
            <div className="cnuced-kpi-v">
              {g.avg != null ? <span className={g.avg >= 70 ? "tone-green" : g.avg >= 50 ? "tone-amber" : "tone-red"} style={{ fontWeight: 600 }}>{g.avg}</span> : <span className="muted">—</span>}
              <span className="text-faint" style={{ fontSize: 11 }}>{g.avg != null ? "/100" : ""}</span>
            </div>
            <div className="text-faint" style={{ fontSize: 11 }}>{g.count} {lang === "fr" ? "indicateurs" : "indicators"}</div>
          </div>
        ))}
      </div>

      {groups.map((g) => {
        const items = INDICATORS.filter((i) => i.group === g);
        return (
          <div key={g} className="card" style={{ marginTop: 14 }}>
            <div className="card-head">
              <div className="card-title">{g}</div>
              <span className="muted" style={{ fontSize: 11.5 }}>{items.length} {lang === "fr" ? "indicateurs" : "indicators"}</span>
              <button className="btn xs ghost" style={{ marginLeft: "auto" }}><Icon.eye /> {lang === "fr" ? "Détail" : "Detail"}</button>
            </div>
            <div className="card-body flush">
              <table className="tbl cnuced-tbl">
                <thead><tr>
                  <th style={{ width: 70 }}>{lang === "fr" ? "Réf." : "Ref."}</th>
                  <th>{lang === "fr" ? "Indicateur" : "Indicator"}</th>
                  <th className="num" style={{ width: 90 }}>{lang === "fr" ? "Base" : "Base"}</th>
                  <th className="num" style={{ width: 90 }}>{lang === "fr" ? "Actuel" : "Current"}</th>
                  <th className="num" style={{ width: 90 }}>{lang === "fr" ? "Cible" : "Target"}</th>
                  <th style={{ width: 100 }}>{lang === "fr" ? "Tendance" : "Trend"}</th>
                  <th style={{ width: 120 }}>{lang === "fr" ? "Progrès" : "Progress"}</th>
                  <th style={{ width: 70 }}>ODD</th>
                  <th style={{ width: 90 }}>{lang === "fr" ? "Statut" : "Status"}</th>
                </tr></thead>
                <tbody>
                  {items.map((ind) => {
                    if (ind.textual) {
                      return (
                        <tr key={ind.code}>
                          <td className="mono text-faint">{ind.code}</td>
                          <td>
                            <div className="strong">{ind.name}</div>
                            {ind.note && <div className="text-faint" style={{ fontSize: 11 }}>{ind.note}</div>}
                          </td>
                          <td colSpan="5" className="muted" style={{ fontStyle: "italic" }}>{ind.value}</td>
                          <td className="muted mono" style={{ fontSize: 10.5 }}>{ind.odd || "—"}</td>
                          <td><span className="pill green dot">OK</span></td>
                        </tr>
                      );
                    }
                    const denom = (ind.invert ? (ind.base - ind.target) : (ind.target - ind.base)) || 1;
                    const num = (ind.invert ? (ind.base - ind.cur) : (ind.cur - ind.base));
                    const pct = Math.max(0, Math.min(100, Math.round(num / denom * 100)));
                    const fmt = (v) => {
                      if (typeof v !== "number") return v;
                      if (ind.unit === "%") return Math.round(v < 1 ? v * 100 : v) + "%";
                      return v.toLocaleString("fr-FR") + (ind.unit && ind.unit.startsWith("/") ? "" : " ") + (ind.unit || "");
                    };
                    return (
                      <tr key={ind.code}>
                        <td className="mono text-faint">{ind.code}</td>
                        <td className="strong">{ind.name}{ind.note ? <div className="text-faint" style={{ fontSize: 11, fontWeight: 400 }}>{ind.note}</div> : null}</td>
                        <td className="num mono muted">{fmt(ind.base)}</td>
                        <td className="num mono strong">{fmt(ind.cur)}</td>
                        <td className="num mono">{fmt(ind.target)}</td>
                        <td><PDSpark data={ind.trend} tone={ind.status} /></td>
                        <td><div className="row gap-sm"><div className="bar" style={{ width: 70 }}><div className="bar-fill" style={{ width: pct + "%", background: ind.status === "ok" ? "var(--green)" : ind.status === "warn" ? "var(--amber)" : "var(--red)" }}></div></div><span className="mono num-sm">{pct}%</span></div></td>
                        <td className="muted mono" style={{ fontSize: 10.5 }}>{ind.odd || "—"}</td>
                        <td>{ind.status === "ok" ? <span className="pill green dot">OK</span> : ind.status === "warn" ? <span className="pill amber dot">{lang === "fr" ? "Vigilance" : "Watch"}</span> : <span className="pill red dot">{lang === "fr" ? "Critique" : "Crit."}</span>}</td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </div>
          </div>
        );
      })}
    </div>
  );
}

window.PDIndicatorsCNUCED = PDIndicatorsCNUCED;
