/* global React, Icon, Modal, PROJECTS, sectorById, sectorLabel, projectName, getPlanFor, generatePlanFor */
const { useState: useStatePL, useMemo: useMemoPL } = React;

// ==================== PLANNING — Table + Gantt ====================

function statusPill(status, lang) {
  if (status === "done") return <span className="pill green dot">{lang === "fr" ? "Terminé" : "Done"}</span>;
  if (status === "active") return <span className="pill accent dot">{lang === "fr" ? "En cours" : "Active"}</span>;
  if (status === "late") return <span className="pill red dot">{lang === "fr" ? "En retard" : "Late"}</span>;
  return <span className="pill dot" style={{ background: "var(--bg-sunken)", color: "var(--text-faint)" }}>{lang === "fr" ? "Planifié" : "Planned"}</span>;
}

function statusColor(status) {
  if (status === "done") return "var(--green)";
  if (status === "active") return "var(--accent)";
  if (status === "late") return "var(--red)";
  return "var(--text-faint)";
}

function fmtDate(s, lang) {
  if (!s) return "—";
  const [y, m, d] = s.split("-");
  return lang === "fr" ? `${d}/${m}/${y}` : `${y}-${m}-${d}`;
}

function durationDays(a, b) {
  return Math.max(1, Math.round((new Date(b) - new Date(a)) / 86400000));
}

function durationLabel(a, b, lang) {
  const d = durationDays(a, b);
  if (d >= 60) return Math.round(d / 30) + (lang === "fr" ? " mois" : " mo");
  if (d >= 14) return Math.round(d / 7) + (lang === "fr" ? " sem" : " w");
  return d + (lang === "fr" ? " j" : " d");
}

// Plan d'action pour un projet — table + Gantt
function PDPlanning({ project, plan, lang, projectUuid, onPlanChanged }) {
  // Plan can come from the database (window.melr.usePlan, mapped in
  // screens-project.jsx) or from the in-memory fixture (data-plans.jsx).
  const [view, setView] = useStatePL("gantt"); // gantt | table
  // CRUD modals — only visible when the plan is live (projectUuid is set).
  const [phaseModal, setPhaseModal] = useStatePL(false);
  const [actionModal, setActionModal] = useStatePL(false);
  const [editingAction, setEditingAction] = useStatePL(null);
  const canEdit = !!projectUuid;
  if (!plan) return null;
  const { actions, phases } = plan;

  // KPIs
  const done = actions.filter((a) => a.status === "done").length;
  const active = actions.filter((a) => a.status === "active").length;
  const planned = actions.filter((a) => a.status === "planned").length;
  const late = actions.filter((a) => a.status === "late").length;
  const avg = actions.length > 0
    ? Math.round(actions.reduce((s, a) => s + (a.progress || 0), 0) / actions.length)
    : 0;

  return (
    <div>
      <div className="row" style={{ marginBottom: 14, gap: 10, flexWrap: "wrap" }}>
        <div className="plan-kpis">
          <div className="plan-kpi"><div className="plan-kpi-l">{lang === "fr" ? "Actions" : "Actions"}</div><div className="plan-kpi-v">{actions.length}</div></div>
          <div className="plan-kpi"><div className="plan-kpi-l">{lang === "fr" ? "Terminées" : "Done"}</div><div className="plan-kpi-v" style={{ color: "var(--green)" }}>{done}</div></div>
          <div className="plan-kpi"><div className="plan-kpi-l">{lang === "fr" ? "En cours" : "Active"}</div><div className="plan-kpi-v" style={{ color: "var(--accent)" }}>{active}</div></div>
          <div className="plan-kpi"><div className="plan-kpi-l">{lang === "fr" ? "Planifiées" : "Planned"}</div><div className="plan-kpi-v" style={{ color: "var(--text-faint)" }}>{planned}</div></div>
          {late > 0 && <div className="plan-kpi"><div className="plan-kpi-l">{lang === "fr" ? "En retard" : "Late"}</div><div className="plan-kpi-v" style={{ color: "var(--red)" }}>{late}</div></div>}
          <div className="plan-kpi"><div className="plan-kpi-l">{lang === "fr" ? "Avancement moyen" : "Avg progress"}</div><div className="plan-kpi-v">{avg}%</div></div>
        </div>
        <div className="row gap-sm" style={{ marginLeft: "auto" }}>
          <div className="seg sm">
            <button className={"seg-btn" + (view === "gantt" ? " active" : "")} onClick={() => setView("gantt")}><Icon.layout /> Gantt</button>
            <button className={"seg-btn" + (view === "table" ? " active" : "")} onClick={() => setView("table")}><Icon.spreadsheet /> {lang === "fr" ? "Tableau" : "Table"}</button>
          </div>
          {/* "Exporter" → CSV of the project's plan actions. Uses the
              shared exportCSV helper. Disabled if there's nothing to export. */}
          <button className="btn sm" disabled={actions.length === 0}
            onClick={() => {
              if (!window.melr || !window.melr.exportCSV) return;
              const phaseLabel = (pid) => {
                const ph = phases.find((p) => p.id === pid);
                return ph ? (lang === "en" ? (ph.name_en || ph.name_fr) : ph.name_fr) : "";
              };
              const rows = actions.map((a) => ({
                code:     a.code || "",
                name:     lang === "en" ? (a.name_en || a.name_fr) : a.name_fr,
                phase:    phaseLabel(a.phase_id),
                start:    a.start_date || "",
                end:      a.end_date || "",
                progress: a.progress_pct != null ? Math.round(a.progress_pct) : 0,
                status:   a.status || "",
                lead:     a.lead || "",
              }));
              const date = new Date().toISOString().slice(0, 10);
              window.melr.exportCSV("plan-actions-" + date + ".csv", rows, [
                { key: "code",     label: "Code" },
                { key: "name",     label: lang === "fr" ? "Action" : "Action" },
                { key: "phase",    label: lang === "fr" ? "Phase" : "Phase" },
                { key: "start",    label: lang === "fr" ? "Début" : "Start" },
                { key: "end",      label: lang === "fr" ? "Fin" : "End" },
                { key: "progress", label: lang === "fr" ? "Avancement %" : "Progress %" },
                { key: "status",   label: lang === "fr" ? "Statut" : "Status" },
                { key: "lead",     label: lang === "fr" ? "Responsable" : "Lead" },
              ]);
            }}>
            <Icon.download /> {lang === "fr" ? "Exporter" : "Export"}
          </button>
          {canEdit && (
            <>
              <button className="btn sm" onClick={() => setPhaseModal(true)}>
                <Icon.layers /> {lang === "fr" ? "Phase" : "Phase"}
              </button>
              <button className="btn sm primary" onClick={() => setActionModal(true)}>
                <Icon.plus /> {lang === "fr" ? "Action" : "Action"}
              </button>
            </>
          )}
        </div>
      </div>

      {view === "gantt" ? <PlanGantt actions={actions} phases={phases} lang={lang} /> : <PlanTable actions={actions} phases={phases} lang={lang} onEditAction={canEdit ? setEditingAction : null} />}

      {phaseModal && (
        <NewPhaseModal
          lang={lang}
          existingCount={phases.length}
          onClose={() => setPhaseModal(false)}
          onCreated={async (payload) => {
            try {
              await window.melr.createPlanPhase(projectUuid, payload);
              setPhaseModal(false);
              if (onPlanChanged) await onPlanChanged();
            } catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
          }}
        />
      )}
      {actionModal && (
        <NewActionModal
          lang={lang}
          phases={phases}
          existingCount={actions.length}
          onClose={() => setActionModal(false)}
          onCreated={async (payload) => {
            try {
              await window.melr.createPlanAction(projectUuid, payload);
              setActionModal(false);
              if (onPlanChanged) await onPlanChanged();
            } catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
          }}
        />
      )}
      {editingAction && (
        <EditActionModal
          lang={lang}
          action={editingAction}
          onClose={() => setEditingAction(null)}
          onSaved={async (patch) => {
            try {
              // action.uuid was added by the live mapping in screens-project.jsx;
              // if not present, do nothing (fixture action).
              if (!editingAction.uuid) { setEditingAction(null); return; }
              await window.melr.updatePlanAction(editingAction.uuid, patch);
              setEditingAction(null);
              if (onPlanChanged) await onPlanChanged();
            } catch (e) { window.alert((lang === "fr" ? "Erreur : " : "Error: ") + e.message); }
          }}
        />
      )}
    </div>
  );
}

window.PDPlanning = PDPlanning;

// ==================== PLAN CRUD MODALS ====================

const PHASE_COLORS = [
  "oklch(0.55 0.13 230)", // blue
  "oklch(0.55 0.13 145)", // green
  "oklch(0.55 0.15 290)", // violet
  "oklch(0.55 0.14 75)",  // amber
  "oklch(0.55 0.12 12)",  // red
  "oklch(0.45 0.10 0)",   // grey
];

const ACTION_STATUSES = [
  { v: "planned", fr: "Planifiée", en: "Planned" },
  { v: "active",  fr: "En cours",  en: "In progress" },
  { v: "done",    fr: "Terminée",  en: "Done" },
  { v: "late",    fr: "En retard", en: "Late" },
  { v: "cancelled", fr: "Annulée", en: "Cancelled" },
];

function ModalShell({ title, onClose, children }) {
  return (
    <div onClick={onClose} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999,
      display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: "var(--bg, white)", color: "var(--text, #111)",
        padding: 22, borderRadius: 10, width: 540, maxWidth: "92vw",
        boxShadow: "0 10px 30px rgba(0,0,0,.25)",
      }}>
        <div style={{ fontSize: 18, fontWeight: 600, marginBottom: 12 }}>{title}</div>
        {children}
      </div>
    </div>
  );
}

const planInp  = { padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 };
const planLbl  = { fontSize: 11, opacity: 0.75 };

function NewPhaseModal({ lang, existingCount, onClose, onCreated }) {
  const [code, setCode]     = useStatePL("ph" + (existingCount + 1));
  const [nameFr, setNameFr] = useStatePL("");
  const [nameEn, setNameEn] = useStatePL("");
  const [color, setColor]   = useStatePL(PHASE_COLORS[existingCount % PHASE_COLORS.length]);
  const [submitting, setSubmitting] = useStatePL(false);
  const submit = async (e) => {
    e.preventDefault();
    setSubmitting(true);
    await onCreated({ code: code.trim(), name_fr: nameFr.trim(), name_en: nameEn.trim() || nameFr.trim(), color, position: existingCount + 1 });
    setSubmitting(false);
  };
  return (
    <ModalShell lang={lang} title={lang === "fr" ? "Nouvelle phase" : "New phase"} onClose={onClose}>
      <form onSubmit={submit} style={{ display: "grid", gap: 10 }}>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Code (ex: ph6)" : "Code (e.g. ph6)"}</span>
            <input required value={code} onChange={(e) => setCode(e.target.value)} style={planInp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Couleur" : "Color"}</span>
            <select value={color} onChange={(e) => setColor(e.target.value)} style={planInp}>
              {PHASE_COLORS.map((c, i) => (
                <option key={c} value={c}>{["Bleu","Vert","Violet","Ambre","Rouge","Gris"][i]}</option>
              ))}
            </select>
          </label>
        </div>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={planLbl}>{lang === "fr" ? "Nom (FR)" : "Name (FR)"}</span>
          <input required value={nameFr} onChange={(e) => setNameFr(e.target.value)}
            placeholder={lang === "fr" ? "Démarrage, Mise en œuvre, ..." : "Inception, Implementation, ..."} style={planInp} />
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={planLbl}>{lang === "fr" ? "Nom (EN — optionnel)" : "Name (EN — optional)"}</span>
          <input value={nameEn} onChange={(e) => setNameEn(e.target.value)} style={planInp} />
        </label>
        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
          <button type="button" onClick={onClose} disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="submit" disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: "pointer", fontWeight: 600 }}>
            {submitting ? "…" : (lang === "fr" ? "Créer la phase" : "Create phase")}
          </button>
        </div>
      </form>
    </ModalShell>
  );
}

function NewActionModal({ lang, phases, existingCount, onClose, onCreated }) {
  // Only live phases have a uuid for phase_id.
  const livePhases = (phases || []).filter((p) => p.uuid || (p.id && p.id.length > 8));
  // For simplicity we use phase.id as the foreign key only if phases come
  // from the database (id is a UUID). Fixture phases (id = 'ph1') cannot be
  // referenced — disable the form in that case.
  const phasesAreLive = livePhases.length > 0 && livePhases.every((p) => p.uuid);
  const [phaseId, setPhaseId] = useStatePL(phasesAreLive ? livePhases[0].uuid : "");
  const [wbs, setWbs]         = useStatePL("");
  const [nameFr, setNameFr]   = useStatePL("");
  const [nameEn, setNameEn]   = useStatePL("");
  const today = new Date().toISOString().slice(0, 10);
  const in90  = new Date(Date.now() + 90 * 86400000).toISOString().slice(0, 10);
  const [start, setStart]     = useStatePL(today);
  const [end, setEnd]         = useStatePL(in90);
  const [progress, setProgress] = useStatePL("0");
  const [status, setStatus]     = useStatePL("planned");
  const [milestone, setMilestone] = useStatePL(false);
  const [submitting, setSubmitting] = useStatePL(false);

  const submit = async (e) => {
    e.preventDefault();
    setSubmitting(true);
    await onCreated({
      phase_id: phaseId || null,
      wbs: wbs.trim(),
      name_fr: nameFr.trim(),
      name_en: nameEn.trim() || nameFr.trim(),
      start_date: start,
      end_date: end,
      progress: Number(progress),
      status,
      milestone,
      position: existingCount + 1,
    });
    setSubmitting(false);
  };

  return (
    <ModalShell lang={lang} title={lang === "fr" ? "Nouvelle action" : "New action"} onClose={onClose}>
      <form onSubmit={submit} style={{ display: "grid", gap: 10 }}>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Phase" : "Phase"}</span>
            <select required value={phaseId} onChange={(e) => setPhaseId(e.target.value)} style={planInp}>
              <option value="">— {lang === "fr" ? "choisir une phase" : "pick a phase"} —</option>
              {livePhases.map((p) => (
                <option key={p.uuid || p.id} value={p.uuid || ""}>
                  {(p.name && (p.name[lang] || p.name.fr)) || p.code}
                </option>
              ))}
            </select>
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>WBS (ex: 3.5)</span>
            <input value={wbs} onChange={(e) => setWbs(e.target.value)} placeholder="3.5" style={planInp} />
          </label>
        </div>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={planLbl}>{lang === "fr" ? "Libellé de l'action (FR)" : "Action label (FR)"}</span>
          <input required value={nameFr} onChange={(e) => setNameFr(e.target.value)}
            placeholder={lang === "fr" ? "Formation cohorte 3 (200 agents)" : "Training cohort 3 (200 staff)"} style={planInp} />
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={planLbl}>{lang === "fr" ? "Libellé (EN — optionnel)" : "Label (EN — optional)"}</span>
          <input value={nameEn} onChange={(e) => setNameEn(e.target.value)} style={planInp} />
        </label>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Début" : "Start"}</span>
            <input required type="date" value={start} onChange={(e) => setStart(e.target.value)} style={planInp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Fin" : "End"}</span>
            <input required type="date" value={end} onChange={(e) => setEnd(e.target.value)} style={planInp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Avancement %" : "Progress %"}</span>
            <input type="number" min="0" max="100" value={progress} onChange={(e) => setProgress(e.target.value)} style={planInp} />
          </label>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, alignItems: "center" }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Statut" : "Status"}</span>
            <select value={status} onChange={(e) => setStatus(e.target.value)} style={planInp}>
              {ACTION_STATUSES.map((s) => <option key={s.v} value={s.v}>{lang === "fr" ? s.fr : s.en}</option>)}
            </select>
          </label>
          <label style={{ display: "flex", gap: 6, alignItems: "center", paddingTop: 18 }}>
            <input type="checkbox" checked={milestone} onChange={(e) => setMilestone(e.target.checked)} />
            <span>{lang === "fr" ? "Jalon (◆)" : "Milestone (◆)"}</span>
          </label>
        </div>
        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
          <button type="button" onClick={onClose} disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="submit" disabled={submitting || !phaseId}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: "pointer", fontWeight: 600 }}>
            {submitting ? "…" : (lang === "fr" ? "Créer l'action" : "Create action")}
          </button>
        </div>
        {!phaseId && (
          <div className="text-faint" style={{ fontSize: 11 }}>
            {lang === "fr"
              ? "Sélectionnez une phase pour activer le bouton. Si la liste est vide, créez d'abord une phase via le bouton « Phase »."
              : "Select a phase to enable the button. If the list is empty, first create a phase via the 'Phase' button."}
          </div>
        )}
      </form>
    </ModalShell>
  );
}

function EditActionModal({ lang, action, onClose, onSaved }) {
  const [progress, setProgress] = useStatePL(String(action.progress || 0));
  const [status, setStatus]     = useStatePL(action.status || "planned");
  const [milestone, setMilestone] = useStatePL(!!action.milestone);
  const [start, setStart]       = useStatePL(action.start || "");
  const [end, setEnd]           = useStatePL(action.end || "");
  const [submitting, setSubmitting] = useStatePL(false);
  const submit = async (e) => {
    e.preventDefault();
    setSubmitting(true);
    await onSaved({
      progress: Number(progress),
      status,
      milestone,
      start_date: start || null,
      end_date:   end   || null,
    });
    setSubmitting(false);
  };
  return (
    <ModalShell lang={lang} title={lang === "fr" ? "Modifier l'action" : "Edit action"} onClose={onClose}>
      <div className="text-faint" style={{ fontSize: 12, marginBottom: 8 }}>
        <span className="tag-mono">{action.wbs || "—"}</span> · {action.name && (action.name[lang] || action.name.fr)}
      </div>
      <form onSubmit={submit} style={{ display: "grid", gap: 10 }}>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Début" : "Start"}</span>
            <input type="date" value={start} onChange={(e) => setStart(e.target.value)} style={planInp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Fin" : "End"}</span>
            <input type="date" value={end} onChange={(e) => setEnd(e.target.value)} style={planInp} />
          </label>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, alignItems: "center" }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Avancement %" : "Progress %"}</span>
            <input type="number" min="0" max="100" value={progress} onChange={(e) => setProgress(e.target.value)} style={planInp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={planLbl}>{lang === "fr" ? "Statut" : "Status"}</span>
            <select value={status} onChange={(e) => setStatus(e.target.value)} style={planInp}>
              {ACTION_STATUSES.map((s) => <option key={s.v} value={s.v}>{lang === "fr" ? s.fr : s.en}</option>)}
            </select>
          </label>
        </div>
        <label style={{ display: "flex", gap: 6, alignItems: "center" }}>
          <input type="checkbox" checked={milestone} onChange={(e) => setMilestone(e.target.checked)} />
          <span>{lang === "fr" ? "Jalon (◆)" : "Milestone (◆)"}</span>
        </label>
        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 4 }}>
          <button type="button" onClick={onClose} disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="submit" disabled={submitting}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#059669", color: "white", cursor: "pointer", fontWeight: 600 }}>
            {submitting ? "…" : (lang === "fr" ? "Enregistrer" : "Save")}
          </button>
        </div>
      </form>
    </ModalShell>
  );
}

// ==================== TABLE VIEW ====================
function PlanTable({ actions, phases, lang, onEditAction }) {
  return (
    <div className="card">
      <div className="card-body flush">
        <table className="tbl">
          <thead><tr>
            <th style={{ width: 50 }}>WBS</th>
            <th style={{ width: 28 }}></th>
            <th>{lang === "fr" ? "Action" : "Action"}</th>
            <th style={{ width: 130 }}>{lang === "fr" ? "Phase" : "Phase"}</th>
            <th style={{ width: 130 }}>{lang === "fr" ? "Responsable" : "Owner"}</th>
            <th className="num" style={{ width: 100 }}>{lang === "fr" ? "Début" : "Start"}</th>
            <th className="num" style={{ width: 100 }}>{lang === "fr" ? "Fin" : "End"}</th>
            <th className="num" style={{ width: 70 }}>{lang === "fr" ? "Durée" : "Duration"}</th>
            <th style={{ width: 70 }}>{lang === "fr" ? "Dép." : "Dep."}</th>
            <th style={{ width: 130 }}>{lang === "fr" ? "Avancement" : "Progress"}</th>
            <th style={{ width: 100 }}>{lang === "fr" ? "Statut" : "Status"}</th>
            {onEditAction && <th style={{ width: 44 }}></th>}
          </tr></thead>
          <tbody>
            {actions.map((a) => {
              const ph = phases.find((p) => p.id === a.phase);
              return (
                <tr key={a.id}>
                  <td className="mono text-faint">{a.wbs}</td>
                  <td>{a.milestone ? <span className="plan-ms-icon" title="Jalon">◆</span> : ""}</td>
                  <td className="strong">{a.name[lang] || a.name.fr}</td>
                  <td><span className="phase-chip" style={{ background: ph.color, color: "white" }}>{ph.name[lang] || ph.name.fr}</span></td>
                  <td>
                    <span className="row gap-xs">
                      <span className="avatar xxs" style={{ background: avColorPL(a.owner) }}>{initialsPL(a.owner)}</span>
                      <span>{a.owner}</span>
                    </span>
                  </td>
                  <td className="num mono">{fmtDate(a.start, lang)}</td>
                  <td className="num mono">{fmtDate(a.end, lang)}</td>
                  <td className="num mono muted">{durationLabel(a.start, a.end, lang)}</td>
                  <td className="mono text-faint" style={{ fontSize: 10.5 }}>{a.dep && a.dep.length ? a.dep.join(", ") : "—"}</td>
                  <td>
                    <div className="row gap-sm">
                      <div className="bar" style={{ width: 70 }}>
                        <div className="bar-fill" style={{ width: a.progress + "%", background: statusColor(a.status) }}></div>
                      </div>
                      <span className="mono num-sm" style={{ minWidth: 28 }}>{a.progress}%</span>
                    </div>
                  </td>
                  <td>{statusPill(a.status, lang)}</td>
                  {onEditAction && (
                    <td>
                      {a.uuid ? (
                        <button className="btn xs ghost" onClick={() => onEditAction(a)} title={lang === "fr" ? "Modifier" : "Edit"}>
                          <Icon.edit />
                        </button>
                      ) : (
                        <span className="text-faint" style={{ fontSize: 10 }} title={lang === "fr" ? "Action démo (non persistée)" : "Demo action (not persisted)"}>—</span>
                      )}
                    </td>
                  )}
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ==================== GANTT ====================
function PlanGantt({ actions, phases, lang, height }) {
  // Empty state — without at least one action, all date math is NaN and
  // the SVG paths crash the render.
  if (!actions || actions.length === 0) {
    return (
      <div className="card" style={{ padding: 40, textAlign: "center" }}>
        <div className="text-faint" style={{ fontSize: 14, marginBottom: 6 }}>
          {lang === "fr" ? "Aucune action planifiée." : "No planned action yet."}
        </div>
        <div className="text-faint" style={{ fontSize: 12 }}>
          {lang === "fr"
            ? "Ajoutez votre première action avec le bouton « + Action » en haut à droite."
            : "Add your first action using the “+ Action” button at the top right."}
        </div>
        {phases && phases.length > 0 && (
          <div style={{ marginTop: 14, display: "flex", gap: 6, justifyContent: "center", flexWrap: "wrap" }}>
            {phases.map((ph) => (
              <span key={ph.id || ph.uuid} className="phase-chip" style={{ background: ph.color || "#888", color: "white" }}>
                {(ph.name && (ph.name[lang] || ph.name.fr)) || ph.code}
              </span>
            ))}
          </div>
        )}
      </div>
    );
  }
  // Window: spans all actions
  const dates = actions.flatMap((a) => [a.start, a.end]).filter(Boolean).sort();
  if (dates.length === 0) {
    return (
      <div className="card" style={{ padding: 40, textAlign: "center" }}>
        <div className="text-faint">
          {lang === "fr" ? "Les actions n'ont pas encore de dates." : "Actions have no dates yet."}
        </div>
      </div>
    );
  }
  const minDate = new Date(dates[0]);
  const maxDate = new Date(dates[dates.length - 1]);
  // Pad
  const start = new Date(minDate); start.setMonth(start.getMonth() - 1);
  const end = new Date(maxDate); end.setMonth(end.getMonth() + 1);
  const totalDays = durationDays(start.toISOString().slice(0, 10), end.toISOString().slice(0, 10));

  const today = new Date("2026-05-16");

  // Build year/quarter/month ticks
  const monthTicks = [];
  let d = new Date(start.getFullYear(), start.getMonth(), 1);
  while (d <= end) {
    monthTicks.push(new Date(d));
    d.setMonth(d.getMonth() + 1);
  }
  const yearTicks = [...new Set(monthTicks.map((m) => m.getFullYear()))].map((y) => new Date(y, 0, 1)).filter((d) => d >= start && d <= end);

  // Layout
  const LEFT = 280;            // label column width
  const ROW_H = 30;
  const HEADER_H = 56;
  const DAY_PX = Math.max(1.0, Math.min(2.2, 1500 / totalDays));
  const totalW = LEFT + totalDays * DAY_PX + 24;
  const totalH = HEADER_H + actions.length * ROW_H + 14;

  const xFor = (dateStr) => LEFT + durationDays(start.toISOString().slice(0, 10), dateStr) * DAY_PX;

  const monthName = (m) => {
    const fr = ["Jan","Fév","Mar","Avr","Mai","Jun","Jul","Aoû","Sep","Oct","Nov","Déc"];
    const en = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
    return (lang === "fr" ? fr : en)[m.getMonth()];
  };

  const actionById = useMemoPL(() => {
    const m = {}; actions.forEach((a) => m[a.id] = a); return m;
  }, [actions]);

  return (
    <div className="gantt-wrap card">
      <div className="gantt-scroll" style={height ? { maxHeight: height } : null}>
        <svg viewBox={`0 0 ${totalW} ${totalH}`} width={totalW} height={totalH} style={{ display: "block" }}>
          {/* Year header */}
          {yearTicks.map((y, i) => {
            const x = xFor(y.toISOString().slice(0, 10));
            const nextY = yearTicks[i + 1];
            const xNext = nextY ? xFor(nextY.toISOString().slice(0, 10)) : LEFT + totalDays * DAY_PX;
            return (
              <g key={i}>
                <rect x={x} y={0} width={xNext - x} height={22} fill={i % 2 === 0 ? "var(--bg-sunken)" : "var(--bg-elev)"} />
                <text x={x + 6} y={15} fontSize="11" fontWeight="600" fill="var(--text)">{y.getFullYear()}</text>
              </g>
            );
          })}
          {/* Month header */}
          {monthTicks.map((m, i) => {
            const x = xFor(m.toISOString().slice(0, 10));
            const nxt = monthTicks[i + 1];
            const xNext = nxt ? xFor(nxt.toISOString().slice(0, 10)) : LEFT + totalDays * DAY_PX;
            const w = xNext - x;
            return (
              <g key={i}>
                <line x1={x} x2={x} y1={22} y2={totalH} stroke="var(--line-faint)" />
                {w > 28 && <text x={x + 4} y={40} fontSize="9.5" fill="var(--text-faint)">{monthName(m)}</text>}
              </g>
            );
          })}
          <line x1={LEFT} x2={LEFT} y1={0} y2={totalH} stroke="var(--line)" />
          <line x1={0} x2={totalW} y1={HEADER_H} y2={HEADER_H} stroke="var(--line)" />

          {/* Today line */}
          {today >= start && today <= end && (
            <g>
              <line x1={xFor(today.toISOString().slice(0, 10))} x2={xFor(today.toISOString().slice(0, 10))} y1={22} y2={totalH} stroke="var(--accent)" strokeWidth="1.5" strokeDasharray="3 3" />
              <rect x={xFor(today.toISOString().slice(0, 10)) - 22} y={24} width="44" height="14" rx="2" fill="var(--accent)" />
              <text x={xFor(today.toISOString().slice(0, 10))} y={34} fontSize="9.5" fill="white" textAnchor="middle" fontWeight="600">{lang === "fr" ? "AUJ" : "TODAY"}</text>
            </g>
          )}

          {/* Rows */}
          {actions.map((a, i) => {
            const y = HEADER_H + i * ROW_H;
            const ph = phases.find((p) => p.id === a.phase);
            const x = xFor(a.start);
            const w = Math.max(4, xFor(a.end) - x);
            return (
              <g key={a.id}>
                <rect x={0} y={y} width={totalW} height={ROW_H} fill={i % 2 === 0 ? "transparent" : "var(--bg-sunken)"} opacity="0.5" />
                {/* Label */}
                <text x={10} y={y + 13} fontSize="10" fill="var(--text-faint)" fontFamily="var(--font-mono)">{a.wbs}</text>
                <text x={44} y={y + 13} fontSize="11.5" fill="var(--text)" fontWeight={a.milestone ? "600" : "500"}>
                  {a.milestone ? "◆ " : ""}{truncate(a.name[lang] || a.name.fr, 36)}
                </text>
                <text x={44} y={y + 25} fontSize="10" fill="var(--text-faint)">{a.owner} · {durationLabel(a.start, a.end, lang)}</text>

                {/* Bar */}
                {a.milestone ? (
                  <g transform={`translate(${x}, ${y + ROW_H / 2})`}>
                    <polygon points="0,-8 8,0 0,8 -8,0" fill={statusColor(a.status)} stroke="white" strokeWidth="1" />
                  </g>
                ) : (
                  <g>
                    <rect x={x} y={y + 9} width={w} height={ROW_H - 16} rx="3"
                      fill={a.status === "done" ? statusColor("done") : ph.color}
                      opacity={a.status === "done" ? 0.85 : 0.92} />
                    {a.progress > 0 && a.progress < 100 && (
                      <rect x={x} y={y + 9} width={w * a.progress / 100} height={ROW_H - 16} rx="3" fill="white" opacity="0.35" />
                    )}
                    {w > 40 && <text x={x + w - 4} y={y + ROW_H / 2 + 3} fontSize="9.5" fill="white" textAnchor="end" fontWeight="600">{a.progress}%</text>}
                  </g>
                )}
              </g>
            );
          })}

          {/* Dependencies */}
          {actions.map((a) => (a.dep || []).map((depId, k) => {
            const dep = actionById[depId];
            if (!dep) return null;
            const fromY = HEADER_H + actions.indexOf(dep) * ROW_H + ROW_H / 2;
            const toY = HEADER_H + actions.indexOf(a) * ROW_H + ROW_H / 2;
            const fromX = xFor(dep.end);
            const toX = xFor(a.start);
            const midX = Math.max(fromX + 8, toX - 8);
            return (
              <path key={a.id + "-" + depId + "-" + k}
                d={`M ${fromX} ${fromY} L ${midX} ${fromY} L ${midX} ${toY} L ${toX} ${toY}`}
                fill="none" stroke="var(--text-faint)" strokeWidth="1" strokeDasharray="3 2" opacity="0.7" />
            );
          }))}
        </svg>
      </div>

      {/* Legend */}
      <div className="gantt-legend">
        {phases.map((ph) => (
          <span key={ph.id} className="row gap-xs" style={{ fontSize: 11.5 }}>
            <span style={{ width: 10, height: 10, borderRadius: 2, background: ph.color }}></span>
            <span>{ph.name[lang] || ph.name.fr}</span>
          </span>
        ))}
        <span className="row gap-xs" style={{ fontSize: 11.5, marginLeft: "auto" }}>
          <span style={{ display: "inline-block", width: 0, height: 0, borderLeft: "5px solid transparent", borderRight: "5px solid transparent", borderBottom: "9px solid var(--text)" }}></span>
          <span>{lang === "fr" ? "Jalon" : "Milestone"}</span>
        </span>
      </div>
    </div>
  );
}

function truncate(s, n) { return s.length > n ? s.slice(0, n - 1) + "…" : s; }

// ==================== PROGRAMME GANTT (auto from projects) ====================
function ProgrammeGantt({ projects, lang }) {
  // Each project becomes 1 row aggregating its plan: bar from min(action.start) to max(action.end),
  // overall progress = weighted by action durations.
  const rows = projects.map((p) => {
    const plan = getPlanFor(p.id, p);
    if (!plan) return null;
    const acts = plan.actions;
    const start = acts.map((a) => a.start).sort()[0];
    const end = acts.map((a) => a.end).sort().slice(-1)[0];
    const totalDur = acts.reduce((s, a) => s + durationDays(a.start, a.end), 0);
    const weightedProgress = acts.reduce((s, a) => s + durationDays(a.start, a.end) * (a.progress || 0), 0);
    const progress = Math.round(weightedProgress / (totalDur || 1));
    const sector = sectorById(p.sector);
    const milestones = acts.filter((a) => a.milestone);
    return { p, plan, start, end, progress, sector, milestones };
  }).filter(Boolean);

  if (!rows.length) return null;

  const minStart = rows.map((r) => r.start).sort()[0];
  const maxEnd = rows.map((r) => r.end).sort().slice(-1)[0];
  const start = new Date(minStart); start.setMonth(start.getMonth() - 1);
  const end = new Date(maxEnd); end.setMonth(end.getMonth() + 1);
  const totalDays = durationDays(start.toISOString().slice(0, 10), end.toISOString().slice(0, 10));

  const LEFT = 280;
  const ROW_H = 44;
  const HEADER_H = 56;
  const DAY_PX = Math.max(0.8, Math.min(1.8, 1300 / totalDays));
  const totalW = LEFT + totalDays * DAY_PX + 24;
  const totalH = HEADER_H + rows.length * ROW_H + 12;

  const xFor = (dateStr) => LEFT + durationDays(start.toISOString().slice(0, 10), dateStr) * DAY_PX;
  const today = new Date("2026-05-16");
  const monthName = (m) => ["Jan","Fév","Mar","Avr","Mai","Jun","Jul","Aoû","Sep","Oct","Nov","Déc"][m.getMonth()];

  const monthTicks = [];
  let d = new Date(start.getFullYear(), start.getMonth(), 1);
  while (d <= end) { monthTicks.push(new Date(d)); d.setMonth(d.getMonth() + 1); }
  const yearTicks = [...new Set(monthTicks.map((m) => m.getFullYear()))].map((y) => new Date(y, 0, 1)).filter((d) => d >= start && d <= end);

  return (
    <div className="gantt-wrap card">
      <div className="gantt-scroll">
        <svg viewBox={`0 0 ${totalW} ${totalH}`} width={totalW} height={totalH} style={{ display: "block" }}>
          {yearTicks.map((y, i) => {
            const x = xFor(y.toISOString().slice(0, 10));
            const nxt = yearTicks[i + 1];
            const xNext = nxt ? xFor(nxt.toISOString().slice(0, 10)) : LEFT + totalDays * DAY_PX;
            return (
              <g key={i}>
                <rect x={x} y={0} width={xNext - x} height={22} fill={i % 2 === 0 ? "var(--bg-sunken)" : "var(--bg-elev)"} />
                <text x={x + 6} y={15} fontSize="11" fontWeight="600" fill="var(--text)">{y.getFullYear()}</text>
              </g>
            );
          })}
          {monthTicks.map((m, i) => {
            const x = xFor(m.toISOString().slice(0, 10));
            const nxt = monthTicks[i + 1];
            const xNext = nxt ? xFor(nxt.toISOString().slice(0, 10)) : LEFT + totalDays * DAY_PX;
            const w = xNext - x;
            return (
              <g key={i}>
                <line x1={x} x2={x} y1={22} y2={totalH} stroke="var(--line-faint)" />
                {w > 28 && <text x={x + 4} y={40} fontSize="9.5" fill="var(--text-faint)">{monthName(m)}</text>}
              </g>
            );
          })}
          <line x1={LEFT} x2={LEFT} y1={0} y2={totalH} stroke="var(--line)" />
          <line x1={0} x2={totalW} y1={HEADER_H} y2={HEADER_H} stroke="var(--line)" />
          {today >= start && today <= end && (
            <g>
              <line x1={xFor(today.toISOString().slice(0, 10))} x2={xFor(today.toISOString().slice(0, 10))} y1={22} y2={totalH} stroke="var(--accent)" strokeWidth="1.5" strokeDasharray="3 3" />
              <rect x={xFor(today.toISOString().slice(0, 10)) - 22} y={24} width="44" height="14" rx="2" fill="var(--accent)" />
              <text x={xFor(today.toISOString().slice(0, 10))} y={34} fontSize="9.5" fill="white" textAnchor="middle" fontWeight="600">{lang === "fr" ? "AUJ" : "TODAY"}</text>
            </g>
          )}

          {rows.map((r, i) => {
            const y = HEADER_H + i * ROW_H;
            const x = xFor(r.start);
            const w = Math.max(8, xFor(r.end) - x);
            return (
              <g key={r.p.id}>
                <rect x={0} y={y} width={totalW} height={ROW_H} fill={i % 2 === 0 ? "transparent" : "var(--bg-sunken)"} opacity="0.5" />
                <text x={10} y={y + 16} fontSize="10.5" fill="var(--text-faint)" fontFamily="var(--font-mono)">{r.p.id}</text>
                <text x={48} y={y + 16} fontSize="12" fill="var(--text)" fontWeight="600">{truncate(projectName(r.p, lang), 32)}</text>
                <text x={48} y={y + 30} fontSize="10.5" fill="var(--text-faint)">{sectorLabel(r.sector, lang)} · {r.p.lead}</text>

                {/* Bar */}
                <rect x={x} y={y + 14} width={w} height={ROW_H - 22} rx="4" fill={r.sector.color} opacity="0.18" />
                <rect x={x} y={y + 14} width={w * r.progress / 100} height={ROW_H - 22} rx="4" fill={r.sector.color} />
                <text x={x + w - 6} y={y + ROW_H / 2 + 3} fontSize="10" fill="white" textAnchor="end" fontWeight="600" style={{ mixBlendMode: "difference" }}>{r.progress}%</text>

                {/* Milestone diamonds */}
                {r.milestones.map((m, k) => (
                  <g key={k} transform={`translate(${xFor(m.start)}, ${y + ROW_H / 2})`}>
                    <polygon points="0,-6 6,0 0,6 -6,0" fill={statusColor(m.status)} stroke="white" strokeWidth="1" />
                  </g>
                ))}
              </g>
            );
          })}
        </svg>
      </div>
    </div>
  );
}

window.ProgrammeGantt = ProgrammeGantt;
window.PlanGantt      = PlanGantt;

// ==================== PROGRAMME PAGE ====================
function ProgrammesPage({ t, lang, onOpen, isSuperAdmin, actingOrgId, myOrgId, isAdmin, hasPerm }) {
  // "Nouveau programme" is an admin action — gate it on users.manage
  // (= is_admin_user RLS helper) or super-admin. Reviewer / data-entry
  // users shouldn't see the button.
  const canCreateProgramme = !!isSuperAdmin || !!isAdmin
    || (hasPerm && hasPerm("users.manage"));
  // All hooks must be called unconditionally — see Projects/ProjectDetail
  // for the same rules-of-hooks fix.
  // Super-admin acting-as-org: pass the target org down to usePrograms so
  // the right set comes back via the super-admin SELECT policy.
  const acting = !!(isSuperAdmin && actingOrgId && actingOrgId !== myOrgId);
  const { projects: livePROJECTS, loading: lpLoading, refresh: refreshProjects } = window.melr.useProjects();
  const { programmes: livePrograms, loading: lprogLoading, realtime: programmesRealtime, refresh: refreshProgrammes } = window.melr.usePrograms(acting ? actingOrgId : undefined);
  const { data: allOrgs } = window.melr.useAllOrganizations();
  const { currency } = window.melr.useCurrency();
  const LiveBadge = window.melr.LiveBadge;
  const fmtM = (v) => window.melr.formatMoney(v, currency, lang);
  // Per-source currency formatting (programmes can be in native ccy)
  const fmtP = (v, src) => window.melr.formatAmount(v, src || "EUR", currency, lang);
  const [createOpen, setCreateOpen] = useStatePL(false);
  const [createMsg,  setCreateMsg]  = useStatePL(null);
  const [bulkAttachOpen, setBulkAttachOpen] = useStatePL(false);
  // Super-admin: programme being transferred to another org (modal)
  const [transferProg, setTransferProg] = useStatePL(null);
  // PROJECTS_SRC narrows to the acting org's projects so the count
  // displayed per programme matches the moved subset.
  const PROJECTS_RAW = (livePROJECTS && livePROJECTS.length > 0) ? livePROJECTS : (typeof PROJECTS !== "undefined" ? PROJECTS : []);
  const PROJECTS_SRC = acting
    ? PROJECTS_RAW.filter((p) => p.organizationId === actingOrgId)
    : PROJECTS_RAW;

  // Build programme → projects hierarchy. A programme can have 0 or more
  // projects; orphan projects (no programme_id) are grouped under a
  // "Sans programme" pseudo-bucket so they remain visible.
  const programmeRows = useMemoPL(() => {
    const byProg = {};
    (livePrograms || []).forEach((pg) => {
      byProg[pg.id] = { programme: pg, projects: [] };
    });
    PROJECTS_SRC.forEach((p) => {
      if (p.programmeId && byProg[p.programmeId]) {
        byProg[p.programmeId].projects.push(p);
      }
    });
    return Object.values(byProg).sort((a, b) => b.projects.length - a.projects.length);
  }, [livePrograms, PROJECTS_SRC]);

  const orphanProjects = useMemoPL(
    () => PROJECTS_SRC.filter((p) => !p.programmeId),
    [PROJECTS_SRC]
  );

  // Group projects by sector (legacy view, conserved below as alt grouping)
  const groups = useMemoPL(() => {
    const map = {};
    PROJECTS_SRC.forEach((p) => { (map[p.sector] = map[p.sector] || []).push(p); });
    return Object.entries(map).map(([sid, projs]) => ({
      sector: sectorById(sid),
      projects: projs,
      budget: projs.reduce((s, p) => s + p.budget, 0),
      sites: projs.reduce((s, p) => s + (p.sites || 0), 0),
    })).filter((g) => g.projects.length >= 1).sort((a, b) => b.projects.length - a.projects.length);
  }, [PROJECTS_SRC]);

  const [selected, setSelected] = useStatePL(null);
  if (lpLoading) {
    return <div className="page"><div className="page-body" style={{ padding: 40 }}>{lang === "fr" ? "Chargement…" : "Loading…"}</div></div>;
  }
  const current = groups.find((g) => g.sector.id === selected) || groups[0];

  return (
    <div className="page">
      <div className="page-header">
        <div className="page-eyebrow">{lang === "fr" ? "PROGRAMMES / GANTT CONSOLIDÉ" : "PROGRAMMES / CONSOLIDATED GANTT"}</div>
        <div className="page-header-row">
          <div>
            <h1 className="page-title">
              {lang === "fr" ? "Programmes & planification consolidée" : "Programmes & consolidated planning"}
              <LiveBadge on={programmesRealtime} lang={lang} />
            </h1>
            <div className="page-sub">
              {lang === "fr"
                ? "Hiérarchie « programme → projets » : un programme regroupe un ou plusieurs projets. Vue consolidée des plans d'action · Gantt programme auto-généré · jalons et avancement global."
                : "Hierarchy “programme → projects”: one programme contains one or more projects. Consolidated view of action plans · auto-generated programme Gantt · milestones and overall progress."}
            </div>
          </div>
          <div className="page-header-actions">
            <button className="btn sm" onClick={() => {
              const date = new Date().toISOString().slice(0, 10);
              const rows = (livePrograms || []).map((pg) => {
                const matches = PROJECTS_SRC.filter((p) => p.programmeId === pg.id);
                const toEur = (v, ccy) => window.melr.convertToEur(v, ccy || "EUR");
                const budgetEur = matches.reduce((s, p) => s + toEur(p.budget || 0, p.nativeCurrency), 0);
                const disbEur   = matches.reduce((s, p) => s + toEur(p.disbursed || 0, p.nativeCurrency), 0);
                const sites     = matches.reduce((s, p) => s + (p.sites || 0), 0);
                const avgProgress = matches.length
                  ? Math.round(matches.reduce((s, p) => s + (p.progress || 0), 0) / matches.length)
                  : 0;
                const sect = sectorById(pg.sector_id);
                return {
                  code: pg.code,
                  name_fr: pg.name_fr,
                  name_en: pg.name_en || "",
                  sector: sect ? (lang === "en" ? sect.en : sect.fr) : "",
                  description: pg.description || "",
                  start_date: pg.start_date || "",
                  end_date:   pg.end_date   || "",
                  budget_native: pg.budget != null ? (pg.budget / 1_000_000).toFixed(2) : "",
                  currency: pg.currency || "EUR",
                  project_count: matches.length,
                  project_codes: matches.map((p) => p.id).join(" / "),
                  budget_cumul_eur: budgetEur.toFixed(2),
                  disbursed_cumul_eur: disbEur.toFixed(2),
                  sites_cumul: sites,
                  avg_progress_pct: avgProgress,
                };
              });
              window.melr.exportCSV(`programmes-${date}.csv`, rows, [
                { key: "code",                 label: "Code" },
                { key: "name_fr",              label: lang === "fr" ? "Nom (FR)" : "Name (FR)" },
                { key: "name_en",              label: lang === "fr" ? "Nom (EN)" : "Name (EN)" },
                { key: "sector",               label: lang === "fr" ? "Secteur" : "Sector" },
                { key: "description",          label: "Description" },
                { key: "start_date",           label: lang === "fr" ? "Date début" : "Start date" },
                { key: "end_date",             label: lang === "fr" ? "Date fin" : "End date" },
                { key: "budget_native",        label: lang === "fr" ? "Budget natif (M)" : "Native budget (M)" },
                { key: "currency",             label: lang === "fr" ? "Devise" : "Currency" },
                { key: "project_count",        label: lang === "fr" ? "Nombre projets" : "Project count" },
                { key: "project_codes",        label: lang === "fr" ? "Codes projets" : "Project codes" },
                { key: "budget_cumul_eur",     label: lang === "fr" ? "Budget cumulé (M€)" : "Cumulative budget (M€)" },
                { key: "disbursed_cumul_eur", label: lang === "fr" ? "Décaissé cumulé (M€)" : "Cumulative disbursed (M€)" },
                { key: "sites_cumul",          label: lang === "fr" ? "Sites cumulés" : "Cumulative sites" },
                { key: "avg_progress_pct",     label: lang === "fr" ? "Avancement moyen %" : "Avg progress %" },
              ]);
            }}>
              <Icon.download /> {lang === "fr" ? "Exporter CSV" : "Export CSV"}
            </button>
            {canCreateProgramme && (
              <button className="btn sm primary" onClick={() => setCreateOpen(true)}><Icon.plus /> {lang === "fr" ? "Nouveau programme" : "New programme"}</button>
            )}
          </div>
        </div>
      </div>

      {/* ===== HIERARCHY: programmes → projects ===== */}
      <div className="card" style={{ marginBottom: 18 }}>
        <div className="card-head">
          <div className="card-title">
            {lang === "fr" ? "Programmes créés" : "Created programmes"}
          </div>
          <span className="pill" style={{ marginLeft: 8 }}>{(livePrograms || []).length}</span>
          <span className="text-faint" style={{ fontSize: 11.5, marginLeft: 8 }}>
            {lang === "fr"
              ? "Un programme regroupe un ou plusieurs projets (champ projects.programme_id)."
              : "A programme groups one or more projects (projects.programme_id)."}
          </span>
        </div>
        <div className="card-body">
          {lprogLoading && (
            <div className="text-faint" style={{ fontSize: 12, padding: 8 }}>
              {lang === "fr" ? "Chargement des programmes…" : "Loading programmes…"}
            </div>
          )}
          {!lprogLoading && (livePrograms || []).length === 0 && (
            <div style={{ padding: 20, textAlign: "center" }}>
              <div className="text-faint" style={{ fontSize: 13, marginBottom: 6 }}>
                {lang === "fr" ? "Aucun programme créé pour le moment." : "No programme created yet."}
              </div>
              <div className="text-faint" style={{ fontSize: 11.5 }}>
                {lang === "fr"
                  ? "Utilisez le bouton « Nouveau programme » en haut à droite, puis associez vos projets via le champ « Programme parent » dans le formulaire de création de projet."
                  : "Use the “New programme” button at the top right, then attach projects via the “Parent programme” field in the create-project form."}
              </div>
            </div>
          )}
          {!lprogLoading && (livePrograms || []).length > 0 && (
            <div style={{ display: "grid", gap: 10 }}>
              {programmeRows.map((row) => {
                const pg = row.programme;
                const projs = row.projects;
                const sector = sectorById(pg.sector_id);
                const totalBudgetEur = projs.reduce(
                  (s, p) => s + window.melr.convertToEur(p.budget || 0, p.nativeCurrency || "EUR"),
                  0
                );
                const totalDisbEur = projs.reduce(
                  (s, p) => s + window.melr.convertToEur(p.disbursed || 0, p.nativeCurrency || "EUR"),
                  0
                );
                const avgProgress = projs.length
                  ? Math.round(projs.reduce((s, p) => s + (p.progress || 0), 0) / projs.length)
                  : 0;
                return (
                  <div key={pg.id}
                    style={{
                      border: "1px solid var(--line-faint)", borderRadius: 8,
                      padding: 12, background: "var(--bg-elev, transparent)",
                    }}>
                    <div className="row" style={{ alignItems: "center", gap: 10, flexWrap: "wrap" }}>
                      <span className="sector-dot" style={{ background: sector.color, width: 10, height: 10, borderRadius: 2 }}></span>
                      <span className="tag-mono" style={{ fontSize: 11, fontWeight: 600 }}>{pg.code}</span>
                      <span className="row-link"
                        onClick={() => onOpen && onOpen("prog:" + pg.id)}
                        style={{ fontWeight: 600, fontSize: 14, cursor: "pointer" }}
                        title={lang === "fr" ? "Ouvrir le détail du programme" : "Open programme detail"}>
                        {lang === "en" ? (pg.name_en || pg.name_fr) : pg.name_fr}
                      </span>
                      <span className="sector-chip"
                        style={{ background: sector.bg, color: sector.color, borderColor: sector.color }}>
                        {sectorLabel(sector, lang)}
                      </span>
                      <span className="pill" title={lang === "fr" ? "Projets rattachés" : "Attached projects"}>
                        {projs.length} {lang === "fr" ? (projs.length > 1 ? "projets" : "projet") : (projs.length > 1 ? "projects" : "project")}
                      </span>
                      <div style={{ flex: 1 }} />
                      <span className="text-faint mono" style={{ fontSize: 11 }}>
                        {lang === "fr" ? "Budget cumulé : " : "Cumulative budget: "}
                        <b style={{ color: "var(--text)" }}>{fmtM(totalBudgetEur)}</b>
                        {totalBudgetEur > 0 && (
                          <> · {Math.round(totalDisbEur / totalBudgetEur * 100)}% {lang === "fr" ? "décaissé" : "disbursed"}</>
                        )}
                      </span>
                      <button className="btn xs ghost" onClick={() => onOpen && onOpen("prog:" + pg.id)}
                        title={lang === "fr" ? "Ouvrir le programme" : "Open programme"}>
                        {lang === "fr" ? "Ouvrir →" : "Open →"}
                      </button>
                      {isSuperAdmin && (
                        <button className="btn xs ghost"
                          onClick={() => setTransferProg({
                            id: pg.id,
                            code: pg.code,
                            name: lang === "en" ? (pg.name_en || pg.name_fr) : pg.name_fr,
                            organization_id: pg.organization_id,
                            projectCount: projs.length,
                          })}
                          title={lang === "fr"
                            ? "Transférer le programme et tous ses projets vers une autre organisation (super-admin)"
                            : "Transfer this programme and all its projects to another organization (super-admin)"}
                          style={{ color: "#d97706" }}>
                          🔁 {lang === "fr" ? "Transférer" : "Transfer"}
                        </button>
                      )}
                    </div>

                    {pg.description && (
                      <div className="text-faint" style={{ fontSize: 11.5, marginTop: 6, marginLeft: 20 }}>
                        {pg.description}
                      </div>
                    )}

                    {projs.length === 0 ? (
                      <div className="text-faint" style={{
                        fontSize: 11.5, marginTop: 10, marginLeft: 20, fontStyle: "italic",
                      }}>
                        {lang === "fr"
                          ? "Aucun projet rattaché. Lors de la création d'un projet, sélectionnez ce programme dans le champ « Programme parent »."
                          : "No project attached yet. When creating a project, pick this programme in the “Parent programme” field."}
                      </div>
                    ) : (
                      <div style={{ marginTop: 10, marginLeft: 20, display: "grid", gap: 4 }}>
                        {projs.map((p) => {
                          const ps = sectorById(p.sector);
                          return (
                            <div key={p.id}
                              className="row-link"
                              onClick={() => onOpen && onOpen(p.id)}
                              style={{
                                display: "grid",
                                gridTemplateColumns: "70px 1fr auto auto auto",
                                alignItems: "center", gap: 10,
                                padding: "6px 8px", borderRadius: 4,
                                fontSize: 12, cursor: "pointer",
                              }}>
                              <span className="tag-mono text-faint">{p.id}</span>
                              <span className="strong" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
                                {projectName(p, lang)}
                              </span>
                              <span className="sector-chip"
                                style={{
                                  background: ps.bg, color: ps.color, borderColor: ps.color,
                                  fontSize: 10, padding: "1px 6px",
                                }}>
                                {sectorLabel(ps, lang)}
                              </span>
                              <span className="mono text-faint" style={{ fontSize: 11 }}>
                                {fmtP(p.budget, p.nativeCurrency)}
                              </span>
                              <span className="mono num-sm" style={{ minWidth: 36, textAlign: "right" }}>
                                {p.progress}%
                              </span>
                            </div>
                          );
                        })}
                        <div className="text-faint mono" style={{
                          fontSize: 10.5, marginTop: 4, paddingLeft: 8,
                        }}>
                          {lang === "fr" ? "Avancement moyen : " : "Average progress: "}
                          <b style={{ color: "var(--text)" }}>{avgProgress}%</b>
                        </div>
                      </div>
                    )}
                  </div>
                );
              })}

              {orphanProjects.length > 0 && (
                <div style={{
                  border: "1px dashed var(--line)", borderRadius: 8,
                  padding: 12, background: "transparent",
                }}>
                  <div className="row" style={{ alignItems: "center", gap: 10 }}>
                    <span style={{ fontWeight: 600, fontSize: 13 }}>
                      ◇ {lang === "fr" ? "Projets sans programme" : "Projects without programme"}
                    </span>
                    <span className="pill">{orphanProjects.length}</span>
                    <div style={{ flex: 1 }} />
                    {livePrograms && livePrograms.length > 0 && (
                      <button className="btn xs primary" onClick={() => setBulkAttachOpen(true)}>
                        <Icon.layers /> {lang === "fr" ? "Rattacher en masse →" : "Bulk attach →"}
                      </button>
                    )}
                  </div>
                  <div style={{ marginTop: 8, marginLeft: 20, display: "flex", gap: 6, flexWrap: "wrap" }}>
                    {orphanProjects.map((p) => (
                      <span key={p.id}
                        className="row-link tag-mono"
                        onClick={() => onOpen && onOpen(p.id)}
                        style={{ fontSize: 11, padding: "2px 6px", borderRadius: 4, background: "var(--bg-sunken)", cursor: "pointer" }}
                        title={projectName(p, lang)}>
                        {p.id} · {projectName(p, lang)}
                      </span>
                    ))}
                  </div>
                </div>
              )}
            </div>
          )}
        </div>
      </div>

      {/* ===== LEGACY VIEW: by sector (alternative grouping) ===== */}
      <div className="row" style={{ marginBottom: 8, alignItems: "baseline" }}>
        <div style={{ fontSize: 13, fontWeight: 600 }}>
          {lang === "fr" ? "Vue alternative — par secteur" : "Alternate view — by sector"}
        </div>
        <span className="text-faint" style={{ fontSize: 11.5, marginLeft: 8 }}>
          {lang === "fr"
            ? "Gantt consolidé et KPIs agrégés par secteur (utile pour les comités multi-programmes)."
            : "Consolidated Gantt and KPIs aggregated by sector (useful for multi-programme committees)."}
        </span>
      </div>

      <div className="prog-tabs">
        {groups.map((g) => (
          <button key={g.sector.id} className={"prog-tab" + (selected === g.sector.id ? " active" : "")} onClick={() => setSelected(g.sector.id)}
            style={selected === g.sector.id ? { borderColor: g.sector.color, color: g.sector.color, background: g.sector.bg } : null}>
            <span className="prog-tab-dot" style={{ background: g.sector.color }}></span>
            <span>{sectorLabel(g.sector, lang)}</span>
            <span className="prog-tab-c">{g.projects.length}</span>
          </button>
        ))}
      </div>

      {current && (
        <>
          <div className="grid cols-4" style={{ marginBottom: 16 }}>
            <div className="kpi">
              <div className="kpi-label">{lang === "fr" ? "Projets dans le programme" : "Projects in programme"}</div>
              <div className="kpi-value">{current.projects.length}</div>
              <div className="kpi-sub">{sectorLabel(current.sector, lang)}</div>
            </div>
            <div className="kpi">
              <div className="kpi-label">{lang === "fr" ? "Budget consolidé" : "Consolidated budget"}</div>
              <div className="kpi-value">{fmtM(current.budget)}</div>
              <div className="kpi-sub">{fmtM(current.projects.reduce((s, p) => s + (p.disbursed || 0), 0))} {lang === "fr" ? "décaissés" : "disbursed"}</div>
            </div>
            <div className="kpi">
              <div className="kpi-label">{lang === "fr" ? "Avancement moyen" : "Average progress"}</div>
              <div className="kpi-value">{Math.round(current.projects.reduce((s, p) => s + p.progress, 0) / current.projects.length)}%</div>
              <div className="kpi-sub">{lang === "fr" ? "pondéré projets" : "project-weighted"}</div>
            </div>
            <div className="kpi">
              <div className="kpi-label">{lang === "fr" ? "Sites couverts" : "Covered sites"}</div>
              <div className="kpi-value">{current.sites}</div>
              <div className="kpi-sub">{lang === "fr" ? "tous projets" : "all projects"}</div>
            </div>
          </div>

          <div className="card" style={{ marginBottom: 16 }}>
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Gantt consolidé du programme" : "Consolidated programme Gantt"}</div>
              <span className="text-faint" style={{ fontSize: 11.5, marginLeft: 8 }}>
                {lang === "fr" ? "Auto-généré à partir des Gantt projets · cliquer une ligne pour ouvrir le projet" : "Auto-generated from project Gantts · click a row to open project"}
              </span>
            </div>
            <ProgrammeGantt projects={current.projects} lang={lang} />
          </div>

          <div className="card">
            <div className="card-head">
              <div className="card-title">{lang === "fr" ? "Projets du programme" : "Programme projects"}</div>
            </div>
            <div className="card-body flush">
              <table className="tbl">
                <thead><tr>
                  <th style={{ width: 70 }}>ID</th>
                  <th>{lang === "fr" ? "Projet" : "Project"}</th>
                  <th>{lang === "fr" ? "Pays" : "Countries"}</th>
                  <th>{lang === "fr" ? "Responsable" : "Owner"}</th>
                  <th className="num">{lang === "fr" ? "Budget" : "Budget"}</th>
                  <th>{lang === "fr" ? "Avancement" : "Progress"}</th>
                  <th>{lang === "fr" ? "Statut" : "Status"}</th>
                </tr></thead>
                <tbody>
                  {current.projects.map((p) => (
                    <tr key={p.id} className="row-link" onClick={() => onOpen && onOpen(p.id)}>
                      <td className="mono text-faint">{p.id}</td>
                      <td className="strong">{projectName(p, lang)}</td>
                      <td className="muted">{p.countriesFr}</td>
                      <td>
                        <span className="row gap-xs">
                          <span className="avatar xxs" style={{ background: avColorPL(p.lead) }}>{initialsPL(p.lead)}</span>
                          {p.lead}
                        </span>
                      </td>
                      <td className="num mono">{fmtM(p.budget)}</td>
                      <td><div className="row gap-sm"><div className="bar" style={{ width: 80 }}><div className="bar-fill" style={{ width: p.progress + "%" }}></div></div><span className="mono num-sm">{p.progress}%</span></div></td>
                      <td>{p.risk === "ok" ? <span className="pill green dot">OK</span> : p.risk === "warn" ? <span className="pill amber dot">{lang === "fr" ? "Vigilance" : "Watch"}</span> : <span className="pill red dot">{lang === "fr" ? "Élevé" : "High"}</span>}</td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </>
      )}
      {createMsg && (
        <div style={{
          position: "fixed", bottom: 16, right: 16, zIndex: 9998,
          padding: "10px 16px", borderRadius: 8,
          background: createMsg.tone === "success" ? "#dcfce7" : "#fee2e2",
          color:      createMsg.tone === "success" ? "#166534" : "#991b1b",
          boxShadow: "0 4px 12px rgba(0,0,0,.15)", fontSize: 13, maxWidth: 360,
        }}>{createMsg.text}</div>
      )}
      {createOpen && (
        <NewProgrammeModal
          lang={lang}
          onClose={() => setCreateOpen(false)}
          onCreated={(prog) => {
            setCreateOpen(false);
            setCreateMsg({ tone: "success", text: (lang === "fr" ? "Programme " : "Programme ") + prog.code + (lang === "fr" ? " créé." : " created.") });
            setTimeout(() => setCreateMsg(null), 4000);
          }}
        />
      )}
      {bulkAttachOpen && (
        <BulkAttachOrphansModal
          lang={lang}
          orphans={orphanProjects}
          programmes={livePrograms || []}
          onClose={() => setBulkAttachOpen(false)}
          onApplied={async (n, prog) => {
            await refreshProjects();
            setBulkAttachOpen(false);
            setCreateMsg({
              tone: "success",
              text: lang === "fr"
                ? `${n} projet${n > 1 ? "s" : ""} rattaché${n > 1 ? "s" : ""} au programme ${prog.code}.`
                : `${n} project${n > 1 ? "s" : ""} attached to programme ${prog.code}.`,
            });
            setTimeout(() => setCreateMsg(null), 4000);
          }}
        />
      )}
      {transferProg && (
        <TransferProgrammeModal
          lang={lang}
          programme={transferProg}
          allOrgs={allOrgs}
          onClose={() => setTransferProg(null)}
          onTransferred={async (result) => {
            await Promise.all([refreshProjects(), refreshProgrammes && refreshProgrammes()]);
            setCreateMsg({
              tone: "success",
              text: lang === "fr"
                ? `Programme ${transferProg.code} transféré (${result.transferredProjects} projet${result.transferredProjects > 1 ? "s" : ""}).`
                : `Programme ${transferProg.code} transferred (${result.transferredProjects} project${result.transferredProjects > 1 ? "s" : ""}).`,
            });
            setTimeout(() => setCreateMsg(null), 5000);
            setTransferProg(null);
          }}
        />
      )}
    </div>
  );
}

window.ProgrammesPage = ProgrammesPage;

// ==================== TRANSFER PROGRAMME MODAL ====================
// Super-admin only: move a whole programme + all its projects to another
// organization. The form mirrors TransferProjectModal but spells out the
// extra consequence: every project in the programme follows along, and
// their per-agent assignments + leads get reset.
function TransferProgrammeModal({ lang, programme, allOrgs, onClose, onTransferred }) {
  const [targetOrgId, setTargetOrgId] = useStatePL("");
  const [busy, setBusy] = useStatePL(false);
  const [err, setErr]   = useStatePL(null);

  const candidates = (allOrgs || [])
    .filter((o) => !o.archived_at && o.id !== programme.organization_id);

  const onConfirm = async () => {
    if (!targetOrgId) {
      setErr(lang === "fr" ? "Choisir une organisation cible." : "Pick a target organization.");
      return;
    }
    setBusy(true); setErr(null);
    try {
      const result = await window.melr.transferProgrammeToOrg(programme.id, targetOrgId);
      if (onTransferred) await onTransferred(result);
    } catch (e) { setErr(e.message); }
    finally { setBusy(false); }
  };

  const target = candidates.find((o) => o.id === targetOrgId);

  return (
    <Modal
      title={<>🔁 {lang === "fr" ? "Transférer le programme entier" : "Transfer whole programme"}</>}
      onClose={onClose}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={busy}>
          {lang === "fr" ? "Annuler" : "Cancel"}
        </button>
        <button type="button" onClick={onConfirm} disabled={busy || !targetOrgId}
          style={{
            padding: "8px 14px", borderRadius: 6, border: 0,
            background: targetOrgId ? "#d97706" : "#9ca3af",
            color: "white", cursor: targetOrgId ? "pointer" : "not-allowed", fontWeight: 600,
          }}>
          {busy ? "…" : (lang === "fr"
            ? "Transférer vers " + (target ? target.name : "…")
            : "Transfer to " + (target ? target.name : "…"))}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 12 }}>
        <div>
          <div className="text-faint" style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: "0.04em" }}>
            {lang === "fr" ? "Programme" : "Programme"}
          </div>
          <div className="strong" style={{ fontSize: 14 }}>
            <span className="mono">{programme.code}</span> · {programme.name}
          </div>
          <div className="text-faint" style={{ fontSize: 12, marginTop: 2 }}>
            {programme.projectCount} {lang === "fr"
              ? (programme.projectCount > 1 ? "projets rattachés" : "projet rattaché")
              : (programme.projectCount > 1 ? "attached projects" : "attached project")}
          </div>
        </div>
        <div className="text-faint" style={{ fontSize: 12.5, lineHeight: 1.55 }}>
          {lang === "fr"
            ? "Le programme et TOUS ses projets vont changer d'organisation. Le lien programme ↔ projets est conservé (ils déménagent ensemble). Conséquences : (1) le responsable de chaque projet est remis à NULL — l'admin de la nouvelle org en désignera un par projet ; (2) les affectations agents → projet existantes sont effacées (les agents de l'ancienne org perdraient l'accès via RLS de toute façon) ; (3) les indicateurs, sites, saisies et audits suivent leurs projets respectifs. Les définitions d'indicateurs (catalogue) ne sont pas affectées — elles restent dans l'org d'origine."
            : "The programme AND all its projects will change organization. The programme ↔ projects link is preserved (they move together). Consequences: (1) each project's lead is reset to NULL — the new org's admin will pick one per project; (2) existing agent → project assignments are wiped (agents of the old org would lose access via RLS anyway); (3) indicators, sites, submissions and audits follow their respective projects. Indicator definitions (catalogue) are NOT affected — they stay in the source org."}
        </div>
        <div>
          <label style={{ display: "block", fontSize: 11, color: "var(--text-faint)", marginBottom: 4, textTransform: "uppercase", letterSpacing: "0.04em" }}>
            {lang === "fr" ? "Organisation cible *" : "Target organization *"}
          </label>
          <select required value={targetOrgId} onChange={(e) => setTargetOrgId(e.target.value)}
            style={{ width: "100%", padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 }}>
            <option value="">{lang === "fr" ? "— Choisir —" : "— Pick —"}</option>
            {candidates.map((o) => (
              <option key={o.id} value={o.id}>{o.name}{o.slug ? " (" + o.slug + ")" : ""}</option>
            ))}
          </select>
        </div>
        {err && (
          <div style={{ padding: "8px 10px", background: "#fee2e2", color: "#991b1b", borderRadius: 6, fontSize: 12.5 }}>{err}</div>
        )}
      </div>
    </Modal>
  );
}

// ==================== NEW PROGRAMME MODAL ====================
function NewProgrammeModal({ lang, onClose, onCreated }) {
  const [code, setCode]         = useStatePL("");
  const [nameFr, setNameFr]     = useStatePL("");
  const [sectorId, setSectorId] = useStatePL("sante");
  const [description, setDescription] = useStatePL("");
  const [startDate, setStartDate] = useStatePL("");
  const [endDate,   setEndDate]   = useStatePL("");
  const [budget, setBudget]     = useStatePL("");
  const [progCcy, setProgCcy]   = useStatePL("EUR");
  const [submitting, setSubmitting] = useStatePL(false);
  const [err, setErr]           = useStatePL(null);
  const rates = window.melr.getMergedRates();
  // Live sectors from the DB (DB-backed catalogue, see Supabase/sectors.sql).
  const { data: liveSectors } = window.melr.useSectors();
  const [newSectorOpen, setNewSectorOpen] = useStatePL(false);

  const SECTOR_CHOICES = (liveSectors && liveSectors.length > 0)
    ? liveSectors
    : ((typeof SECTORS !== "undefined" && SECTORS) || []);

  // Sentinel "+ Nouveau secteur…" opens the inline modal; the new sector's
  // slug is then auto-selected via onCreated.
  const onSectorChange = (val) => {
    if (val === "__NEW__") setNewSectorOpen(true);
    else                   setSectorId(val);
  };

  const submit = async (e) => {
    e.preventDefault();
    setErr(null); setSubmitting(true);
    try {
      const prog = await window.melr.createProgramme({
        code: code.trim(),
        name_fr: nameFr.trim(),
        sector_id: sectorId,
        description: description.trim() || null,
        start_date: startDate || null,
        end_date:   endDate   || null,
        budget_native: budget || null,
        currency: progCcy,
      });
      if (onCreated) onCreated(prog);
    } catch (e2) {
      setErr(e2.message);
    } finally {
      setSubmitting(false);
    }
  };

  const inp = { padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13 };
  const lbl = { fontSize: 11, opacity: 0.75 };

  return (
    <Modal
      title={lang === "fr" ? "Nouveau programme" : "New programme"}
      onClose={onClose}
      onSubmit={submit}
      footer={<>
        <button type="button" className="btn sm ghost" onClick={onClose} disabled={submitting}>
          {lang === "fr" ? "Annuler" : "Cancel"}
        </button>
        <button type="submit" className="btn sm primary" disabled={submitting}>
          {submitting ? "…" : (lang === "fr" ? "Créer le programme" : "Create programme")}
        </button>
      </>}>
      <div style={{ display: "grid", gap: 10 }}>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 2fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Code (ex: PROG-2026)" : "Code (e.g. PROG-2026)"}</span>
            <input required value={code} onChange={(e) => setCode(e.target.value)} placeholder="PROG-2026" style={inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Nom du programme" : "Programme name"}</span>
            <input required value={nameFr} onChange={(e) => setNameFr(e.target.value)} placeholder={lang === "fr" ? "Santé maternelle Sahel" : "Sahel Maternal Health"} style={inp} />
          </label>
        </div>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Secteur" : "Sector"}</span>
          <select value={sectorId} onChange={(e) => onSectorChange(e.target.value)} style={inp}>
            {SECTOR_CHOICES.map((s) => (
              <option key={s.id} value={s.id}>{lang === "en" ? s.en : s.fr}</option>
            ))}
            <option value="__NEW__">{lang === "fr" ? "+ Nouveau secteur…" : "+ New sector…"}</option>
          </select>
        </label>
        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Description (optionnel)" : "Description (optional)"}</span>
          <textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={2}
            placeholder={lang === "fr" ? "Objectif global, géographie, partenaires…" : "Global objective, geography, partners…"}
            style={{ ...inp, resize: "vertical" }} />
        </label>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Date début" : "Start date"}</span>
            <input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} style={inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Date fin" : "End date"}</span>
            <input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} style={inp} />
          </label>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "2fr 1fr", gap: 10 }}>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Budget (en millions)" : "Budget (in millions)"}</span>
            <input type="number" step="0.1" value={budget} onChange={(e) => setBudget(e.target.value)} placeholder="12.5" style={inp} />
          </label>
          <label style={{ display: "grid", gap: 4 }}>
            <span style={lbl}>{lang === "fr" ? "Devise native" : "Native currency"}</span>
            <select value={progCcy} onChange={(e) => setProgCcy(e.target.value)} style={inp}>
              {Object.keys(rates).map((c) => <option key={c} value={c}>{c}</option>)}
            </select>
          </label>
        </div>
        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}
      </div>
      {newSectorOpen && (
        <NewSectorModal lang={lang}
          onClose={() => setNewSectorOpen(false)}
          onCreated={(sec) => { setSectorId(sec.id); setNewSectorOpen(false); }} />
      )}
    </Modal>
  );
}

// ==================== BULK-ATTACH ORPHAN PROJECTS ============================

function BulkAttachOrphansModal({ lang, orphans, programmes, onClose, onApplied }) {
  const [selected, setSelected] = useStatePL(() => new Set());
  const [targetId, setTargetId] = useStatePL((programmes[0] && programmes[0].id) || "");
  const [busy, setBusy] = useStatePL(false);
  const [err, setErr] = useStatePL(null);
  const [progress, setProgress] = useStatePL({ done: 0, total: 0 });

  const allSelected = selected.size === orphans.length && orphans.length > 0;
  const toggle = (id) => {
    const next = new Set(selected);
    if (next.has(id)) next.delete(id); else next.add(id);
    setSelected(next);
  };
  const toggleAll = () => {
    if (allSelected) setSelected(new Set());
    else setSelected(new Set(orphans.map((o) => o.uuid)));
  };

  const apply = async () => {
    if (selected.size === 0 || !targetId) return;
    setBusy(true); setErr(null);
    setProgress({ done: 0, total: selected.size });
    const targetProg = programmes.find((p) => p.id === targetId);
    try {
      // Sequential updates — keeps the UI predictable. For very large
      // selections we could batch via Promise.all, but realtime fires for
      // each row anyway so sequential is fine.
      let done = 0;
      for (const uuid of selected) {
        await window.melr.updateProject(uuid, { programme_id: targetId });
        done++;
        setProgress({ done, total: selected.size });
      }
      if (onApplied) await onApplied(done, targetProg);
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  };

  const inp = { padding: "8px 10px", borderRadius: 6, border: "1px solid var(--line)", fontSize: 13, width: "100%", boxSizing: "border-box", background: "var(--bg, white)", color: "var(--text)" };
  const lbl = { fontSize: 11, opacity: 0.75 };
  const targetProg = programmes.find((p) => p.id === targetId);

  return (
    <div onClick={(e) => { if (e.target === e.currentTarget && !busy) onClose(); }} style={{
      position: "fixed", inset: 0, background: "rgba(0,0,0,.45)", zIndex: 9999,
      display: "flex", alignItems: "center", justifyContent: "center", padding: 16, overflow: "auto",
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: "var(--bg, white)", color: "var(--text, #111)",
        padding: 22, borderRadius: 10, width: 640, maxWidth: "100%",
        boxShadow: "0 10px 30px rgba(0,0,0,.25)", display: "grid", gap: 12,
        maxHeight: "calc(100vh - 32px)", overflow: "auto",
      }}>
        <div style={{ fontSize: 18, fontWeight: 600 }}>
          {lang === "fr" ? "Rattacher en masse" : "Bulk attach"}
        </div>
        <div className="text-faint" style={{ fontSize: 11.5 }}>
          {lang === "fr"
            ? "Sélectionnez les projets orphelins à rattacher à un programme. Plus de modification individuelle nécessaire."
            : "Select the orphan projects to attach to a programme. No more per-project edit needed."}
        </div>

        <label style={{ display: "grid", gap: 4 }}>
          <span style={lbl}>{lang === "fr" ? "Programme cible" : "Target programme"}</span>
          <select required value={targetId} onChange={(e) => setTargetId(e.target.value)} disabled={busy} style={inp}>
            <option value="">— {lang === "fr" ? "choisir un programme" : "select a programme"} —</option>
            {programmes.map((p) => (
              <option key={p.id} value={p.id}>
                {p.code} — {lang === "en" ? (p.name_en || p.name_fr) : p.name_fr}
              </option>
            ))}
          </select>
        </label>

        <div style={{
          border: "1px solid var(--line)", borderRadius: 6,
          maxHeight: 360, overflowY: "auto",
        }}>
          {/* Header with select-all */}
          <div style={{
            padding: "8px 12px", borderBottom: "1px solid var(--line)",
            background: "var(--bg-sunken)", display: "flex", alignItems: "center", gap: 10,
            position: "sticky", top: 0, zIndex: 1,
          }}>
            <input type="checkbox" checked={allSelected} onChange={toggleAll} disabled={busy} />
            <span style={{ fontSize: 12, fontWeight: 600 }}>
              {allSelected
                ? (lang === "fr" ? "Tout désélectionner" : "Deselect all")
                : (lang === "fr" ? "Tout sélectionner" : "Select all")}
            </span>
            <div style={{ flex: 1 }} />
            <span className="pill" style={{ fontSize: 10.5 }}>
              {selected.size} / {orphans.length}
            </span>
          </div>
          {/* Rows */}
          {orphans.length === 0 ? (
            <div className="text-faint" style={{ padding: 16, textAlign: "center", fontSize: 12 }}>
              {lang === "fr" ? "Aucun projet orphelin." : "No orphan project."}
            </div>
          ) : (
            orphans.map((p) => (
              <label key={p.uuid} style={{
                display: "flex", alignItems: "center", gap: 10,
                padding: "8px 12px", borderBottom: "1px solid var(--line-faint)",
                cursor: busy ? "not-allowed" : "pointer",
              }}>
                <input type="checkbox" checked={selected.has(p.uuid)} onChange={() => toggle(p.uuid)} disabled={busy} />
                <span className="tag-mono text-faint" style={{ fontSize: 10.5, width: 60 }}>{p.id}</span>
                <span className="strong" style={{ fontSize: 12, flex: 1 }}>{projectName(p, lang)}</span>
                <span className="text-faint" style={{ fontSize: 11 }}>{sectorLabel(sectorById(p.sector), lang)}</span>
              </label>
            ))
          )}
        </div>

        {/* Progress / errors */}
        {busy && progress.total > 0 && (
          <div className="row" style={{ alignItems: "center", gap: 10, fontSize: 12 }}>
            <div className="bar" style={{ flex: 1 }}>
              <div className="bar-fill" style={{
                width: ((progress.done / progress.total) * 100) + "%",
                background: "var(--accent)",
              }} />
            </div>
            <span className="mono">{progress.done}/{progress.total}</span>
          </div>
        )}
        {err && <div style={{ color: "#b91c1c", fontSize: 12 }}>{err}</div>}

        <div style={{ display: "flex", gap: 8, justifyContent: "flex-end", borderTop: "1px solid var(--line-faint)", paddingTop: 12 }}>
          <button type="button" onClick={onClose} disabled={busy}
            style={{ padding: "8px 14px", borderRadius: 6, border: "1px solid var(--line)", background: "transparent", color: "var(--text)", cursor: "pointer" }}>
            {lang === "fr" ? "Annuler" : "Cancel"}
          </button>
          <button type="button" onClick={apply} disabled={busy || selected.size === 0 || !targetId}
            style={{ padding: "8px 14px", borderRadius: 6, border: 0, background: "#2563eb", color: "white", cursor: "pointer", fontWeight: 600, opacity: (selected.size === 0 || !targetId) ? 0.5 : 1 }}>
            {busy
              ? (lang === "fr" ? "Rattachement…" : "Attaching…")
              : (selected.size === 0
                ? (lang === "fr" ? "Aucun projet sélectionné" : "No project selected")
                : !targetProg
                  ? (lang === "fr" ? "Choisir un programme" : "Choose a programme")
                  : (lang === "fr"
                    ? `Rattacher ${selected.size} projet${selected.size > 1 ? "s" : ""} au programme ${targetProg.code}`
                    : `Attach ${selected.size} project${selected.size > 1 ? "s" : ""} to ${targetProg.code}`))}
          </button>
        </div>
      </div>
    </div>
  );
}

function initialsPL(name) { return name.split(/\s+/).slice(0, 2).map((s) => s[0]).join("").toUpperCase(); }
function avColorPL(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})`;
}
