const { useState, useEffect, useCallback, useMemo, useRef } = React;

/* --------------------------------------------------- Filet anti-crash ---- */
// Un bug dans un composant n'écroule plus toute l'appli : on affiche un message
// récupérable au lieu d'un écran blanc. (Doit être une classe : les hooks ne
// peuvent pas intercepter les erreurs de rendu.)
class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { err: null }; }
  static getDerivedStateFromError(err) { return { err }; }
  componentDidCatch(err, info) { console.error("[RMA] crash intercepté :", err, info); }
  componentDidUpdate(prev) {
    if (prev.resetKey !== this.props.resetKey && this.state.err) this.setState({ err: null });
  }
  render() {
    if (!this.state.err) return this.props.children;
    const reset = () => this.setState({ err: null });
    if (this.props.fallback) return this.props.fallback(this.state.err, reset);
    return (
      <div style={{ padding: 40, maxWidth: 560, margin: "60px auto", textAlign: "center" }}>
        <div style={{ fontSize: 40, marginBottom: 12 }}>⚠️</div>
        <h2 style={{ margin: "0 0 8px" }}>Une erreur est survenue</h2>
        <p className="muted" style={{ fontSize: 13 }}>
          L'application a rencontré un problème mais tes données sont intactes (rien n'est perdu).
        </p>
        <pre style={{ textAlign: "left", background: "var(--glass)", border: "1px solid var(--stroke)",
          borderRadius: 10, padding: 12, fontSize: 11.5, color: "#fb9d9d", overflow: "auto", maxHeight: 140 }}>
          {String(this.state.err && (this.state.err.message || this.state.err))}
        </pre>
        <div style={{ display: "flex", gap: 8, justifyContent: "center", marginTop: 12 }}>
          <button className="btn" onClick={reset}>Réessayer</button>
          <button className="btn ghost" onClick={() => window.location.reload()}>Recharger l'appli</button>
        </div>
      </div>
    );
  }
}

/* ---------------------------------------------------------------- API ---- */
let onUnauthorized = () => {};   // appelé si une requête renvoie 401 (session expirée)
const api = {
  async get(url) { const r = await fetch(url); if (!r.ok) throw await err(r); return r.json(); },
  async post(url, body) { return send("POST", url, body); },
  async patch(url, body) { return send("PATCH", url, body); },
  async del(url) { const r = await fetch(url, { method: "DELETE" }); if (!r.ok) throw await err(r); },
};
async function send(method, url, body) {
  const r = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
  if (!r.ok) throw await err(r);
  return r.status === 204 ? null : r.json();
}
async function err(r) {
  if (r.status === 401) onUnauthorized();
  try { const j = await r.json(); return new Error(j.detail || r.statusText); } catch { return new Error(r.statusText); }
}

/* ------------------------------------------------------------ helpers ---- */
const STATUSES = [
  { key: "ouvert", label: "Ouvert" },
  { key: "en_attente", label: "En attente" },
  { key: "valide", label: "Validé" },
  { key: "rembourse", label: "Remboursé" },
  { key: "refuse", label: "Refusé" },
];
const ST_COLOR = { ouvert: "#4f8ff7", en_attente: "#f5a623", valide: "#a78bfa", rembourse: "#34d399", refuse: "#f87171" };
const ACTION_TYPES = [
  { key: "relancer", label: "Relancer", icon: "📣" },
  { key: "attendre", label: "Attendre réponse", icon: "⏳" },
  { key: "escalader", label: "Escalader", icon: "🚀" },
  { key: "soumettre", label: "Soumettre", icon: "📤" },
  { key: "autre", label: "Autre", icon: "✦" },
];
const actionMeta = (k) => ACTION_TYPES.find((a) => a.key === k) || { label: k, icon: "✦" };
const DOC_CATS = [
  { key: "facture", label: "Facture / preuve d'achat", icon: "🧾" },
  { key: "photo", label: "Photos du produit", icon: "📷" },
  { key: "serie", label: "Photo du n° de série", icon: "🔢" },
  { key: "etiquette", label: "Étiquette de retour", icon: "🏷️" },
  { key: "autre", label: "Autre document", icon: "📎" },
];
const docCatMeta = (k) => DOC_CATS.find((c) => c.key === k) || DOC_CATS[4];
const todayISO = () => new Date().toISOString().slice(0, 10);
const plusDaysISO = (n) => { const d = new Date(); d.setDate(d.getDate() + n); return d.toISOString().slice(0, 10); };
const statusLabel = (k) => (STATUSES.find((s) => s.key === k) || { label: k }).label;
const fmtMoney = (n, c) => (n || 0).toLocaleString("fr-FR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + " " + (c || "€").replace("EUR", "€");
// Les dates "YYYY-MM-DD" doivent être interprétées en heure LOCALE (pas UTC),
// sinon un dossier dû aujourd'hui apparaît "J-1" le matin.
const parseLocalDate = (s) => { const [y, m, dd] = String(s).slice(0, 10).split("-").map(Number); return new Date(y, m - 1, dd); };
const todayLocal = () => { const n = new Date(); return new Date(n.getFullYear(), n.getMonth(), n.getDate()); };
const fmtDate = (s) => (s ? (String(s).length === 10 ? parseLocalDate(s) : new Date(s)).toLocaleDateString("fr-FR") : "—");
const fmtDateTime = (s) => (s ? new Date(s).toLocaleString("fr-FR") : "—");
const CLOSED = ["rembourse", "refuse"];
const isOverdue = (d) => d.due_date && !CLOSED.includes(d.status) && parseLocalDate(d.due_date) < todayLocal();
const daysUntil = (s) => Math.round((parseLocalDate(s) - todayLocal()) / 86400000);
const ago = (s) => {
  const days = Math.floor((Date.now() - new Date(s)) / 86400000);
  return days <= 0 ? "aujourd'hui" : days === 1 ? "hier" : `il y a ${days} j`;
};

function Badge({ status }) { return <span className={"badge " + status}>{statusLabel(status)}</span>; }

function SiteLogo({ name, logo, size = 30 }) {
  const [broken, setBroken] = useState(false);
  useEffect(() => setBroken(false), [logo]);
  if (!logo || broken) {
    const initials = (name || "?").replace(/[^A-Za-zÀ-ÿ0-9 ]/g, "").split(/\s+/).filter(Boolean).map((w) => w[0]).slice(0, 2).join("").toUpperCase() || "?";
    return <span className="mono-logo" style={{ width: size, height: size, fontSize: size * 0.36 }}>{initials}</span>;
  }
  return <img className="site-logo" src={"/logos/" + logo} width={size} height={size}
    style={{ width: size, height: size }} alt={name} onError={() => setBroken(true)} />;
}

/* ------------------------------------------------------------- icons ----- */
const Icon = {
  grid: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>,
  folder: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 7a2 2 0 0 1 2-2h4l2 3h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/></svg>,
  layers: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="m2 17 10 5 10-5"/><path d="m2 12 10 5 10-5"/></svg>,
  store: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M3 9l1.5-5h15L21 9"/><path d="M3 9a3 3 0 0 0 6 0 3 3 0 0 0 6 0 3 3 0 0 0 6 0"/><path d="M5 12v8h14v-8"/></svg>,
  search: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="15" height="15"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>,
  mail: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>,
  shield: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 3l7 3v6c0 4.4-3 7.5-7 9-4-1.5-7-4.6-7-9V6l7-3z"/></svg>,
};

/* Icônes inline (bouton/texte) — remplacent les emojis les plus répétés dans l'UI. */
const ic = { display: "inline-block", verticalAlign: "-2px", marginRight: 5 };
const Ico = {
  x: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" style={{ ...ic, marginRight: 0 }}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>,
  check: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.6" style={ic}><path d="M20 6 9 17l-5-5"/></svg>,
  edit: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" style={ic}><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4Z"/></svg>,
  trash: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" style={{ ...ic, marginRight: 0 }}><path d="M3 6h18"/><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg>,
  warn: <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" strokeWidth="2.2" style={ic}><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>,
  key: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" style={ic}><circle cx="7.5" cy="15.5" r="4.5"/><path d="m10.5 12.5 8-8"/><path d="M15 8l2 2"/><path d="M18 5l2 2"/></svg>,
  eye: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" style={ic}><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7-11-7-11-7Z"/><circle cx="12" cy="12" r="3"/></svg>,
  lock: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" style={ic}><rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>,
  lockOpen: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" style={ic}><rect x="4" y="11" width="16" height="9" rx="2"/><path d="M8 11V7a4 4 0 0 1 7.5-2"/></svg>,
  doc: <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" style={ic}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z"/><path d="M14 2v6h6"/></svg>,
  copy: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" style={ic}><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>,
  globe: <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" style={ic}><circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10Z"/></svg>,
  send: <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.2" style={ic}><path d="m22 2-7 20-4-9-9-4Z"/><path d="M22 2 11 13"/></svg>,
};

/* ------------------------------------------------------------- toasts ---- */
let pushToast = () => {};
function toast(msg, isError) { pushToast(msg, isError); }
function Toasts() {
  const [items, setItems] = useState([]);
  useEffect(() => {
    pushToast = (msg, isError) => {
      const id = Date.now() + Math.random();
      setItems((it) => [...it, { id, msg, isError }]);
      setTimeout(() => setItems((it) => it.filter((x) => x.id !== id)), 3500);
    };
    return () => { pushToast = () => {}; };
  }, []);
  return (
    <div className="toasts">
      {items.map((t) => (
        <div key={t.id} className={"toast" + (t.isError ? " error" : "")}><span className="tdot" />{t.msg}</div>
      ))}
    </div>
  );
}

/* ----------------------------------------------------------- Auth gate --- */
function AuthGate() {
  const [state, setState] = useState("loading"); // loading | login | ready
  const [user, setUser] = useState(null);

  const check = useCallback(async () => {
    try { const u = await api.get("/api/auth/me"); setUser(u); setState("ready"); }
    catch { setState("login"); }
  }, []);
  useEffect(() => { check(); }, [check]);
  useEffect(() => { onUnauthorized = () => { setUser(null); setState("login"); }; return () => { onUnauthorized = () => {}; }; }, []);

  const logout = async () => { try { await api.post("/api/auth/logout", {}); } catch {} setUser(null); setState("login"); };

  if (state === "loading") return <div className="boot">Chargement…</div>;
  if (state === "login") return <Login onLogged={check} />;
  return <App user={user} onLogout={logout} onUserChange={check} />;
}

function StarField({ className = "login-stars" }) {
  const ref = useRef(null);
  useEffect(() => {
    const canvas = ref.current;
    const ctx = canvas.getContext("2d");
    const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    let w = 0, h = 0, dust = [], shooting = [], raf = 0;
    let lastShoot = 0, nextShoot = 5000;

    function resize() {
      w = canvas.clientWidth; h = canvas.clientHeight;
      canvas.width = w * dpr; canvas.height = h * dpr;
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
      const count = Math.min(150, Math.round((w * h) / 11000));
      dust = Array.from({ length: count }, () => ({
        x: Math.random() * w, y: Math.random() * h,
        r: Math.random() * 1.2 + 0.3,
        vx: (Math.random() * 0.12 + 0.02) * (Math.random() < 0.5 ? -1 : 1),
        vy: (Math.random() * 0.08 + 0.015),
        a: Math.random() * 0.5 + 0.2,
        tw: Math.random() * Math.PI * 2,
      }));
    }
    resize();
    window.addEventListener("resize", resize);

    function spawnShoot() {
      const dir = Math.random() < 0.5 ? 1 : -1;
      const speed = 7 + Math.random() * 5;
      const ang = (16 + Math.random() * 14) * Math.PI / 180;
      shooting.push({
        x: dir > 0 ? Math.random() * w * 0.5 : w * 0.5 + Math.random() * w * 0.5,
        y: Math.random() * h * 0.35,
        vx: Math.cos(ang) * speed * dir, vy: Math.sin(ang) * speed,
        len: 110 + Math.random() * 90,
      });
    }

    function drawDust() {
      for (const p of dust) {
        const a = p.a * (0.6 + 0.4 * Math.sin(p.tw));
        ctx.globalAlpha = a; ctx.fillStyle = "#fff";
        ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, 7); ctx.fill();
      }
      ctx.globalAlpha = 1;
    }

    function frame(t) {
      ctx.clearRect(0, 0, w, h);
      for (const p of dust) {
        p.x += p.vx; p.y += p.vy; p.tw += 0.02;
        if (p.x < -2) p.x = w + 2; if (p.x > w + 2) p.x = -2;
        if (p.y > h + 2) p.y = -2; if (p.y < -2) p.y = h + 2;
      }
      drawDust();
      if (t - lastShoot > nextShoot) { spawnShoot(); lastShoot = t; nextShoot = 9000 + Math.random() * 15000; }
      shooting = shooting.filter((s) => s.x > -300 && s.x < w + 300 && s.y < h + 300);
      for (const s of shooting) {
        const mag = Math.hypot(s.vx, s.vy), ux = s.vx / mag, uy = s.vy / mag;
        const tx = s.x - ux * s.len, ty = s.y - uy * s.len;
        const grad = ctx.createLinearGradient(s.x, s.y, tx, ty);
        grad.addColorStop(0, "rgba(255,255,255,0.9)");
        grad.addColorStop(1, "rgba(255,255,255,0)");
        ctx.strokeStyle = grad; ctx.lineWidth = 2; ctx.lineCap = "round";
        ctx.beginPath(); ctx.moveTo(s.x, s.y); ctx.lineTo(tx, ty); ctx.stroke();
        ctx.fillStyle = "rgba(255,255,255,0.95)";
        ctx.beginPath(); ctx.arc(s.x, s.y, 1.6, 0, 7); ctx.fill();
        s.x += s.vx; s.y += s.vy;
      }
      raf = requestAnimationFrame(frame);
    }

    if (reduced) { drawDust(); }              // pas d'animation si l'utilisateur l'a désactivée
    else raf = requestAnimationFrame(frame);

    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", resize); };
  }, []);
  return <canvas ref={ref} className={className} aria-hidden="true" />;
}

function Login({ onLogged }) {
  const [loginId, setLoginId] = useState("");
  const [code, setCode] = useState("");
  const [error, setError] = useState("");
  const [busy, setBusy] = useState(false);

  const submit = async (e) => {
    e && e.preventDefault();
    setError(""); setBusy(true);
    try {
      await api.post("/api/auth/login", { login_id: loginId.trim(), license_code: code.trim() });
      onLogged();
    } catch (err) { setError(err.message || "Connexion impossible"); setBusy(false); }
  };

  return (
    <div className="login-wrap">
      <StarField />
      <form className="login-card" onSubmit={submit}>
        <div className="login-brand login-brand-logo">
          <img src="/logo.png" alt="Logo" className="brand-logo-img" />
        </div>

        <h2 className="login-title serif">Connexion</h2>
        <p className="login-sub">Accès privé — identifie-toi avec ton ID et ton code licence.</p>
        <div className="login-rule" />

        <div className="field"><label>Identifiant</label>
          <input value={loginId} autoFocus onChange={(e) => setLoginId(e.target.value)} placeholder="ton identifiant" /></div>
        <div className="field"><label>Code licence</label>
          <input type="password" value={code} onChange={(e) => setCode(e.target.value)} placeholder="XXXX-XXXX-XXXX" /></div>

        {error && <div className="err" style={{ marginBottom: 10 }}>{error}</div>}
        <button className="btn-lux" type="submit" disabled={busy}>
          {busy ? "Connexion…" : "Se connecter"}
        </button>
        <div className="faint" style={{ fontSize: 11, marginTop: 16, textAlign: "center", letterSpacing: ".3px" }}>
          Accès réservé — contacte l'administrateur pour obtenir un code.
        </div>
      </form>
    </div>
  );
}

/* ================================================================ APP ==== */
function App({ user, onLogout, onUserChange }) {
  const [tab, setTab] = useState("overview");
  const [sites, setSites] = useState([]);
  const [dossiers, setDossiers] = useState([]);
  const [refreshKey, setRefreshKey] = useState(0);
  const [paletteOpen, setPaletteOpen] = useState(false);
  const [openDossier, setOpenDossier] = useState(null);
  const [creating, setCreating] = useState(false);
  const bump = useCallback(() => setRefreshKey((k) => k + 1), []);

  useEffect(() => { api.get("/api/sites").then(setSites); }, [refreshKey]);
  // include_archived : on garde les archivés pour les stats historiques (taux de succès,
  // montant récupéré) ; chaque vue opérationnelle les filtre via d.archived_at.
  useEffect(() => { api.get("/api/dossiers?include_archived=true").then(setDossiers); }, [refreshKey]);

  // Raccourcis clavier globaux
  useEffect(() => {
    const h = (e) => {
      const typing = /INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName || "");
      if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setPaletteOpen((o) => !o); }
      else if (!typing && !e.ctrlKey && !e.metaKey && e.key.toLowerCase() === "n") { e.preventDefault(); setCreating(true); }
    };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, []);

  const alerts = useMemo(() => {
    const open = dossiers.filter((d) => !CLOSED.includes(d.status) && d.due_date && !d.archived_at);
    return open
      .map((d) => ({ d, days: daysUntil(d.due_date) }))
      .filter((x) => x.days <= 7)
      .sort((a, b) => a.days - b.days);
  }, [dossiers]);

  // File de travail : actions dues aujourd'hui ou en retard
  const dueActions = useMemo(() => dossiers
    .filter((d) => !CLOSED.includes(d.status) && !d.archived_at && d.next_action_date && daysUntil(d.next_action_date) <= 0)
    .sort((a, b) => new Date(a.next_action_date) - new Date(b.next_action_date)), [dossiers]);

  const NAV = [
    ["overview", "Vue d'ensemble", Icon.grid, dueActions.length],
    ["dossiers", "Dossiers", Icon.folder, dossiers.filter((d) => !CLOSED.includes(d.status) && !d.archived_at).length],
    ["bulk", "Création en masse", Icon.layers, 0],
    ["sites", "Sites & marques", Icon.store, 0],
    ["gmail", "Gmail", Icon.mail, 0],
    ...(user && user.is_admin ? [["admin", "Admin", Icon.shield, 0]] : []),
  ];
  const TITLES = {
    overview: ["Vue d'ensemble", "Pilotage global de l'activité RMA"],
    dossiers: ["Dossiers", "Suivi et gestion des dossiers d'indemnisation"],
    bulk: ["Création en masse", "Saisie multiple et import CSV"],
    sites: ["Sites & marques", "Configuration des fabricants et parcours RMA"],
    gmail: ["Gmail", "Détection automatique des réponses fabricants"],
    admin: ["Admin", "Gestion des comptes et des accès"],
  };

  // Retour du consentement Google (?gmail=ok|refused|error)
  useEffect(() => {
    const p = new URLSearchParams(window.location.search);
    const r = p.get("gmail");
    if (r) {
      const msg = p.get("msg");
      window.history.replaceState({}, "", "/");
      setTab("gmail");
      if (r === "ok") toast("Boîte connectée ✓" + (p.get("account") ? " : " + p.get("account") : ""));
      else if (r === "refused") toast("Accès Gmail refusé", true);
      else toast("Erreur Gmail : " + (msg || "connexion échouée"), true);
    }
  }, []);

  return (
    <div className="shell">
      <StarField className="app-stars" />
      <aside className="sidebar">
        <div className="brand brand-logo">
          <img src="/logo.png" alt="Logo" className="brand-logo-img sidebar-logo-img" />
        </div>
        {NAV.map(([k, label, icon, badge]) => (
          <button key={k} className={"nav-item" + (tab === k ? " active" : "")} onClick={() => setTab(k)}>
            {icon}{label}
            {badge > 0 && <span className="nbadge">{badge}</span>}
          </button>
        ))}
        <div className="side-foot">
          <div className="side-kbd"><kbd>Ctrl</kbd><kbd>K</kbd> recherche</div>
          <div className="side-kbd"><kbd>N</kbd> nouveau dossier</div>
          {user && (
            <div className="side-user">
              <div className="su-avatar">{(user.full_name || user.login_id || "?")[0].toUpperCase()}</div>
              <div style={{ minWidth: 0, flex: 1 }}>
                <div className="su-name">{user.full_name || user.login_id}{user.is_admin && <span className="su-admin"> admin</span>}</div>
                <button className="linkish" style={{ fontSize: 11.5 }} onClick={onLogout}>Se déconnecter</button>
              </div>
            </div>
          )}
        </div>
      </aside>

      <main className="main">
        <div className="topbar">
          <div>
            <h1 className="serif">{TITLES[tab][0]}</h1>
            <div className="sub">{TITLES[tab][1]}</div>
          </div>
          <button className="searchbox" onClick={() => setPaletteOpen(true)}>
            {Icon.search} Rechercher… <kbd>Ctrl K</kbd>
          </button>
          <a className="btn ghost sm" href="/api/dossiers/csv/export" download>⭳ Export CSV</a>
          <button className="btn" onClick={() => setCreating(true)}>+ Nouveau dossier</button>
        </div>

        <ErrorBoundary resetKey={tab}>
          {tab === "overview" && <Overview dossiers={dossiers} sites={sites} alerts={alerts} dueActions={dueActions} onOpen={setOpenDossier} onChange={bump} goTo={setTab} />}
          {tab === "dossiers" && <Dossiers sites={sites} refreshKey={refreshKey} onChange={bump} openId={openDossier} clearOpen={() => setOpenDossier(null)} />}
          {tab === "bulk" && <BulkCreate sites={sites} onDone={() => { bump(); setTab("dossiers"); toast("Dossiers créés"); }} />}
          {tab === "sites" && <SitesConfig sites={sites} onChange={bump} user={user} />}
          {tab === "gmail" && <GmailView onChange={bump} onOpen={setOpenDossier} />}
          {tab === "admin" && <AdminPanel currentUser={user} />}
        </ErrorBoundary>
      </main>

      {openDossier != null && tab !== "dossiers" &&
        <DossierDrawer id={openDossier} sites={sites} onClose={() => setOpenDossier(null)} onChanged={bump} />}
      {creating &&
        <DossierForm sites={sites} onClose={() => setCreating(false)}
          onSaved={() => { setCreating(false); bump(); toast("Dossier créé"); }} />}
      {paletteOpen &&
        <Palette dossiers={dossiers} sites={sites} onClose={() => setPaletteOpen(false)}
          onPick={(action) => {
            setPaletteOpen(false);
            if (action.type === "dossier") setOpenDossier(action.id);
            if (action.type === "tab") setTab(action.id);
            if (action.type === "new") setCreating(true);
          }} />}
      <Toasts />
    </div>
  );
}

/* ------------------------------------------------------ Command palette -- */
function Palette({ dossiers, sites, onClose, onPick }) {
  const [q, setQ] = useState("");
  const [sel, setSel] = useState(0);
  const ref = useRef(null);
  useEffect(() => ref.current?.focus(), []);

  const results = useMemo(() => {
    const ql = q.toLowerCase().trim();
    const acts = [
      { type: "new", label: "➕ Créer un nouveau dossier", tag: "action", key: "a-new" },
      { type: "tab", id: "overview", label: "📊 Vue d'ensemble", tag: "navigation", key: "a-ov" },
      { type: "tab", id: "dossiers", label: "🗂 Dossiers", tag: "navigation", key: "a-do" },
      { type: "tab", id: "bulk", label: "⚡ Création en masse", tag: "navigation", key: "a-bu" },
      { type: "tab", id: "sites", label: "🏬 Sites & marques", tag: "navigation", key: "a-si" },
      { type: "tab", id: "gmail", label: "📧 Gmail", tag: "navigation", key: "a-gm" },
    ].filter((a) => !ql || a.label.toLowerCase().includes(ql));
    const doss = !ql ? [] : dossiers
      .filter((d) => [d.reference, d.client_name, d.product, d.order_ref, d.serial_number, d.site_name]
        .some((f) => (f || "").toLowerCase().includes(ql)))
      .slice(0, 7)
      .map((d) => ({ type: "dossier", id: d.id, label: `${d.reference} · ${d.client_name} — ${d.product}`, tag: statusLabel(d.status), key: "d" + d.id, logo: d.site_logo, site: d.site_name }));
    const sts = !ql ? [] : sites
      .filter((s) => s.name.toLowerCase().includes(ql) || (s.category || "").toLowerCase().includes(ql))
      .slice(0, 5)
      .map((s) => ({ type: "tab", id: "sites", label: s.name, tag: s.category || "site", key: "s" + s.id, logo: s.logo, site: s.name }));
    return [...doss, ...sts, ...acts].slice(0, 12);
  }, [q, dossiers, sites]);

  useEffect(() => setSel(0), [q]);

  const onKey = (e) => {
    if (e.key === "Escape") onClose();
    else if (e.key === "ArrowDown") { e.preventDefault(); setSel((s) => Math.min(s + 1, results.length - 1)); }
    else if (e.key === "ArrowUp") { e.preventDefault(); setSel((s) => Math.max(s - 1, 0)); }
    else if (e.key === "Enter" && results[sel]) onPick(results[sel]);
  };

  return (
    <div className="palette-overlay" onClick={(e) => e.target.classList.contains("palette-overlay") && onClose()}>
      <div className="palette">
        <input ref={ref} placeholder="Rechercher un dossier, un site, une action…" value={q}
          onChange={(e) => setQ(e.target.value)} onKeyDown={onKey} />
        <div className="results">
          {results.length === 0 && <div className="pal-empty">Aucun résultat pour « {q} »</div>}
          {results.map((r, i) => (
            <div key={r.key} className={"pal-item" + (i === sel ? " sel" : "")}
              onMouseEnter={() => setSel(i)} onClick={() => onPick(r)}>
              {r.site != null ? <SiteLogo name={r.site} logo={r.logo} size={24} /> : null}
              <span>{r.label}</span>
              <span className="tag">{r.tag}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ----------------------------------------------------------- Overview ---- */
function Overview({ dossiers, sites, alerts, dueActions, onOpen, onChange, goTo }) {
  const open = dossiers.filter((d) => !CLOSED.includes(d.status) && !d.archived_at);
  const openAmount = open.reduce((s, d) => s + d.amount_claimed, 0);
  const refunded = dossiers.filter((d) => d.status === "rembourse");   // historique (archivés inclus)
  const refundedAmount = refunded.reduce((s, d) => s + d.amount_claimed, 0);
  const decided = dossiers.filter((d) => CLOSED.includes(d.status)).length;
  const successRate = decided ? Math.round((refunded.length / decided) * 100) : null;

  const byStatus = STATUSES.map((s) => ({ ...s, count: dossiers.filter((d) => d.status === s.key).length }));
  const bySite = useMemo(() => {
    const m = {};
    for (const d of open) {
      const key = d.site_name || "Sans site";
      m[key] = m[key] || { name: key, logo: d.site_logo, count: 0, amount: 0 };
      m[key].count += 1; m[key].amount += d.amount_claimed;
    }
    return Object.values(m).sort((a, b) => b.count - a.count).slice(0, 8);
  }, [dossiers]);
  const maxSite = Math.max(1, ...bySite.map((s) => s.count));
  const recent = [...dossiers].filter((d) => !d.archived_at).sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)).slice(0, 6);

  // Sparkline : dossiers créés par semaine (8 dernières semaines)
  const spark = useMemo(() => {
    const weeks = Array(8).fill(0);
    for (const d of dossiers) {
      const w = Math.floor((Date.now() - new Date(d.created_at)) / (7 * 86400000));
      if (w >= 0 && w < 8) weeks[7 - w] += 1;
    }
    return weeks;
  }, [dossiers]);

  // Suggestions de relance : dossiers actifs sans action planifiée dont la
  // dernière mise à jour dépasse le SLA de la marque.
  const slaMap = useMemo(() => Object.fromEntries(sites.map((s) => [s.id, s.sla_days ?? 7])), [sites]);
  const suggestions = useMemo(() => dossiers
    .filter((d) => !CLOSED.includes(d.status) && !d.next_action_type)
    .map((d) => ({ d, silent: Math.floor((Date.now() - new Date(d.updated_at)) / 86400000), sla: slaMap[d.site_id] ?? 7 }))
    .filter((x) => x.silent >= x.sla)
    .sort((a, b) => b.silent - a.silent)
    .slice(0, 6), [dossiers, slaMap]);

  const markDone = async (d) => {
    try { await api.post(`/api/dossiers/${d.id}/action-done`, {}); toast("Action faite : " + actionMeta(d.next_action_type).label); onChange(); }
    catch (e) { toast(e.message, true); }
  };
  const postpone = async (d, days) => {
    try {
      await api.post(`/api/dossiers/${d.id}/next-action`, { action_type: d.next_action_type, action_date: plusDaysISO(days), note: d.next_action_note || null });
      toast(`Reporté à J+${days}`); onChange();
    } catch (e) { toast(e.message, true); }
  };
  const planRelance = async (d) => {
    try {
      await api.post(`/api/dossiers/${d.id}/next-action`, { action_type: "relancer", action_date: todayISO(), note: "Suggestion SLA : sans réponse depuis trop longtemps" });
      toast("Relance planifiée pour aujourd'hui"); onChange();
    } catch (e) { toast(e.message, true); }
  };

  return (
    <div>
      <div className="panel wq" style={{ marginBottom: 13 }}>
        <h3>🎯 File de travail
          <span className="hint">{dueActions.length === 0 ? "rien à faire aujourd'hui" : `${dueActions.length} action(s) à faire`}</span>
        </h3>
        {dueActions.length === 0 && suggestions.length === 0 &&
          <div className="faint" style={{ padding: "4px 0 2px" }}>Tout est à jour. Les actions planifiées et les suggestions de relance apparaîtront ici.</div>}
        {dueActions.map((d) => {
          const late = daysUntil(d.next_action_date) < 0;
          const meta = actionMeta(d.next_action_type);
          return (
            <div className="alert-item" key={d.id}>
              <span className="adot" style={{ background: late ? ST_COLOR.refuse : ST_COLOR.rembourse }} />
              <SiteLogo name={d.site_name} logo={d.site_logo} size={24} />
              <span className="atxt" onClick={() => onOpen(d.id)} style={{ cursor: "pointer" }}>
                <b>{meta.icon} {meta.label}</b> — {d.reference} · {d.client_name} ({d.product})
                {d.next_action_note && <span className="faint"> · {d.next_action_note}</span>}
              </span>
              {late && <span className="awhen overdue">{-daysUntil(d.next_action_date)} j de retard</span>}
              <button className="btn sm" onClick={() => markDone(d)}>{Ico.check} Fait</button>
              <button className="btn ghost sm" onClick={() => postpone(d, 3)} title="Reporter de 3 jours">+3 j</button>
            </div>
          );
        })}
        {suggestions.length > 0 && (
          <div style={{ marginTop: dueActions.length ? 12 : 0 }}>
            <div className="faint" style={{ fontSize: 11.5, textTransform: "uppercase", letterSpacing: ".8px", margin: "6px 0 8px" }}>
              Suggestions — sans réponse au-delà du délai cible de la marque
            </div>
            {suggestions.map(({ d, silent, sla }) => (
              <div className="alert-item" key={d.id}>
                <span className="adot" style={{ background: ST_COLOR.en_attente }} />
                <SiteLogo name={d.site_name} logo={d.site_logo} size={24} />
                <span className="atxt" onClick={() => onOpen(d.id)} style={{ cursor: "pointer" }}>
                  <b>{d.reference}</b> · {d.client_name} ({d.product}) — silencieux depuis <b>{silent} j</b> <span className="faint">(SLA {sla} j)</span>
                </span>
                <button className="btn gold sm" onClick={() => planRelance(d)}>📣 Planifier relance</button>
              </div>
            ))}
          </div>
        )}
      </div>

      <div className="kpis">
        <div className="kpi">
          <div className="k">Dossiers en cours</div>
          <div className="v">{open.length}</div>
          <div className="trend">{dossiers.length} au total</div>
          <Sparkline data={spark} />
        </div>
        <div className="kpi">
          <div className="k">Montant en jeu</div>
          <div className="v gold">{fmtMoney(openAmount)}</div>
          <div className="trend">sur les dossiers ouverts</div>
        </div>
        <div className="kpi">
          <div className="k">Récupéré</div>
          <div className="v" style={{ color: ST_COLOR.rembourse }}>{fmtMoney(refundedAmount)}</div>
          <div className="trend">{refunded.length} dossier(s) remboursé(s)</div>
        </div>
        <div className="kpi">
          <div className="k">Taux de succès</div>
          <div className="v">{successRate == null ? "—" : successRate + " %"}</div>
          <div className="trend">{decided} dossier(s) tranché(s)</div>
        </div>
        <div className="kpi">
          <div className="k">Actions dues</div>
          <div className="v" style={{ color: dueActions.length ? "var(--gold)" : "inherit" }}>{dueActions.length}</div>
          <div className="trend">file de travail du jour</div>
        </div>
        <div className="kpi">
          <div className="k">Échéances ≤ 7 j</div>
          <div className="v" style={{ color: alerts.length ? ST_COLOR.en_attente : "inherit" }}>{alerts.length}</div>
          <div className="trend">{alerts.filter((a) => a.days < 0).length} en retard</div>
        </div>
      </div>

      <div className="two-col">
        <div className="panel">
          <h3>Charge par fabricant <span className="hint">dossiers en cours</span></h3>
          {bySite.length === 0 && <div className="empty">Aucun dossier en cours.</div>}
          {bySite.map((s) => (
            <div className="hbar" key={s.name}>
              <div className="lbl"><SiteLogo name={s.name} logo={s.logo} size={22} />{s.name}</div>
              <div className="track"><div className="fill" style={{ width: (s.count / maxSite) * 100 + "%" }} /></div>
              <div className="val">{s.count}</div>
            </div>
          ))}
        </div>
        <div className="panel">
          <h3>Répartition par statut</h3>
          <Donut data={byStatus} total={dossiers.length} />
        </div>
      </div>

      <div className="two-col">
        <div className="panel">
          <h3>{Ico.warn} Échéances proches <span className="hint">≤ 7 jours ou dépassées</span></h3>
          {alerts.length === 0 && <div className="empty" style={{ padding: 22 }}>Rien d'urgent. 👌</div>}
          {alerts.slice(0, 8).map(({ d, days }) => (
            <div className="alert-item" key={d.id} onClick={() => onOpen(d.id)}>
              <span className="adot" style={{ background: days < 0 ? ST_COLOR.refuse : ST_COLOR.en_attente }} />
              <SiteLogo name={d.site_name} logo={d.site_logo} size={22} />
              <span className="atxt"><b>{d.reference}</b> · {d.client_name} — {d.product}</span>
              <span className="awhen" style={{ color: days < 0 ? ST_COLOR.refuse : ST_COLOR.en_attente }}>
                {days < 0 ? `${-days} j de retard` : days === 0 ? "aujourd'hui" : `J-${days}`}
              </span>
            </div>
          ))}
        </div>
        <div className="panel">
          <h3>Activité récente</h3>
          {recent.length === 0 && <div className="empty" style={{ padding: 22 }}>Aucune activité.</div>}
          {recent.map((d) => (
            <div className="alert-item" key={d.id} onClick={() => onOpen(d.id)}>
              <SiteLogo name={d.site_name} logo={d.site_logo} size={22} />
              <span className="atxt"><b>{d.client_name}</b> — {d.product}</span>
              <Badge status={d.status} />
              <span className="awhen faint">{ago(d.updated_at)}</span>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

function Sparkline({ data }) {
  const max = Math.max(1, ...data);
  const w = 90, h = 30;
  const pts = data.map((v, i) => `${(i / (data.length - 1)) * w},${h - (v / max) * (h - 4) - 2}`).join(" ");
  return (
    <svg className="spark" width={w} height={h} viewBox={`0 0 ${w} ${h}`}>
      <polyline points={pts} fill="none" stroke="url(#sg)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
      <defs><linearGradient id="sg" x1="0" x2="1"><stop offset="0" stopColor="#ffffff" stopOpacity="0.55"/><stop offset="1" stopColor="#ffffff"/></linearGradient></defs>
    </svg>
  );
}

function Donut({ data, total }) {
  const R = 54, C = 2 * Math.PI * R;
  let acc = 0;
  return (
    <div className="donut-wrap">
      <svg width="150" height="150" viewBox="0 0 150 150">
        <circle cx="75" cy="75" r={R} fill="none" stroke="rgba(255,255,255,.06)" strokeWidth="16" />
        {total > 0 && data.map((s) => {
          const frac = s.count / total;
          const el = (
            <circle key={s.key} cx="75" cy="75" r={R} fill="none" stroke={ST_COLOR[s.key]} strokeWidth="16"
              strokeDasharray={`${frac * C} ${C}`} strokeDashoffset={-acc * C}
              transform="rotate(-90 75 75)" strokeLinecap="butt" />
          );
          acc += frac;
          return el;
        })}
        <text x="75" y="71" textAnchor="middle" fill="#eef2f7" fontSize="24" fontWeight="750">{total}</text>
        <text x="75" y="90" textAnchor="middle" fill="#96a0ae" fontSize="10">dossiers</text>
      </svg>
      <div className="donut-legend">
        {data.map((s) => (
          <div className="dleg" key={s.key}>
            <span className="sw" style={{ background: ST_COLOR[s.key] }} />
            {s.label}
            <span className="n">{s.count}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ----------------------------------------------------------- Dossiers ---- */
function Dossiers({ sites, refreshKey, onChange, openId, clearOpen }) {
  const [rows, setRows] = useState([]);
  const [loading, setLoading] = useState(true);
  const [view, setView] = useState("table");
  const [archived, setArchived] = useState(false);
  const [f, setF] = useState({ site_id: "", status: "", q: "", min: "", max: "" });
  const [sort, setSort] = useState({ key: "updated_at", dir: -1 });
  const [selected, setSelected] = useState(openId ?? null);

  useEffect(() => { if (openId != null) { setSelected(openId); clearOpen(); } }, [openId]);

  const load = useCallback(async () => {
    setLoading(true);
    const p = new URLSearchParams();
    if (f.site_id) p.set("site_id", f.site_id);
    if (f.status) p.set("status", f.status);
    if (f.q) p.set("q", f.q);
    if (f.min) p.set("min_amount", f.min);
    if (f.max) p.set("max_amount", f.max);
    if (archived) p.set("archived_only", "true");
    setRows(await api.get("/api/dossiers?" + p.toString()));
    setLoading(false);
  }, [f, archived, refreshKey]);
  useEffect(() => { load(); }, [load]);

  const sorted = useMemo(() => {
    const r = [...rows];
    r.sort((a, b) => {
      let va = a[sort.key], vb = b[sort.key];
      if (va == null) return 1; if (vb == null) return -1;
      if (typeof va === "string") { va = va.toLowerCase(); vb = (vb || "").toLowerCase(); }
      return (va < vb ? -1 : va > vb ? 1 : 0) * sort.dir;
    });
    return r;
  }, [rows, sort]);

  const clickSort = (key) => setSort((s) => ({ key, dir: s.key === key ? -s.dir : -1 }));
  const refreshAll = () => { load(); onChange(); };

  return (
    <div>
      <div className="filters">
        <input className="grow" placeholder="Filtrer (client, produit, réf, n° série…)"
          value={f.q} onChange={(e) => setF({ ...f, q: e.target.value })} />
        <select value={f.site_id} onChange={(e) => setF({ ...f, site_id: e.target.value })}>
          <option value="">Tous les sites</option>
          {sites.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
        </select>
        <select value={f.status} onChange={(e) => setF({ ...f, status: e.target.value })}>
          <option value="">Tous statuts</option>
          {STATUSES.map((s) => <option key={s.key} value={s.key}>{s.label}</option>)}
        </select>
        <input style={{ width: 88 }} type="number" placeholder="€ min" value={f.min} onChange={(e) => setF({ ...f, min: e.target.value })} />
        <input style={{ width: 88 }} type="number" placeholder="€ max" value={f.max} onChange={(e) => setF({ ...f, max: e.target.value })} />
        {(f.site_id || f.status || f.q || f.min || f.max) &&
          <button className="btn ghost sm" onClick={() => setF({ site_id: "", status: "", q: "", min: "", max: "" })}>{Ico.x} Filtres</button>}
        <button className={"chip" + (archived ? " active" : "")} style={{ marginLeft: "auto" }}
          onClick={() => setArchived((a) => !a)} title="Dossiers clos et archivés">
          🗄 {archived ? "Archives" : "Voir archives"}
        </button>
        <div className="seg">
          <button className={view === "table" ? "active" : ""} onClick={() => setView("table")}>Tableau</button>
          <button className={view === "kanban" ? "active" : ""} onClick={() => setView("kanban")}>Kanban</button>
        </div>
      </div>

      {loading ? <div className="empty">Chargement…</div>
        : rows.length === 0 ? <div className="empty"><div className="big">📭</div>Aucun dossier trouvé.</div>
        : view === "table" ? <TableView rows={sorted} sort={sort} onSort={clickSort} onSelect={setSelected} />
        : <KanbanView rows={sorted} onSelect={setSelected} />}

      {selected != null &&
        <DossierDrawer id={selected} sites={sites} onClose={() => setSelected(null)} onChanged={refreshAll} />}
    </div>
  );
}

function TableView({ rows, sort, onSort, onSelect }) {
  const H = ({ k, children, right }) => (
    <th onClick={() => onSort(k)} style={right ? { textAlign: "right" } : null}>
      {children}{sort.key === k && <span className="arrow">{sort.dir === -1 ? "↓" : "↑"}</span>}
    </th>
  );
  return (
    <div className="table-wrap">
      <table className="data">
        <thead><tr>
          <H k="reference">Référence</H>
          <H k="site_name">Site</H>
          <H k="client_name">Client</H>
          <H k="product">Produit</H>
          <H k="amount_claimed" right>Montant</H>
          <H k="status">Statut</H>
          <H k="next_action_date">Action</H>
          <H k="due_date">Échéance</H>
          <H k="updated_at">MàJ</H>
        </tr></thead>
        <tbody>
          {rows.map((d) => {
            const days = d.due_date ? daysUntil(d.due_date) : null;
            return (
              <tr key={d.id} onClick={() => onSelect(d.id)}>
                <td className="faint">{d.reference}</td>
                <td><span className="cell-site"><SiteLogo name={d.site_name} logo={d.site_logo} size={26} />{d.site_name || "—"}</span></td>
                <td style={{ fontWeight: 600 }}>{d.client_name}</td>
                <td className="muted">{d.product}</td>
                <td className="amount">{fmtMoney(d.amount_claimed, d.currency)}</td>
                <td><Badge status={d.status} /></td>
                <td>{d.next_action_type ? (() => {
                  const ad = daysUntil(d.next_action_date);
                  const m = actionMeta(d.next_action_type);
                  return <span className={ad < 0 ? "overdue" : ad === 0 ? "duesoon" : "muted"} style={{ fontSize: 12 }}>
                    {m.icon} {m.label} · {ad < 0 ? `+${-ad} j` : ad === 0 ? "auj." : "J-" + ad}
                  </span>;
                })() : <span className="faint">—</span>}</td>
                <td className={isOverdue(d) ? "overdue" : days != null && days <= 7 && !CLOSED.includes(d.status) ? "duesoon" : "muted"}>
                  {fmtDate(d.due_date)}{isOverdue(d) && " ⚠"}
                </td>
                <td className="faint">{ago(d.updated_at)}</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

function KanbanView({ rows, onSelect }) {
  return (
    <div className="kanban">
      {STATUSES.map((s) => {
        const items = rows.filter((d) => d.status === s.key);
        return (
          <div className="kcol" key={s.key}>
            <h3><span className="sw" style={{ width: 8, height: 8, borderRadius: 4, background: ST_COLOR[s.key], display: "inline-block" }} />{s.label}<span className="count">{items.length}</span></h3>
            {items.map((d) => (
              <div className="kcard" key={d.id} onClick={() => onSelect(d.id)}>
                <div className="khead">
                  <SiteLogo name={d.site_name} logo={d.site_logo} size={24} />
                  <span className="ref">{d.reference}</span>
                </div>
                <div className="cli">{d.client_name}</div>
                <div className="prod">{d.product}</div>
                <div className="amt">{fmtMoney(d.amount_claimed, d.currency)}
                  {isOverdue(d) && <span className="overdue" style={{ fontSize: 11, marginLeft: 7 }}>⚠ retard</span>}
                  {d.next_action_type && daysUntil(d.next_action_date) <= 0 &&
                    <span style={{ fontSize: 11, marginLeft: 7, color: "var(--gold)" }}>{actionMeta(d.next_action_type).icon} à faire</span>}</div>
              </div>
            ))}
          </div>
        );
      })}
    </div>
  );
}

/* ------------------------------------------------------- Dossier form ---- */
const EMPTY = { site_id: "", client_name: "", product: "", order_ref: "", serial_number: "", dispute_reason: "", carrier: "", carrier_code: "", tracking_number: "", proxy_server: "", proxy_username: "", proxy_password: "", amount_claimed: "", currency: "EUR", status: "ouvert", due_date: "" };

// Applique les champs OCR sans écraser ce que l'utilisateur a déjà saisi.
function applyOcrFields(prev, f) {
  const next = { ...prev };
  if (f.order_ref && !String(prev.order_ref || "").trim()) next.order_ref = f.order_ref;
  if (f.serial_number && !String(prev.serial_number || "").trim()) next.serial_number = f.serial_number;
  if (f.amount_claimed && !Number(prev.amount_claimed)) next.amount_claimed = f.amount_claimed;
  return next;
}

function DossierForm({ sites, onClose, onSaved, initial }) {
  const [d, setD] = useState(initial || EMPTY);
  const [error, setError] = useState("");
  const set = (k, v) => setD({ ...d, [k]: v });

  const save = async () => {
    if (!d.client_name.trim() || !d.product.trim()) { setError("Client et produit sont obligatoires."); return; }
    try {
      await api.post("/api/dossiers", {
        ...d,
        site_id: d.site_id ? Number(d.site_id) : null,
        amount_claimed: Number(d.amount_claimed) || 0,
        due_date: d.due_date || null,
      });
      onSaved();
    } catch (e) { setError(e.message); }
  };

  return (
    <Overlay center onClose={onClose}>
      <button className="btn ghost sm close" onClick={onClose}>{Ico.x}</button>
      <h2>{initial ? "Dupliquer le dossier" : "Nouveau dossier"}</h2>
      <div style={{ height: 14 }} />
      <FormFields d={d} set={set} sites={sites} onApplyOcr={(f) => setD((prev) => applyOcrFields(prev, f))} />
      {error && <div className="err">{error}</div>}
      <div className="toolbar"><button className="btn" onClick={save}>Enregistrer</button></div>
    </Overlay>
  );
}

function OcrImport({ onApply }) {
  const [busy, setBusy] = useState(false);
  const [info, setInfo] = useState(null);
  const run = async (file) => {
    setBusy(true); setInfo(null);
    try {
      const fd = new FormData(); fd.append("file", file);
      const r = await fetch("/api/tools/ocr", { method: "POST", body: fd });
      const j = await r.json();
      if (!r.ok) throw new Error(j.detail || "Analyse impossible");
      const f = j.fields;
      const applied = onApply(f);
      const found = [
        f.amount_claimed && `montant ${f.amount_claimed}`,
        f.order_ref && `commande ${f.order_ref}`,
        f.serial_number && `n° série ${f.serial_number}`,
      ].filter(Boolean);
      setInfo(found.length ? `Détecté (${j.method}) : ${found.join(", ")}` : `Rien trouvé (${j.method}) — remplis à la main.`);
      if (found.length) toast("Champs pré-remplis depuis le document");
    } catch (e) { setInfo("⚠ " + e.message); }
    setBusy(false);
  };
  return (
    <div className="dropzone" style={{ padding: 14, marginBottom: 14 }}>
      <label className="btn ghost sm" style={{ display: "inline-block" }}>
        {busy ? "Analyse en cours…" : "📄 Remplir depuis une facture / photo (OCR)"}
        <input type="file" accept=".pdf,.png,.jpg,.jpeg,.webp,.tif,.tiff,.bmp" style={{ display: "none" }}
          disabled={busy} onChange={(e) => e.target.files[0] && run(e.target.files[0])} />
      </label>
      {info && <div className="muted" style={{ fontSize: 12, marginTop: 8 }}>{info}</div>}
    </div>
  );
}

function FormFields({ d, set, sites, onApplyOcr }) {
  return (
    <div>
      {onApplyOcr && <OcrImport onApply={onApplyOcr} />}
      <div className="row2">
        <div className="field"><label>Site / marque</label>
          <select value={d.site_id || ""} onChange={(e) => set("site_id", e.target.value)}>
            <option value="">— aucun —</option>
            {sites.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
          </select></div>
        <div className="field"><label className="req">Client</label>
          <input value={d.client_name} onChange={(e) => set("client_name", e.target.value)} /></div>
      </div>
      <div className="row2">
        <div className="field"><label className="req">Produit</label>
          <input value={d.product} onChange={(e) => set("product", e.target.value)} /></div>
        <div className="field"><label>Référence commande</label>
          <input value={d.order_ref || ""} onChange={(e) => set("order_ref", e.target.value)} /></div>
      </div>
      <div className="row3">
        <div className="field"><label>Numéro de série</label>
          <input value={d.serial_number || ""} onChange={(e) => set("serial_number", e.target.value)} /></div>
        <div className="field"><label>Transporteur</label>
          <input value={d.carrier || ""} onChange={(e) => set("carrier", e.target.value)} /></div>
        <div className="field"><label>N° dossier fabricant</label>
          <input value={d.manufacturer_ref || ""} onChange={(e) => set("manufacturer_ref", e.target.value)} /></div>
      </div>
      <div className="field"><label>Motif du litige</label>
        <textarea rows="2" value={d.dispute_reason || ""} onChange={(e) => set("dispute_reason", e.target.value)} /></div>
      <div className="row3">
        <div className="field"><label>Montant réclamé</label>
          <input type="number" step="0.01" value={d.amount_claimed} onChange={(e) => set("amount_claimed", e.target.value)} /></div>
        <div className="field"><label>Statut</label>
          <select value={d.status} onChange={(e) => set("status", e.target.value)}>
            {STATUSES.map((s) => <option key={s.key} value={s.key}>{s.label}</option>)}
          </select></div>
        <div className="field"><label>Date d'échéance</label>
          <input type="date" value={d.due_date || ""} onChange={(e) => set("due_date", e.target.value)} /></div>
      </div>

      <div className="section-title">Proxy du dossier <span style={{ color: "var(--faint)", fontWeight: 400, textTransform: "none", letterSpacing: 0 }}>— assigné à cette marque jusqu'à la clôture</span></div>
      <div className="row3">
        <div className="field"><label>Proxy (host:port)</label>
          <input value={d.proxy_server || ""} placeholder="http://host:port ou socks5://host:port"
            onChange={(e) => set("proxy_server", e.target.value)} /></div>
        <div className="field"><label>Identifiant proxy</label>
          <input value={d.proxy_username || ""} placeholder="optionnel"
            onChange={(e) => set("proxy_username", e.target.value)} /></div>
        <div className="field"><label>Mot de passe proxy</label>
          <input type="password" value={d.proxy_password || ""} placeholder="optionnel"
            onChange={(e) => set("proxy_password", e.target.value)} /></div>
      </div>
    </div>
  );
}

/* ----------------------------------------------------- Dossier drawer ---- */
function DossierDrawer({ id, sites, onClose, onChanged }) {
  const [d, setD] = useState(null);
  const [edit, setEdit] = useState(null);
  const [dup, setDup] = useState(false);
  const [note, setNote] = useState("");
  const [error, setError] = useState("");
  const [actionForm, setActionForm] = useState(null); // {action_type, action_date, note}
  const [copilot, setCopilot] = useState(false);
  const [carriersList, setCarriersList] = useState([]);
  const [track, setTrack] = useState(null); // {tracking_number, carrier_code}
  const [launching, setLaunching] = useState(false);
  const [lettersOpen, setLettersOpen] = useState(false);
  const [docCat, setDocCat] = useState("facture");
  const [browserStatus, setBrowserStatus] = useState(null);

  const load = useCallback(async () => setD(await api.get("/api/dossiers/" + id)), [id]);
  useEffect(() => { load(); }, [load]);
  useEffect(() => { api.get("/api/tools/carriers").then(setCarriersList); }, []);
  useEffect(() => { api.get("/api/dossiers/browser/status").then(setBrowserStatus); }, []);

  if (!d) return <Overlay onClose={onClose}><div className="empty">Chargement…</div></Overlay>;

  const saveEdit = async () => {
    try {
      await api.patch("/api/dossiers/" + id, {
        ...edit,
        site_id: edit.site_id ? Number(edit.site_id) : null,
        amount_claimed: Number(edit.amount_claimed) || 0,
        due_date: edit.due_date || null,
      });
      setEdit(null); await load(); onChanged(); toast("Dossier mis à jour");
    } catch (e) { setError(e.message); }
  };
  const quickStatus = async (status) => {
    await api.patch("/api/dossiers/" + id, { status });
    await load(); onChanged(); toast("Statut : " + statusLabel(status));
  };
  const addNote = async () => {
    if (!note.trim()) return;
    await api.post(`/api/dossiers/${id}/notes`, { content: note });
    setNote(""); await load(); onChanged();
  };
  const del = async () => {
    if (!confirm("Supprimer définitivement ce dossier ? (préfère l'archivage pour garder les stats)")) return;
    await api.del("/api/dossiers/" + id); onClose(); onChanged(); toast("Dossier supprimé");
  };
  const archive = async () => { await api.post(`/api/dossiers/${id}/archive`, {}); await load(); onChanged(); toast("Dossier archivé"); };
  const unarchive = async () => { await api.post(`/api/dossiers/${id}/unarchive`, {}); await load(); onChanged(); toast("Dossier désarchivé"); };
  const upload = async (file, category) => {
    const fd = new FormData(); fd.append("file", file); fd.append("category", category || "autre");
    await fetch(`/api/dossiers/${id}/attachments`, { method: "POST", body: fd });
    await load(); onChanged(); toast("Document ajouté");
  };
  const deleteAttachment = async (attId) => {
    if (!confirm("Supprimer ce document ?")) return;
    await api.del("/api/dossiers/attachments/" + attId);
    await load(); onChanged();
  };
  const copySummary = async () => {
    const lines = [
      `Dossier ${d.reference} — ${d.site_name || "sans site"}`,
      `Client : ${d.client_name}`,
      `Produit : ${d.product}`,
      d.order_ref && `Réf. commande : ${d.order_ref}`,
      d.serial_number && `N° de série : ${d.serial_number}`,
      d.manufacturer_ref && `N° dossier fabricant : ${d.manufacturer_ref}`,
      d.dispute_reason && `Motif : ${d.dispute_reason}`,
      d.carrier && `Transporteur : ${d.carrier}`,
      `Montant réclamé : ${fmtMoney(d.amount_claimed, d.currency)}`,
      `Statut : ${statusLabel(d.status)}`,
      d.due_date && `Échéance : ${fmtDate(d.due_date)}`,
    ].filter(Boolean).join("\n");
    await navigator.clipboard.writeText(lines);
    toast("Résumé copié dans le presse-papier");
  };

  const saveAction = async () => {
    try {
      await api.post(`/api/dossiers/${id}/next-action`, {
        action_type: actionForm.action_type,
        action_date: actionForm.action_date,
        note: actionForm.note || null,
      });
      setActionForm(null); await load(); onChanged(); toast("Action planifiée");
    } catch (e) { toast(e.message, true); }
  };
  const doneAction = async () => {
    try {
      await api.post(`/api/dossiers/${id}/action-done`, {});
      await load(); onChanged(); toast("Action faite ✓");
    } catch (e) { toast(e.message, true); }
  };

  const trackUrl = (code, num) => {
    if (!num) return null;
    const c = carriersList.find((x) => x.key === code) || carriersList.find((x) => x.key === "universel");
    return c ? c.url.replace("{num}", num.trim().replace(/\s/g, "")) : null;
  };
  const detectCarrier = async (num) => {
    if (!num || num.length < 6) return;
    try { const r = await api.get("/api/tools/carriers/detect?number=" + encodeURIComponent(num)); setTrack((t) => ({ ...t, carrier_code: r.detected })); } catch {}
  };
  const saveTracking = async () => {
    try {
      await api.patch("/api/dossiers/" + id, { tracking_number: track.tracking_number || null, carrier_code: track.carrier_code || null });
      setTrack(null); await load(); onChanged(); toast("Suivi colis enregistré");
    } catch (e) { toast(e.message, true); }
  };
  const launchBrowser = async () => {
    setLaunching(true);
    try {
      const r = await api.post(`/api/dossiers/${id}/launch-browser`, {});
      toast(r.proxy ? `Navigateur ouvert via ${r.proxy}` : "Navigateur ouvert (sans proxy)");
      await load();
    } catch (e) { toast(e.message, true); }
    setLaunching(false);
  };

  const site = sites.find((s) => s.id === d.site_id);
  const actionDays = d.next_action_date ? daysUntil(d.next_action_date) : null;
  const carrierName = (code) => (carriersList.find((c) => c.key === code) || {}).name || code;

  return (
    <Overlay onClose={onClose}>
      <button className="btn ghost sm close" onClick={onClose}>{Ico.x}</button>
      <h2 className="serif"><SiteLogo name={d.site_name} logo={d.site_logo} size={38} />{d.client_name} <Badge status={d.status} />
        {d.archived_at && <span className="pill" style={{ background: "var(--glass-2)", color: "var(--faint)" }}>🗄 archivé</span>}</h2>
      <div className="muted" style={{ marginLeft: 50 }}>{d.reference} · {d.site_name || "sans site"} · {d.product}</div>

      {!edit ? (
        <div>
          <div className="toolbar" style={{ marginTop: 18 }}>
            {site && !CLOSED.includes(d.status) && !d.archived_at &&
              <button className="btn gold sm" onClick={() => setCopilot(true)}>🚀 Soumettre chez {site.name}</button>}
            <button className="btn sm" onClick={() => setEdit({ ...d, due_date: d.due_date || "" })}>{Ico.edit} Modifier</button>
            <button className="btn ghost sm" onClick={() => setLettersOpen(true)}>{Ico.doc} Courriers</button>
            <button className="btn ghost sm" onClick={copySummary}>{Ico.copy} Copier le résumé</button>
            <button className="btn ghost sm" onClick={() => setDup(true)}>⧉ Dupliquer</button>
            <select defaultValue="" onChange={(e) => e.target.value && quickStatus(e.target.value)}
              style={{ background: "var(--glass)", color: "var(--text)", border: "1px solid var(--stroke)", borderRadius: 9, padding: "7px 10px", fontFamily: "inherit", fontSize: 12 }}>
              <option value="">Changer statut…</option>
              {STATUSES.filter((s) => s.key !== d.status).map((s) => <option key={s.key} value={s.key}>{s.label}</option>)}
            </select>
            {d.archived_at
              ? <button className="btn ghost sm" style={{ marginLeft: "auto" }} onClick={unarchive}>↩ Désarchiver</button>
              : <button className="btn ghost sm" style={{ marginLeft: "auto" }} onClick={archive}>🗄 Archiver</button>}
            <button className="btn danger sm" onClick={del}>Supprimer</button>
          </div>

          <div className="row3">
            <Info k="Montant réclamé" v={fmtMoney(d.amount_claimed, d.currency)} gold />
            <Info k="Échéance" v={fmtDate(d.due_date)} cls={isOverdue(d) ? "overdue" : ""} />
            <Info k="Transporteur" v={d.carrier || "—"} />
          </div>
          <div className="row3">
            <Info k="Réf. commande" v={d.order_ref || "—"} />
            <Info k="N° série" v={d.serial_number || "—"} />
            <Info k="N° dossier fabricant" v={d.manufacturer_ref || "—"} gold={!!d.manufacturer_ref} />
          </div>
          {d.dispute_reason && <div className="field"><label>Motif du litige</label><div>{d.dispute_reason}</div></div>}

          {site && (
            <div className="alert-item" style={{ cursor: "default" }}>
              <SiteLogo name={site.name} logo={site.logo} size={24} />
              <span className="atxt">Parcours <b>{site.flow_type}</b>{site.category ? ` · ${site.category}` : ""} · délai cible <b>{site.sla_days ?? 7} j</b></span>
              {site.support_url && <a href={site.support_url} target="_blank" rel="noreferrer">Ouvrir le support ↗</a>}
            </div>
          )}

          {site && !CLOSED.includes(d.status) && !d.archived_at && browserStatus?.reason === "hosted" && (
            <div className="alert-item" style={{ cursor: "default" }}>
              <span className="adot" style={{ background: "var(--faint)" }} />
              <span className="atxt faint">{Ico.globe} Navigateur + proxy — disponible uniquement sur l'app installée en local.</span>
            </div>
          )}
          {site && !CLOSED.includes(d.status) && !d.archived_at && browserStatus && browserStatus.reason !== "hosted" && (
            <div className="alert-item" style={{ cursor: "default", borderColor: d.proxy_server ? "rgba(232,194,104,.35)" : "var(--stroke)" }}>
              <span className="adot" style={{ background: d.proxy_server ? "var(--gold)" : "var(--faint)" }} />
              <span className="atxt">
                {d.proxy_server
                  ? <>Proxy assigné : <b style={{ color: "var(--gold)" }}>{d.proxy_server}</b>{d.proxy_username ? ` (${d.proxy_username})` : ""}</>
                  : <span className="faint">Aucun proxy — ajoute-le via « Modifier ».</span>}
              </span>
              <button className="btn gold sm" onClick={launchBrowser} disabled={launching}>
                {launching ? "Ouverture…" : (<>{Ico.globe} Ouvrir le navigateur</>)}
              </button>
            </div>
          )}

          <div className="section-title">Prochaine action</div>
          {!actionForm && d.next_action_type && (
            <div className="alert-item" style={{ cursor: "default" }}>
              <span className="adot" style={{ background: actionDays < 0 ? ST_COLOR.refuse : actionDays === 0 ? ST_COLOR.rembourse : "var(--accent)" }} />
              <span className="atxt">
                <b>{actionMeta(d.next_action_type).icon} {actionMeta(d.next_action_type).label}</b> le {fmtDate(d.next_action_date)}
                {actionDays < 0 && <span className="overdue"> — {-actionDays} j de retard</span>}
                {actionDays === 0 && <span style={{ color: ST_COLOR.rembourse }}> — aujourd'hui</span>}
                {d.next_action_note && <span className="faint"> · {d.next_action_note}</span>}
              </span>
              <button className="btn sm" onClick={doneAction}>{Ico.check} Fait</button>
              <button className="btn ghost sm" onClick={() => setActionForm({ action_type: d.next_action_type, action_date: d.next_action_date, note: d.next_action_note || "" })}>{Ico.edit}</button>
            </div>
          )}
          {!actionForm && !d.next_action_type && (
            <div className="toolbar" style={{ margin: "4px 0" }}>
              <span className="faint">Aucune action planifiée.</span>
              <button className="btn ghost sm" onClick={() => setActionForm({ action_type: "relancer", action_date: todayISO(), note: "" })}>+ Planifier une action</button>
              {site && <button className="btn gold sm" onClick={() => setActionForm({ action_type: "relancer", action_date: plusDaysISO(site.sla_days ?? 7), note: "" })}>📣 Relance à J+{site.sla_days ?? 7} (SLA)</button>}
            </div>
          )}
          {actionForm && (
            <div>
              <div className="row3">
                <div className="field"><label>Action</label>
                  <select value={actionForm.action_type} onChange={(e) => setActionForm({ ...actionForm, action_type: e.target.value })}>
                    {ACTION_TYPES.map((a) => <option key={a.key} value={a.key}>{a.icon} {a.label}</option>)}
                  </select></div>
                <div className="field"><label>Date</label>
                  <input type="date" value={actionForm.action_date} onChange={(e) => setActionForm({ ...actionForm, action_date: e.target.value })} /></div>
                <div className="field"><label>Note</label>
                  <input value={actionForm.note} placeholder="optionnel" onChange={(e) => setActionForm({ ...actionForm, note: e.target.value })} /></div>
              </div>
              <div className="toolbar" style={{ marginTop: 2 }}>
                <button className="btn sm" onClick={saveAction}>Planifier</button>
                <button className="btn ghost sm" onClick={() => setActionForm(null)}>Annuler</button>
              </div>
            </div>
          )}

          <div className="section-title">Suivi colis</div>
          {!track && d.tracking_number && (
            <div className="alert-item" style={{ cursor: "default" }}>
              <span className="adot" style={{ background: "var(--accent)" }} />
              <span className="atxt"><b>{carrierName(d.carrier_code) || "Transporteur"}</b> · {d.tracking_number}</span>
              <a className="btn sm" href={trackUrl(d.carrier_code, d.tracking_number)} target="_blank" rel="noreferrer">Suivre ↗</a>
              <button className="btn ghost sm" onClick={() => setTrack({ tracking_number: d.tracking_number, carrier_code: d.carrier_code || "" })}>{Ico.edit}</button>
            </div>
          )}
          {!track && !d.tracking_number && (
            <button className="btn ghost sm" onClick={() => setTrack({ tracking_number: "", carrier_code: "" })}>+ Ajouter un n° de suivi</button>
          )}
          {track && (
            <div>
              <div className="row2">
                <div className="field"><label>N° de suivi</label>
                  <input value={track.tracking_number} autoFocus
                    onChange={(e) => setTrack({ ...track, tracking_number: e.target.value })}
                    onBlur={(e) => detectCarrier(e.target.value)} placeholder="colle le n° de suivi" /></div>
                <div className="field"><label>Transporteur (auto-détecté)</label>
                  <select value={track.carrier_code} onChange={(e) => setTrack({ ...track, carrier_code: e.target.value })}>
                    <option value="">— choisir —</option>
                    {carriersList.map((c) => <option key={c.key} value={c.key}>{c.name}</option>)}
                  </select></div>
              </div>
              <div className="toolbar" style={{ marginTop: 2 }}>
                <button className="btn sm" onClick={saveTracking}>Enregistrer</button>
                {track.tracking_number && trackUrl(track.carrier_code, track.tracking_number) &&
                  <a className="btn ghost sm" href={trackUrl(track.carrier_code, track.tracking_number)} target="_blank" rel="noreferrer">Tester le lien ↗</a>}
                <button className="btn ghost sm" onClick={() => setTrack(null)}>Annuler</button>
              </div>
            </div>
          )}

          <div className="section-title">Documents du dossier</div>
          {d.serial_number &&
            <div className="faint" style={{ fontSize: 12, marginBottom: 8 }}>🔢 N° de série (saisi) : <b style={{ color: "var(--text)" }}>{d.serial_number}</b> — inclus automatiquement dans les courriers.</div>}
          {d.attachments.length === 0 && <div className="faint">Aucun document. Ajoute la facture, des photos, l'étiquette…</div>}
          {DOC_CATS.map((cat) => {
            const items = d.attachments.filter((a) => (a.category || "autre") === cat.key);
            if (items.length === 0) return null;
            return (
              <div key={cat.key} style={{ marginBottom: 8 }}>
                <div className="faint" style={{ fontSize: 11, textTransform: "uppercase", letterSpacing: ".6px", marginBottom: 3 }}>{cat.icon} {cat.label} ({items.length})</div>
                {items.map((a) => (
                  <div key={a.id} className="alert-item" style={{ cursor: "default", padding: "7px 10px" }}>
                    <span className="atxt"><a href={"/api/dossiers/attachments/" + a.id}>{a.filename}</a> <span className="faint">({Math.round(a.size / 1024)} Ko)</span>
                      {a.source && a.source.startsWith("gmail:") && <span className="pill" style={{ background: "var(--gold-soft)", color: "var(--gold)", marginLeft: 6 }}>📧 reçu par email</span>}</span>
                    <button className="btn ghost sm" onClick={() => deleteAttachment(a.id)} title="Supprimer">{Ico.trash}</button>
                  </div>
                ))}
              </div>
            );
          })}
          <div className="toolbar" style={{ marginTop: 6 }}>
            <select value={docCat} onChange={(e) => setDocCat(e.target.value)}
              style={{ background: "var(--glass)", color: "var(--text)", border: "1px solid var(--stroke)", borderRadius: 9, padding: "8px 10px", fontFamily: "inherit", fontSize: 12.5 }}>
              {DOC_CATS.map((c) => <option key={c.key} value={c.key}>{c.icon} {c.label}</option>)}
            </select>
            <label className="btn ghost sm" style={{ display: "inline-block" }}>
              + Ajouter un document
              <input type="file" style={{ display: "none" }} onChange={(e) => e.target.files[0] && upload(e.target.files[0], docCat)} />
            </label>
          </div>

          {d.emails && d.emails.length > 0 && (
            <div>
              <div className="section-title">Emails reçus ({d.emails.length})</div>
              {d.emails.map((e) => (
                <div className="alert-item" key={e.id} style={{ cursor: "default", alignItems: "flex-start" }}>
                  <span className="adot" style={{ background: "var(--accent)", marginTop: 6 }} />
                  <span className="atxt" style={{ whiteSpace: "normal" }}>
                    <b>{e.subject || "(sans objet)"}</b> <span className="faint">— {e.sender}{e.received_at ? " · " + fmtDateTime(e.received_at) : ""}</span>
                    <div className="muted" style={{ fontSize: 12, marginTop: 3 }}>{e.snippet}</div>
                  </span>
                  <span className="pill" style={{ flexShrink: 0 }}>{e.matched_on}</span>
                </div>
              ))}
            </div>
          )}

          <div className="section-title">Historique des échanges</div>
          <div className="note-input">
            <input placeholder="Ajouter une note… (Entrée pour valider)" value={note} onChange={(e) => setNote(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && addNote()} />
            <button className="btn sm" onClick={addNote}>Ajouter</button>
          </div>
          <ul className="hist">
            {d.history.map((h) => (
              <li key={h.id}><div className="when">{fmtDateTime(h.created_at)} · {h.author}</div><div className="what">{h.content}</div></li>
            ))}
          </ul>
        </div>
      ) : (
        <div style={{ marginTop: 18 }}>
          <FormFields d={edit} set={(k, v) => setEdit({ ...edit, [k]: v })} sites={sites}
            onApplyOcr={(f) => setEdit((prev) => applyOcrFields(prev, f))} />
          {error && <div className="err">{error}</div>}
          <div className="toolbar">
            <button className="btn" onClick={saveEdit}>Enregistrer</button>
            <button className="btn ghost" onClick={() => { setEdit(null); setError(""); }}>Annuler</button>
          </div>
        </div>
      )}
      {copilot && site &&
        <SubmitCopilot dossier={d} site={site} onClose={() => setCopilot(false)}
          onDone={async () => { setCopilot(false); await load(); onChanged(); }} />}
      {lettersOpen &&
        <LetterModal dossier={d} site={site} onClose={() => setLettersOpen(false)}
          onSent={async () => { await load(); onChanged(); }} />}
      {dup && <DossierForm sites={sites}
        initial={{ ...EMPTY, site_id: d.site_id || "", client_name: d.client_name, product: d.product, order_ref: d.order_ref || "", serial_number: "", dispute_reason: d.dispute_reason || "", carrier: d.carrier || "", amount_claimed: d.amount_claimed, currency: d.currency, status: "ouvert", due_date: "" }}
        onClose={() => setDup(false)}
        onSaved={() => { setDup(false); onChanged(); toast("Dossier dupliqué"); }} />}
    </Overlay>
  );
}
function Info({ k, v, cls, gold }) {
  return <div className="field"><label>{k}</label><div className={cls} style={gold ? { color: "var(--gold)", fontWeight: 650 } : null}>{v}</div></div>;
}

/* -------------------------------------------------------- Bulk create ---- */
function emptyBulkRow() { return { site_id: "", client_name: "", product: "", order_ref: "", serial_number: "", amount_claimed: "", status: "ouvert", due_date: "" }; }

function BulkCreate({ sites, onDone }) {
  const [rows, setRows] = useState(Array.from({ length: 3 }, emptyBulkRow));
  const [error, setError] = useState("");
  const [importResult, setImportResult] = useState(null);

  const setCell = (i, k, v) => setRows(rows.map((r, idx) => (idx === i ? { ...r, [k]: v } : r)));
  const addRow = () => setRows([...rows, emptyBulkRow()]);
  const removeRow = (i) => setRows(rows.filter((_, idx) => idx !== i));

  const submit = async () => {
    setError("");
    const valid = rows.filter((r) => r.client_name.trim() && r.product.trim());
    if (valid.length === 0) { setError("Renseigne au moins une ligne avec client + produit."); return; }
    const invalid = rows.filter((r) => (r.client_name.trim() || r.product.trim()) && !(r.client_name.trim() && r.product.trim()));
    if (invalid.length) { setError(`${invalid.length} ligne(s) incomplète(s) : client ET produit obligatoires.`); return; }
    try {
      await api.post("/api/dossiers/bulk", {
        dossiers: valid.map((r) => ({
          site_id: r.site_id ? Number(r.site_id) : null,
          client_name: r.client_name, product: r.product,
          order_ref: r.order_ref || null, serial_number: r.serial_number || null,
          amount_claimed: Number(r.amount_claimed) || 0,
          status: r.status, due_date: r.due_date || null,
        })),
      });
      onDone();
    } catch (e) { setError(e.message); }
  };

  const importCsv = async (file) => {
    setImportResult(null); setError("");
    const fd = new FormData(); fd.append("file", file);
    try {
      const r = await fetch("/api/dossiers/csv/import", { method: "POST", body: fd });
      const j = await r.json();
      if (!r.ok) throw new Error(j.detail || "Import échoué");
      setImportResult(j);
      if (j.created > 0 && (!j.errors || j.errors.length === 0)) setTimeout(onDone, 1300);
    } catch (e) { setError(e.message); }
  };

  return (
    <div>
      <div className="panel" style={{ marginBottom: 14 }}>
        <h3>Saisie manuelle <span className="hint">plusieurs dossiers d'un coup</span></h3>
        <div style={{ overflowX: "auto" }}>
          <table className="bulk-table">
            <thead><tr>
              <th>Site</th><th className="req">Client</th><th className="req">Produit</th><th>Réf. cmd</th>
              <th>N° série</th><th>Montant</th><th>Statut</th><th>Échéance</th><th></th>
            </tr></thead>
            <tbody>
              {rows.map((r, i) => (
                <tr key={i}>
                  <td><select value={r.site_id} onChange={(e) => setCell(i, "site_id", e.target.value)}>
                    <option value="">—</option>
                    {sites.map((s) => <option key={s.id} value={s.id}>{s.name}</option>)}
                  </select></td>
                  <td><input value={r.client_name} onChange={(e) => setCell(i, "client_name", e.target.value)} /></td>
                  <td><input value={r.product} onChange={(e) => setCell(i, "product", e.target.value)} /></td>
                  <td><input value={r.order_ref} onChange={(e) => setCell(i, "order_ref", e.target.value)} /></td>
                  <td><input value={r.serial_number} onChange={(e) => setCell(i, "serial_number", e.target.value)} /></td>
                  <td><input type="number" step="0.01" value={r.amount_claimed} onChange={(e) => setCell(i, "amount_claimed", e.target.value)} /></td>
                  <td><select value={r.status} onChange={(e) => setCell(i, "status", e.target.value)}>
                    {STATUSES.map((s) => <option key={s.key} value={s.key}>{s.label}</option>)}
                  </select></td>
                  <td><input type="date" value={r.due_date} onChange={(e) => setCell(i, "due_date", e.target.value)} /></td>
                  <td><button className="btn ghost sm" onClick={() => removeRow(i)} title="Supprimer la ligne">{Ico.x}</button></td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
        <div className="toolbar">
          <button className="btn ghost" onClick={addRow}>+ Ligne</button>
          <button className="btn" onClick={submit}>Créer les dossiers</button>
          {error && <span className="err">{error}</span>}
        </div>
      </div>

      <div className="panel">
        <h3>Import CSV</h3>
        <div className="dropzone">
          <p style={{ margin: "0 0 12px" }}>Colonnes attendues (séparateur <b>;</b> ou <b>,</b>) :<br />
            <code style={{ fontSize: 11.5 }}>site;client_name;product;order_ref;serial_number;dispute_reason;carrier;amount_claimed;currency;status;due_date</code></p>
          <label className="btn" style={{ display: "inline-block" }}>
            Choisir un fichier CSV
            <input type="file" accept=".csv,text/csv" style={{ display: "none" }} onChange={(e) => e.target.files[0] && importCsv(e.target.files[0])} />
          </label>
          {" "}
          <a className="linkish" href="/api/dossiers/csv/template" download="modele_rma.csv">Télécharger le modèle</a>
        </div>
        {importResult && (
          <div style={{ marginTop: 12 }}>
            <div className="ok-msg">{importResult.created} dossier(s) importé(s).</div>
            {importResult.errors && importResult.errors.length > 0 &&
              <ul className="err">{importResult.errors.map((e, i) => <li key={i}>{e}</li>)}</ul>}
          </div>
        )}
      </div>
    </div>
  );
}

/* -------------------------------------------------------------- Gmail ---- */
function GmailView({ onChange, onOpen }) {
  const [st, setSt] = useState(null);
  const [matches, setMatches] = useState([]);
  const [scanning, setScanning] = useState(false);
  const [scanResult, setScanResult] = useState(null);
  const [error, setError] = useState("");
  const [setupOpen, setSetupOpen] = useState(false);

  const load = useCallback(async () => {
    const s = await api.get("/api/gmail/status");
    setSt(s);
    setMatches(await api.get("/api/gmail/matches?limit=25"));
    setSetupOpen((prev) => prev || !s.accounts || s.accounts.length === 0);
  }, []);
  useEffect(() => { load(); }, [load]);

  const uploadCreds = async (file) => {
    setError("");
    const fd = new FormData(); fd.append("file", file);
    const r = await fetch("/api/gmail/credentials", { method: "POST", body: fd });
    if (!r.ok) { const j = await r.json().catch(() => ({})); setError(j.detail || "Fichier invalide"); return; }
    toast("Credentials enregistrés"); await load();
  };
  const authorize = async () => {
    setError("");
    try {
      const { url } = await api.get("/api/gmail/auth-url");
      window.location.href = url;   // Google → consentement → retour automatique ici
    } catch (e) { setError(e.message); }
  };
  const scan = async () => {
    setScanning(true); setError(""); setScanResult(null);
    try {
      const r = await api.post("/api/gmail/scan?days=14", {});
      setScanResult(r);
      if (r.matched > 0) { toast(`${r.matched} email(s) rattaché(s) à des dossiers`); onChange(); }
      await load();
    } catch (e) { setError(e.message); }
    setScanning(false);
  };
  const disconnect = async (email) => {
    if (!confirm(`Déconnecter la boîte ${email} ?`)) return;
    await api.del("/api/gmail/accounts/" + encodeURIComponent(email));
    toast("Compte déconnecté"); await load();
  };

  if (!st) return <div className="empty">Chargement…</div>;

  const hasAny = st.accounts && st.accounts.length > 0;
  const canScan = st.accounts && st.accounts.some((a) => a.valid);
  const addAccount = () => { if (st.has_credentials) authorize(); else setSetupOpen(true); };

  return (
    <div>
      {/* --- Boîtes connectées : l'essentiel, en premier --- */}
      <div className="panel" style={{ marginBottom: 13 }}>
        <h3>Boîtes connectées {hasAny && <span className="hint">{st.accounts.length} boîte{st.accounts.length > 1 ? "s" : ""} · lecture seule sauf envoi autorisé</span>}</h3>

        {!hasAny && (
          <div className="empty" style={{ padding: "26px 10px" }}>
            <div className="big">📭</div>
            Aucune boîte Gmail connectée pour l'instant.
            <div style={{ marginTop: 12 }}>
              <button className="btn" onClick={addAccount}>+ Connecter une boîte Gmail</button>
            </div>
          </div>
        )}

        {hasAny && st.accounts.map((a) => (
          <div className="alert-item" key={a.email} style={{ cursor: "default" }}>
            <span className="adot" style={{ background: !a.valid ? "var(--st-refuse)" : a.can_send ? "var(--st-rembourse)" : "var(--st-en_attente)" }} />
            <span className="atxt"><b style={{ color: a.valid ? "var(--gold)" : "inherit" }}>{a.email}</b>
              {!a.valid && <span className="err" style={{ fontSize: 12 }}> — expiré, reconnecte</span>}
              {a.valid && !a.can_send && <span className="duesoon" style={{ fontSize: 12 }}> — lecture seule (envoi non autorisé)</span>}
              {a.valid && a.can_send && <span className="faint" style={{ fontSize: 12 }}> — lecture + envoi ✓</span>}</span>
            {!a.valid && <button className="btn sm" onClick={authorize}>Reconnecter</button>}
            {a.valid && !a.can_send && <button className="btn gold sm" onClick={authorize}>{Ico.lockOpen} Autoriser l'envoi</button>}
            <button className="btn ghost sm" onClick={() => disconnect(a.email)}>Déconnecter</button>
          </div>
        ))}

        {hasAny && (
          <div className="toolbar">
            <button className="btn ghost sm" onClick={addAccount}>+ Ajouter une autre boîte</button>
          </div>
        )}
        {error && <div className="err">{error}</div>}
      </div>

      {/* --- Scan & synchronisation : ses propres boutons, séparés --- */}
      {hasAny && (
        <div className="panel" style={{ marginBottom: 13 }}>
          <h3>Scan & synchronisation <span className="hint">auto toutes les 15 min</span></h3>
          <div className="toolbar" style={{ marginTop: 0 }}>
            <button className="btn gold" onClick={scan} disabled={scanning || !canScan}>{scanning ? "Scan en cours…" : (<>{Icon.search} Scanner toutes les boîtes</>)}</button>
          </div>
          {st.last_scan &&
            <p className="muted" style={{ fontSize: 12, margin: "10px 0 0" }}>Dernier scan : {new Date(st.last_scan).toLocaleString("fr-FR")} — {st.last_scanned} analysé(s), {st.last_matched} rattaché(s).</p>}
          {scanResult && (
            <div className="ok-msg" style={{ marginTop: 8 }}>
              {scanResult.scanned} email(s) analysé(s), {scanResult.matched} rattaché(s).
              {scanResult.new_matches.map((m, i) => <div key={i} className="muted" style={{ fontSize: 12 }}>→ {m.dossier} · « {m.subject} » ({m.matched_on}{m.account ? ` · ${m.account}` : ""})</div>)}
            </div>
          )}
        </div>
      )}

      {/* --- Ajouter une boîte / configuration : replié une fois connecté --- */}
      <div className="panel" style={{ marginBottom: 13 }}>
        <div className="toolbar" style={{ marginTop: 0, cursor: "pointer" }} onClick={() => setSetupOpen((o) => !o)}>
          <h3 style={{ margin: 0 }}>{st.has_credentials ? "Ajouter / configurer une boîte" : "Connecter Gmail — configuration (une fois)"}</h3>
          <button className="btn ghost sm" style={{ marginLeft: "auto" }}>{setupOpen ? "Réduire ▲" : "Déplier ▼"}</button>
        </div>

        {setupOpen && (
          <div style={{ marginTop: 14 }}>
            {!st.has_credentials && (
              <div>
                <p className="muted" style={{ marginTop: 0 }}>Une seule fois, ~10 minutes : crée tes identifiants Google (gratuit) puis dépose le fichier ici.</p>
                <ol style={{ lineHeight: 2, margin: "0 0 14px", paddingLeft: 20 }}>
                  <li>Ouvre <a href="https://console.cloud.google.com/projectcreate" target="_blank" rel="noreferrer">console.cloud.google.com</a> → crée un projet (nom libre, ex. « RMA Dashboard »).</li>
                  <li>Menu <b>API et services → Bibliothèque</b> → cherche <b>Gmail API</b> → <b>Activer</b>.</li>
                  <li><b>Écran de consentement OAuth</b> → type <b>Externe</b> → renseigne le nom et ton email → ajoute ton adresse comme <b>utilisateur test</b>.</li>
                  <li><b>Identifiants → Créer des identifiants → ID client OAuth</b> → type <b>Application Web</b>.</li>
                  <li><b>Important</b> — dans <b>« URI de redirection autorisés »</b>, ajoute exactement :<br />
                    <code style={{ fontSize: 11.5 }}>{window.location.origin}/api/gmail/oauth2callback</code></li>
                  <li><b>Télécharger le JSON</b>, puis dépose-le ci-dessous :</li>
                </ol>
                <label className="btn" style={{ display: "inline-block" }}>
                  {Ico.doc} Déposer credentials.json
                  <input type="file" accept=".json,application/json" style={{ display: "none" }}
                    onChange={(e) => e.target.files[0] && uploadCreds(e.target.files[0])} />
                </label>
              </div>
            )}

            {st.has_credentials && (
              <div>
                <p className="muted" style={{ marginTop: 0 }}>Credentials en place. Clique pour connecter une boîte supplémentaire (même procédure Google à chaque fois).</p>
                <div className="toolbar" style={{ marginTop: 0 }}>
                  <button className="btn" onClick={authorize}>+ Ajouter une boîte Gmail</button>
                  <label className="btn ghost sm" style={{ display: "inline-block" }}>
                    Remplacer credentials.json
                    <input type="file" accept=".json" style={{ display: "none" }}
                      onChange={(e) => e.target.files[0] && uploadCreds(e.target.files[0])} />
                  </label>
                </div>
              </div>
            )}

            <div className="alert-item" style={{ cursor: "default", borderColor: "rgba(245,166,35,.28)", marginTop: 12 }}>
              <span className="adot" style={{ background: "var(--st-en_attente)" }} />
              <span className="atxt" style={{ whiteSpace: "normal", fontSize: 12 }}>
                À la connexion : écran Google → <b>Paramètres avancés → Accéder quand même</b>. En mode « Test », l'accès expire tous les 7 jours (reclique « Reconnecter »).
              </span>
            </div>
          </div>
        )}
      </div>

      {/* --- Emails rattachés --- */}
      <div className="panel">
        <h3>Derniers emails rattachés</h3>
        {matches.length === 0 && <div className="faint">Aucun pour l'instant. Lance un scan une fois connecté.</div>}
        {matches.map((m) => (
          <div className="alert-item" key={m.id} onClick={() => onOpen(m.dossier_id)}>
            <span className="adot" style={{ background: "var(--accent)" }} />
            <span className="atxt">
              <b>{m.dossier_ref}</b> · {m.client} — « {m.subject} » <span className="faint">de {m.sender}{m.account ? ` → ${m.account}` : ""}</span>
            </span>
            <span className="pill">{m.matched_on}</span>
            <span className="awhen faint">{m.received_at ? ago(m.received_at) : ""}</span>
          </div>
        ))}
      </div>
    </div>
  );
}

/* ------------------------------------------------------- Sites config ---- */
function SitesConfig({ sites, onChange, user }) {
  const isAdmin = !!user?.is_admin;
  const [editing, setEditing] = useState(null);
  const [creating, setCreating] = useState(false);
  const [q, setQ] = useState("");
  // Par défaut on tombe sur la première catégorie de la liste, pas sur "Toutes".
  const [cat, setCat] = useState(() => {
    const set = new Set(sites.map((s) => s.category).filter(Boolean));
    const first = [...set].sort()[0];
    return first || "";
  });

  const categories = useMemo(() => {
    const set = new Set(sites.map((s) => s.category).filter(Boolean));
    return [...set].sort();
  }, [sites]);

  const filtered = sites.filter((s) =>
    (!q || s.name.toLowerCase().includes(q.toLowerCase()) || (s.brand_group || "").toLowerCase().includes(q.toLowerCase())) &&
    (!cat || s.category === cat));

  return (
    <div>
      <div className="filters">
        <input className="grow" placeholder={`Rechercher parmi ${sites.length} marques…`} value={q} onChange={(e) => setQ(e.target.value)} />
        {isAdmin && <button className="btn sm" onClick={() => setCreating(true)}>+ Ajouter une marque</button>}
      </div>
      <div className="filters" style={{ gap: 7 }}>
        {categories.map((c) => (
          <button key={c} className={"chip" + (cat === c ? " active" : "")} onClick={() => setCat(cat === c ? "" : c)}>
            {c} ({sites.filter((s) => s.category === c).length})
          </button>
        ))}
        <button className={"chip" + (!cat ? " active" : "")} onClick={() => setCat("")}>Toutes ({sites.length})</button>
      </div>

      <div className="grid">
        {filtered.map((s) => (
          <div className="scard" key={s.id} style={{ opacity: s.active ? 1 : 0.5 }}>
            {s.open_count > 0 && <span className="open-pill">{s.open_count} en cours</span>}
            <div className="shead">
              <SiteLogo name={s.name} logo={s.logo} size={42} />
              <div>
                <h3>{s.name}</h3>
                <div className="meta">{s.category || "—"}{s.brand_group ? ` · groupe ${s.brand_group}` : ""}</div>
              </div>
            </div>
            <div className="meta">Parcours {s.flow_type} · {s.contact_method === "web_form" ? "formulaire web" : s.contact_method}</div>
            <div className="foot">
              {s.support_url && <a href={s.support_url} target="_blank" rel="noreferrer" style={{ fontSize: 12 }}>Support ↗</a>}
              {isAdmin && <button className="btn ghost sm" style={{ marginLeft: "auto" }} onClick={() => setEditing(s)}>Éditer</button>}
            </div>
          </div>
        ))}
      </div>
      {filtered.length === 0 && <div className="empty"><div className="big">🔎</div>Aucune marque ne correspond.</div>}

      {(editing || creating) &&
        <SiteForm site={editing} onClose={() => { setEditing(null); setCreating(false); }}
          onSaved={() => { setEditing(null); setCreating(false); onChange(); toast("Site enregistré"); }} />}
    </div>
  );
}

function SiteForm({ site, onClose, onSaved }) {
  const [s, setS] = useState(site || { name: "", brand_group: "", category: "", domain: "", logo: "", support_url: "", entry_url: "", flow_type: "dynamique", contact_method: "web_form", contact_email: "", contact_phone: "", required_fields: ["serial_number", "order_ref", "dispute_reason"], frequency_rank: 999, active: true, notes: "" });
  const [error, setError] = useState("");
  const set = (k, v) => setS({ ...s, [k]: v });

  const save = async () => {
    if (!s.name.trim()) { setError("Le nom est obligatoire."); return; }
    const body = { ...s, required_fields: Array.isArray(s.required_fields) ? s.required_fields : String(s.required_fields).split(",").map((x) => x.trim()).filter(Boolean), frequency_rank: Number(s.frequency_rank) || 999 };
    try {
      if (site) await api.patch("/api/sites/" + site.id, body);
      else await api.post("/api/sites", body);
      onSaved();
    } catch (e) { setError(e.message); }
  };

  return (
    <Overlay center onClose={onClose}>
      <button className="btn ghost sm close" onClick={onClose}>{Ico.x}</button>
      <h2><SiteLogo name={s.name} logo={s.logo} size={34} />{site ? "Éditer la marque" : "Nouvelle marque"}</h2>
      <div style={{ height: 14 }} />
      <div className="row3">
        <div className="field"><label className="req">Nom</label><input value={s.name} onChange={(e) => set("name", e.target.value)} /></div>
        <div className="field"><label>Groupe</label><input value={s.brand_group || ""} onChange={(e) => set("brand_group", e.target.value)} /></div>
        <div className="field"><label>Catégorie</label><input value={s.category || ""} onChange={(e) => set("category", e.target.value)} placeholder="Informatique, Audio…" /></div>
      </div>
      <div className="row2">
        <div className="field"><label>Domaine web (pour le logo)</label><input value={s.domain || ""} onChange={(e) => set("domain", e.target.value)} placeholder="dell.com" /></div>
        <div className="field"><label>Fichier logo</label><input value={s.logo || ""} onChange={(e) => set("logo", e.target.value)} placeholder="dell.png (dans frontend/logos/)" /></div>
      </div>
      <div className="row2">
        <div className="field"><label>Type de flux</label>
          <select value={s.flow_type} onChange={(e) => set("flow_type", e.target.value)}>
            <option value="dynamique">Dynamique (compte / n° série)</option>
            <option value="statique">Statique (URL de formulaire fixe)</option>
            <option value="email">Email</option>
            <option value="telephone">Téléphone</option>
          </select></div>
        <div className="field"><label>Méthode de contact</label>
          <select value={s.contact_method} onChange={(e) => set("contact_method", e.target.value)}>
            <option value="web_form">Formulaire web</option>
            <option value="email">Email</option>
            <option value="phone">Téléphone</option>
          </select></div>
      </div>
      <div className="field"><label>URL support</label><input value={s.support_url || ""} onChange={(e) => set("support_url", e.target.value)} /></div>
      <div className="field"><label>URL d'entrée du parcours RMA</label><input value={s.entry_url || ""} onChange={(e) => set("entry_url", e.target.value)} /></div>
      <div className="row2">
        <div className="field"><label>Email support</label><input value={s.contact_email || ""} onChange={(e) => set("contact_email", e.target.value)} /></div>
        <div className="field"><label>Téléphone support</label><input value={s.contact_phone || ""} onChange={(e) => set("contact_phone", e.target.value)} /></div>
      </div>
      <div className="row2">
        <div className="field"><label>Champs requis (virgules)</label>
          <input value={Array.isArray(s.required_fields) ? s.required_fields.join(", ") : s.required_fields}
            onChange={(e) => set("required_fields", e.target.value.split(",").map((x) => x.trim()).filter(Boolean))} /></div>
        <div className="field"><label>Rang de fréquence (1 = plus fréquent)</label>
          <input type="number" value={s.frequency_rank} onChange={(e) => set("frequency_rank", e.target.value)} /></div>
      </div>
      <div className="row2">
        <div className="field"><label>Délai de réponse cible — SLA (jours)</label>
          <input type="number" min="1" value={s.sla_days ?? 7} onChange={(e) => set("sla_days", Number(e.target.value) || 7)} /></div>
        <div />
      </div>
      <div className="field"><label>Notes</label><textarea rows="2" value={s.notes || ""} onChange={(e) => set("notes", e.target.value)} /></div>
      <div className="field"><label style={{ display: "flex", alignItems: "center", gap: 8, textTransform: "none", fontSize: 13 }}>
        <input type="checkbox" style={{ width: "auto" }} checked={s.active} onChange={(e) => set("active", e.target.checked)} /> Marque active</label></div>
      {error && <div className="err">{error}</div>}
      <div className="toolbar"><button className="btn" onClick={save}>Enregistrer</button></div>
    </Overlay>
  );
}

/* ------------------------------------------------- Copilote de soumission */
function SubmitCopilot({ dossier, site, onClose, onDone }) {
  const [checked, setChecked] = useState({});
  const [copied, setCopied] = useState({});
  const [mref, setMref] = useState(dossier.manufacturer_ref || "");
  const [editingCk, setEditingCk] = useState(null); // texte de la checklist en édition
  const [checklist, setChecklist] = useState(site.checklist || []);
  const [proxy, setProxy] = useState({
    proxy_server: dossier.proxy_server || "", proxy_username: dossier.proxy_username || "", proxy_password: dossier.proxy_password || "",
  });
  const [launching, setLaunching] = useState(false);
  const [browserStatus, setBrowserStatus] = useState(null);
  useEffect(() => { api.get("/api/dossiers/browser/status").then(setBrowserStatus); }, []);

  const launchBrowser = async () => {
    setLaunching(true);
    try {
      await api.patch("/api/dossiers/" + dossier.id, proxy);   // persiste le proxy du dossier
      const r = await api.post(`/api/dossiers/${dossier.id}/launch-browser`, {});
      toast(r.proxy ? `Navigateur ouvert via ${r.proxy}` : "Navigateur ouvert");
    } catch (e) { toast(e.message, true); }
    setLaunching(false);
  };

  useEffect(() => { api.post(`/api/dossiers/${dossier.id}/submission/start`, {}); }, [dossier.id]);

  const FIELDS = [
    ["Référence interne", dossier.reference],
    ["Client", dossier.client_name],
    ["Produit", dossier.product],
    ["Réf. commande", dossier.order_ref],
    ["N° de série", dossier.serial_number],
    ["Motif du litige", dossier.dispute_reason],
    ["Montant réclamé", dossier.amount_claimed ? String(dossier.amount_claimed).replace(".", ",") : null],
    ["Transporteur", dossier.carrier],
  ].filter(([, v]) => v);

  const copy = async (label, val) => {
    await navigator.clipboard.writeText(String(val));
    setCopied({ ...copied, [label]: true });
    toast(label + " copié");
  };
  const saveChecklist = async () => {
    const lines = editingCk.split("\n").map((l) => l.trim()).filter(Boolean);
    try {
      const updated = await api.patch("/api/sites/" + site.id, { checklist: lines });
      setChecklist(updated.checklist); setEditingCk(null); toast("Checklist enregistrée pour " + site.name);
    } catch (e) { toast(e.message, true); }
  };
  const complete = async () => {
    try {
      await api.post(`/api/dossiers/${dossier.id}/submission/complete`, { manufacturer_ref: mref.trim() || null });
      toast("Soumission enregistrée — dossier en attente, relance planifiée à J+" + (site.sla_days ?? 7));
      onDone();
    } catch (e) { toast(e.message, true); }
  };
  const cancel = async () => {
    await api.post(`/api/dossiers/${dossier.id}/submission/cancel`, {});
    onClose();
  };

  const doneSteps = checklist.filter((_, i) => checked[i]).length;

  return (
    <Overlay center onClose={cancel}>
      <button className="btn ghost sm close" onClick={cancel}>{Ico.x}</button>
      <h2><SiteLogo name={site.name} logo={site.logo} size={36} />Soumettre chez {site.name}</h2>
      <div className="muted" style={{ margin: "4px 0 14px 48px" }}>
        {dossier.reference} · {dossier.client_name} — {dossier.product}
      </div>

      <div className="section-title" style={{ marginTop: 6 }}>Navigateur & proxy (propre à ce dossier)</div>
      <div className="row3">
        <div className="field"><label>Proxy (host:port)</label>
          <input value={proxy.proxy_server} placeholder="http://host:port ou socks5://host:port"
            onChange={(e) => setProxy({ ...proxy, proxy_server: e.target.value })} /></div>
        <div className="field"><label>Identifiant proxy</label>
          <input value={proxy.proxy_username} placeholder="optionnel"
            onChange={(e) => setProxy({ ...proxy, proxy_username: e.target.value })} /></div>
        <div className="field"><label>Mot de passe proxy</label>
          <input type="password" value={proxy.proxy_password} placeholder="optionnel"
            onChange={(e) => setProxy({ ...proxy, proxy_password: e.target.value })} /></div>
      </div>
      <div className="toolbar" style={{ marginTop: 2 }}>
        {browserStatus?.reason !== "hosted" && (
          <button className="btn" onClick={launchBrowser} disabled={launching}>
            {launching ? "Ouverture…" : (<>{Ico.globe} Ouvrir le navigateur{proxy.proxy_server ? " (proxy)" : ""}</>)}
          </button>
        )}
        {site.entry_url && <a className="btn ghost sm" href={site.entry_url} target="_blank" rel="noreferrer">↗ Ouvrir dans mon navigateur</a>}
        <span className="faint" style={{ fontSize: 12 }}>
          {browserStatus?.reason === "hosted"
            ? "Le lancement Chromium+proxy est réservé à l'app installée en local — utilise « Ouvrir dans mon navigateur » ici."
            : "Chromium visible via ton proxy. Connexion, CAPTCHA et validation finale restent à ta main."}
        </span>
      </div>

      <div className="section-title">Parcours {site.name} — {doneSteps}/{checklist.length} étapes
        {editingCk == null && <button className="linkish" style={{ marginLeft: 10, fontSize: 11 }} onClick={() => setEditingCk(checklist.join("\n"))}>{Ico.edit} modifier</button>}
      </div>
      {editingCk == null ? (
        <div>
          {checklist.map((step, i) => (
            <label key={i} style={{ display: "flex", gap: 10, alignItems: "flex-start", padding: "6px 2px", cursor: "pointer", opacity: checked[i] ? 0.55 : 1 }}>
              <input type="checkbox" checked={!!checked[i]} onChange={(e) => setChecked({ ...checked, [i]: e.target.checked })} style={{ marginTop: 3 }} />
              <span style={{ textDecoration: checked[i] ? "line-through" : "none", fontSize: 13.5 }}>{step}</span>
            </label>
          ))}
        </div>
      ) : (
        <div>
          <textarea rows={Math.max(6, checklist.length + 1)} style={{ width: "100%", background: "var(--glass)", border: "1px solid var(--stroke)", color: "var(--text)", borderRadius: 10, padding: 10, fontFamily: "inherit", fontSize: 13 }}
            value={editingCk} onChange={(e) => setEditingCk(e.target.value)} placeholder="Une étape par ligne" />
          <div className="toolbar" style={{ marginTop: 6 }}>
            <button className="btn sm" onClick={saveChecklist}>Enregistrer la checklist</button>
            <button className="btn ghost sm" onClick={() => setEditingCk(null)}>Annuler</button>
            <span className="faint" style={{ fontSize: 11.5 }}>Vider tout = revenir à la checklist par défaut.</span>
          </div>
        </div>
      )}

      <div className="section-title">Champs du dossier — clique pour copier</div>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 8 }}>
        {FIELDS.map(([label, val]) => (
          <button key={label} className="alert-item" style={{ width: "100%", textAlign: "left", background: copied[label] ? "rgba(52,211,153,.07)" : "var(--glass-2)", border: "1px solid " + (copied[label] ? "rgba(52,211,153,.35)" : "var(--stroke)"), fontFamily: "inherit", color: "var(--text)" }}
            onClick={() => copy(label, val)}>
            <span className="atxt" style={{ whiteSpace: "normal" }}>
              <span className="faint" style={{ fontSize: 10.5, textTransform: "uppercase", letterSpacing: ".6px", display: "block" }}>{label} {copied[label] && "✓"}</span>
              <b style={{ fontSize: 13 }}>{String(val).slice(0, 80)}</b>
            </span>
            <span className="pill">{Ico.copy}</span>
          </button>
        ))}
      </div>

      <div className="section-title">Terminer</div>
      <div className="row2">
        <div className="field"><label>N° de dossier fabricant (obtenu à la fin du parcours)</label>
          <input value={mref} onChange={(e) => setMref(e.target.value)} placeholder="ex. SR-123456789" /></div>
        <div className="field" style={{ display: "flex", alignItems: "flex-end", gap: 8 }}>
          <button className="btn gold" onClick={complete}>✅ Soumission terminée</button>
          <button className="btn ghost" onClick={cancel}>Abandonner</button>
        </div>
      </div>
      <div className="faint" style={{ fontSize: 12 }}>
        À la validation : statut → <b>en attente</b>, action « ⏳ attendre réponse » planifiée à J+{site.sla_days ?? 7},
        n° fabricant utilisé pour détecter les emails de réponse (Gmail).
      </div>
    </Overlay>
  );
}

/* ------------------------------------------------------- Courriers ------- */
function LetterModal({ dossier, site, onClose, onSent }) {
  const [templates, setTemplates] = useState([]);
  const [key, setKey] = useState("");
  const [rendered, setRendered] = useState(null);   // {subject, body, to}
  const [editTpl, setEditTpl] = useState(null);     // {subject, body} du modèle en édition
  const [accounts, setAccounts] = useState([]);     // boîtes Gmail (avec can_send)
  const [sendAccount, setSendAccount] = useState("");
  const [toAddr, setToAddr] = useState("");
  const [selAtts, setSelAtts] = useState({});       // {attachmentId: true}
  const [sending, setSending] = useState(false);

  useEffect(() => {
    api.get("/api/letters/templates").then((t) => { setTemplates(t); if (t[0]) setKey(t[0].key); });
    api.get("/api/gmail/status").then((s) => {
      const accs = s.accounts || [];
      setAccounts(accs);
      const firstSendable = accs.find((a) => a.valid && a.can_send);
      if (firstSendable) setSendAccount(firstSendable.email);
    });
  }, []);

  const load = useCallback(async () => {
    if (!key) return;
    setEditTpl(null);
    const r = await api.get(`/api/letters/render/${dossier.id}/${key}`);
    setRendered(r);
    setToAddr((prev) => prev || r.to || "");
  }, [key, dossier.id]);
  useEffect(() => { load(); }, [load]);

  const sendable = accounts.filter((a) => a.valid && a.can_send);
  const sendMail = async () => {
    if (!sendAccount) { toast("Aucune boîte avec permission d'envoi", true); return; }
    if (!toAddr.trim()) { toast("Renseigne le destinataire", true); return; }
    const ids = Object.keys(selAtts).filter((k) => selAtts[k]).map(Number);
    if (!confirm(`Envoyer ce courrier à ${toAddr} depuis ${sendAccount}${ids.length ? ` avec ${ids.length} pièce(s) jointe(s)` : ""} ?`)) return;
    setSending(true);
    try {
      const r = await api.post("/api/gmail/send", {
        dossier_id: dossier.id, account: sendAccount, to: toAddr.trim(),
        subject: rendered.subject, body: rendered.body, attachment_ids: ids,
      });
      toast(`Email envoyé ✓${r.attachments ? ` (${r.attachments} PJ)` : ""}`);
      if (onSent) onSent();
      onClose();
    } catch (e) { toast(e.message, true); }
    setSending(false);
  };

  const currentTpl = templates.find((t) => t.key === key);

  const copyMail = async () => {
    await navigator.clipboard.writeText(`Objet : ${rendered.subject}\n\n${rendered.body}`);
    toast("Courrier copié (objet + corps)");
  };
  const openGmail = () => {
    const u = new URL("https://mail.google.com/mail/");
    u.searchParams.set("view", "cm"); u.searchParams.set("fs", "1");
    if (rendered.to) u.searchParams.set("to", rendered.to);
    u.searchParams.set("su", rendered.subject);
    u.searchParams.set("body", rendered.body);
    window.open(u.toString(), "_blank");
  };
  const saveTpl = async () => {
    await api.put(`/api/letters/templates/${key}`, editTpl);
    const t = await api.get("/api/letters/templates"); setTemplates(t);
    setEditTpl(null); await load(); toast("Modèle enregistré");
  };
  const resetTpl = async () => {
    await api.post(`/api/letters/templates/${key}/reset`, {});
    const t = await api.get("/api/letters/templates"); setTemplates(t);
    setEditTpl(null); await load(); toast("Modèle réinitialisé");
  };

  return (
    <Overlay center onClose={onClose}>
      <button className="btn ghost sm close" onClick={onClose}>{Ico.x}</button>
      <h2>{Ico.doc} Courriers — {dossier.client_name}</h2>
      <div className="muted" style={{ margin: "2px 0 14px" }}>{dossier.reference} · {site ? site.name : "sans marque"} · rempli automatiquement, signé <b>{dossier.client_name}</b></div>

      <div className="row2">
        <div className="field"><label>Type de courrier</label>
          <select value={key} onChange={(e) => setKey(e.target.value)}>
            {templates.map((t) => <option key={t.key} value={t.key}>{t.name}{t.customized ? " ✎" : ""}</option>)}
          </select></div>
        <div className="field" style={{ display: "flex", alignItems: "flex-end", gap: 6 }}>
          {currentTpl && <button className="btn ghost sm" onClick={() => setEditTpl(editTpl ? null : { subject: currentTpl.subject, body: currentTpl.body })}>{Ico.edit} Modifier le modèle</button>}
        </div>
      </div>

      {editTpl ? (
        <div>
          <div className="field"><label>Objet du modèle</label>
            <input value={editTpl.subject} onChange={(e) => setEditTpl({ ...editTpl, subject: e.target.value })} /></div>
          <div className="field"><label>Corps du modèle <span className="faint">— variables : {"{client} {produit} {marque} {commande} {serie} {montant} {ref_fabricant} {reference}"} · {"{signature}"} = nom du titulaire</span></label>
            <textarea rows="12" value={editTpl.body} onChange={(e) => setEditTpl({ ...editTpl, body: e.target.value })} style={{ fontFamily: "inherit" }} /></div>
          <div className="toolbar">
            <button className="btn sm" onClick={saveTpl}>Enregistrer le modèle</button>
            <button className="btn ghost sm" onClick={resetTpl}>Réinitialiser au défaut</button>
            <button className="btn ghost sm" onClick={() => setEditTpl(null)}>Annuler</button>
          </div>
        </div>
      ) : rendered ? (
        <div>
          <div className="field"><label>Objet</label>
            <input readOnly value={rendered.subject} style={{ background: "var(--glass-2)" }} /></div>
          <div className="field"><label>Courrier (prêt à envoyer)</label>
            <textarea readOnly rows="14" value={rendered.body} style={{ background: "var(--glass-2)", fontFamily: "inherit", lineHeight: 1.5 }} /></div>
          <div className="section-title" style={{ marginTop: 8 }}>Envoi</div>
          <div className="row2">
            <div className="field"><label className="req">Destinataire</label>
              <input value={toAddr} onChange={(e) => setToAddr(e.target.value)} placeholder="email du SAV / fabricant" /></div>
            <div className="field"><label>Envoyer depuis</label>
              {sendable.length > 0 ? (
                <select value={sendAccount} onChange={(e) => setSendAccount(e.target.value)}>
                  {sendable.map((a) => <option key={a.email} value={a.email}>{a.email}</option>)}
                </select>
              ) : <input readOnly value="aucune boîte autorisée à envoyer" style={{ background: "var(--glass-2)", color: "var(--faint)" }} />}
            </div>
          </div>

          {dossier.attachments && dossier.attachments.length > 0 && (
            <div className="field">
              <label>Pièces jointes à inclure (coche selon la demande du fabricant)</label>
              <div>
                {dossier.attachments.map((a) => (
                  <label key={a.id} className="alert-item" style={{ cursor: "pointer", padding: "7px 10px" }}>
                    <input type="checkbox" checked={!!selAtts[a.id]} onChange={(e) => setSelAtts({ ...selAtts, [a.id]: e.target.checked })} />
                    <span className="atxt">{docCatMeta(a.category).icon} {a.filename} <span className="faint">({Math.round(a.size / 1024)} Ko · {docCatMeta(a.category).label})</span></span>
                  </label>
                ))}
              </div>
            </div>
          )}

          {sendable.length === 0 && (
            <div className="alert-item" style={{ cursor: "default", borderColor: "rgba(245,166,35,.35)" }}>
              <span className="adot" style={{ background: "var(--st-en_attente)" }} />
              <span className="atxt" style={{ whiteSpace: "normal", fontSize: 12.5 }}>Pour envoyer directement, va dans l'onglet <b>Gmail</b> et <b>reconnecte ta boîte</b> — Google demandera cette fois la permission d'envoi.</span>
            </div>
          )}

          <div className="toolbar">
            <button className="btn gold" onClick={sendMail} disabled={sending || sendable.length === 0}>{sending ? "Envoi…" : (<>{Ico.send} Envoyer par Gmail</>)}</button>
            <button className="btn ghost" onClick={copyMail}>{Ico.copy} Copier</button>
            <button className="btn ghost sm" onClick={openGmail}>Ouvrir dans Gmail (sans PJ)</button>
          </div>
          <div className="faint" style={{ fontSize: 12 }}>L'envoi part de ta boîte, avec les pièces cochées. Tu valides avant l'envoi. Champs vides du dossier retirés automatiquement.</div>
        </div>
      ) : <div className="empty">Chargement…</div>}
    </Overlay>
  );
}

/* -------------------------------------------------------------- Admin ---- */
function AdminPanel({ currentUser }) {
  const [users, setUsers] = useState([]);
  const [creating, setCreating] = useState(false);
  const [form, setForm] = useState({ login_id: "", full_name: "", is_admin: false });
  const [newCode, setNewCode] = useState(null);   // {login_id, code} affiché une fois
  const [error, setError] = useState("");
  const [accessOf, setAccessOf] = useState(null); // user dont on regarde les accès

  const load = useCallback(async () => setUsers(await api.get("/api/admin/users")), []);
  useEffect(() => { load(); }, [load]);

  const create = async () => {
    setError("");
    if (!form.login_id.trim()) { setError("Identifiant requis."); return; }
    try {
      const r = await api.post("/api/admin/users", form);
      setNewCode({ login_id: r.user.login_id, code: r.license_code });
      setCreating(false); setForm({ login_id: "", full_name: "", is_admin: false });
      await load(); toast("Compte créé");
    } catch (e) { setError(e.message); }
  };
  const resetCode = async (u) => {
    if (!confirm(`Générer un nouveau code pour ${u.login_id} ? L'ancien ne marchera plus.`)) return;
    const r = await api.post(`/api/admin/users/${u.id}/reset-code`, {});
    setNewCode({ login_id: u.login_id, code: r.license_code }); toast("Nouveau code généré");
  };
  const toggle = async (u) => {
    await api.patch(`/api/admin/users/${u.id}?active=${!u.active}`, {});
    await load();
  };
  const remove = async (u) => {
    if (!confirm(`Supprimer définitivement le compte ${u.login_id} ?`)) return;
    try { await api.del(`/api/admin/users/${u.id}`); await load(); toast("Compte supprimé"); }
    catch (e) { toast(e.message, true); }
  };

  return (
    <div>
      {newCode && (
        <div className="panel" style={{ marginBottom: 13, borderColor: "rgba(232,194,104,.4)" }}>
          <h3>{Ico.key} Code licence de « {newCode.login_id} »</h3>
          <p className="muted" style={{ margin: "0 0 8px", fontSize: 13 }}>Communique ce code au titulaire. Il ne sera <b>plus jamais affiché</b> — note-le maintenant.</p>
          <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
            <code style={{ fontSize: 18, letterSpacing: 2, background: "var(--glass-2)", border: "1px solid var(--stroke)", borderRadius: 10, padding: "10px 16px" }}>{newCode.code}</code>
            <button className="btn ghost sm" onClick={() => { navigator.clipboard.writeText(newCode.code); toast("Code copié"); }}>{Ico.copy} Copier</button>
            <button className="btn ghost sm" onClick={() => setNewCode(null)}>Fermer</button>
          </div>
        </div>
      )}

      <div className="toolbar" style={{ marginTop: 0 }}>
        <div className="section-title" style={{ borderTop: 0, paddingTop: 0, margin: 0 }}>Comptes ({users.length})</div>
        <button className="btn sm" style={{ marginLeft: "auto" }} onClick={() => setCreating((c) => !c)}>+ Nouveau compte</button>
      </div>

      {creating && (
        <div className="panel" style={{ marginBottom: 13 }}>
          <div className="row3">
            <div className="field"><label className="req">Identifiant</label>
              <input value={form.login_id} onChange={(e) => setForm({ ...form, login_id: e.target.value })} placeholder="ex. julien" /></div>
            <div className="field"><label>Nom complet</label>
              <input value={form.full_name} onChange={(e) => setForm({ ...form, full_name: e.target.value })} placeholder="Julien Martin" /></div>
            <div className="field"><label>Rôle</label>
              <select value={form.is_admin ? "1" : "0"} onChange={(e) => setForm({ ...form, is_admin: e.target.value === "1" })}>
                <option value="0">Agent (accès standard)</option>
                <option value="1">Administrateur</option>
              </select></div>
          </div>
          {error && <div className="err">{error}</div>}
          <div className="toolbar"><button className="btn" onClick={create}>Créer le compte</button>
            <button className="btn ghost" onClick={() => setCreating(false)}>Annuler</button></div>
        </div>
      )}

      <div className="table-wrap">
        <table className="data">
          <thead><tr><th>Identifiant</th><th>Nom</th><th>Rôle</th><th>Statut</th><th>Dernier accès</th><th></th></tr></thead>
          <tbody>
            {users.map((u) => (
              <tr key={u.id}>
                <td style={{ fontWeight: 600 }}>{u.login_id}{u.id === currentUser.id && <span className="faint"> (toi)</span>}</td>
                <td className="muted">{u.full_name || "—"}</td>
                <td>{u.is_admin ? <span className="badge valide">Admin</span> : <span className="muted">Agent</span>}</td>
                <td>{u.active ? <span className="badge rembourse">Actif</span> : <span className="badge refuse">Désactivé</span>}</td>
                <td className="muted" style={{ fontSize: 12 }}>
                  {u.last_login
                    ? <button className="linkish" onClick={() => setAccessOf(u)}>{fmtDateTime(u.last_login)}<br /><span className="faint">{u.last_ip}{u.last_location ? ` · ${u.last_location}` : ""} · {u.login_count} conn.</span></button>
                    : <span className="faint">jamais connecté</span>}
                </td>
                <td style={{ textAlign: "right", whiteSpace: "nowrap" }}>
                  <button className="btn ghost sm" onClick={() => setAccessOf(u)}>{Ico.eye} Accès</button>{" "}
                  <button className="btn ghost sm" onClick={() => resetCode(u)}>{Ico.key} Code</button>{" "}
                  {u.id !== currentUser.id && <button className="btn ghost sm" onClick={() => toggle(u)}>{u.active ? "Désactiver" : "Réactiver"}</button>}{" "}
                  {u.id !== currentUser.id && <button className="btn danger sm" onClick={() => remove(u)}>Suppr.</button>}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>

      {accessOf && <AccessLog user={accessOf} onClose={() => setAccessOf(null)} />}
      <div className="ok-msg" style={{ fontSize: 12, marginTop: 10 }}>
        {Ico.lock} Isolation des données <b>active</b> : chaque compte ne voit que ses propres dossiers, marques, documents et boîtes Gmail.
      </div>
    </div>
  );
}

function AccessLog({ user, onClose }) {
  const [events, setEvents] = useState(null);
  useEffect(() => { api.get(`/api/admin/users/${user.id}/access?limit=100`).then(setEvents); }, [user.id]);
  const devIcon = (d) => d === "Mobile" ? "📱" : d === "Tablette" ? "📟" : "🖥️";
  return (
    <Overlay center onClose={onClose}>
      <button className="btn ghost sm close" onClick={onClose}>{Ico.x}</button>
      <h2>{Ico.eye} Accès — {user.full_name || user.login_id}</h2>
      <div className="muted" style={{ margin: "2px 0 14px" }}>Journal des connexions (IP, machine, opérateur).</div>
      {!events ? <div className="empty">Chargement…</div>
        : events.length === 0 ? <div className="empty">Aucune connexion enregistrée.</div>
        : (
          <div className="table-wrap">
            <table className="data">
              <thead><tr><th>Date</th><th>IP</th><th>Machine</th><th>Opérateur</th><th>Localisation</th></tr></thead>
              <tbody>
                {events.map((e, i) => (
                  <tr key={i}>
                    <td className="muted" style={{ fontSize: 12 }}>{fmtDateTime(e.created_at)}</td>
                    <td style={{ fontVariantNumeric: "tabular-nums" }}>{e.ip || "—"}</td>
                    <td style={{ fontSize: 12.5 }} title={e.user_agent}>{devIcon(e.device)} {e.os} · {e.browser}</td>
                    <td className="muted" style={{ fontSize: 12.5 }}>{e.isp || "—"}</td>
                    <td className="muted" style={{ fontSize: 12.5 }}>{e.location || "—"}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      <div className="faint" style={{ fontSize: 11.5, marginTop: 10 }}>
        Opérateur & localisation : géolocalisation d'IP (best-effort, indisponible pour les accès locaux/réseau interne).
      </div>
    </Overlay>
  );
}

/* ------------------------------------------------------------ Overlay ---- */
function Overlay({ children, onClose, center }) {
  useEffect(() => {
    const h = (e) => e.key === "Escape" && onClose();
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, [onClose]);
  return (
    <div className="overlay" onClick={(e) => e.target.classList.contains("overlay") && onClose()}>
      <div className={"drawer" + (center ? " center" : "")}>
        <ErrorBoundary fallback={(err, reset) => (
          <div style={{ padding: 10 }}>
            <button className="btn ghost sm" onClick={onClose}>✕ Fermer</button>
            <div style={{ fontSize: 34, margin: "18px 0 6px" }}>⚠️</div>
            <h2 style={{ margin: "0 0 6px" }}>Problème d'affichage</h2>
            <p className="muted" style={{ fontSize: 13 }}>Ce panneau a rencontré une erreur — le reste de l'appli fonctionne toujours.</p>
            <pre style={{ background: "var(--glass)", border: "1px solid var(--stroke)", borderRadius: 10, padding: 10, fontSize: 11.5, color: "#fb9d9d", overflow: "auto", maxHeight: 120 }}>{String(err && (err.message || err))}</pre>
            <div className="toolbar"><button className="btn" onClick={reset}>Réessayer</button><button className="btn ghost" onClick={onClose}>Fermer</button></div>
          </div>
        )}>
          {children}
        </ErrorBoundary>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(
  <ErrorBoundary><AuthGate /></ErrorBoundary>
);
