/* CargoNote — app shell, sidebar, top bar, routing. Data comes from the backend API (window.CN.api); the open case detail and the inbox list live in App state and are passed to the views as `data`. */ const App = () => { const [cases, setCases] = React.useState([]); const [detail, setDetail] = React.useState(null); // the case open in review/final const [route, setRoute] = React.useState("loading"); // loading → inbox/review/... const [toast, setToast] = React.useState(null); const data = { cases, heroCase: detail }; const showToast = (m) => { setToast(m); setTimeout(() => setToast(null), 2600); }; const refreshCases = React.useCallback(async () => { const list = await window.CN.api.listCases(); setCases(list); return list; }, []); const openCase = React.useCallback(async (ref) => { try { const d = await window.CN.api.getCase(ref); setDetail(d); setRoute("review"); } catch (e) { showToast("Impossible d'ouvrir le dossier"); } }, []); // Initial load: fetch the inbox, then open the hero (or first reviewable) case. React.useEffect(() => { (async () => { try { const list = await refreshCases(); const hero = list.find((c) => c.hero) || list.find((c) => c.status === "review"); if (hero) { const d = await window.CN.api.getCase(hero.id); setDetail(d); setRoute("review"); } else { setRoute("inbox"); } } catch (e) { setRoute("apierror"); } })(); }, [refreshCases]); // Tweaks const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{ "density": "comfortable", "confViz": "all", "accent": "indigo" }/*EDITMODE-END*/; const [t, setTweak] = useTweaks(TWEAK_DEFAULTS); React.useEffect(() => { document.body.setAttribute("data-density", t.density); document.body.setAttribute("data-conf-viz", t.confViz); const accents = { indigo: "53% 0.14 256", teal: "55% 0.12 195", olive: "55% 0.11 130", terracotta: "58% 0.14 40", }; const v = accents[t.accent] || accents.indigo; document.documentElement.style.setProperty("--accent", `oklch(${v})`); document.documentElement.style.setProperty("--accent-2", `oklch(${v.replace(/^[\d.]+%/, (m) => (parseFloat(m) - 8) + "%")})`); }, [t]); // Create a case: real files run the extraction pipeline; no files → demo email. const handleIngest = React.useCallback(async (files) => { const hasFiles = files && files.length > 0; try { const created = await window.CN.api.createCase(hasFiles ? files : null, !hasFiles); await refreshCases(); if (created.status === "error") { setRoute("inbox"); showToast(created.errorMsg || "Échec de l'extraction"); } else { await openCase(created.id); showToast(`Brouillon ${created.ref} ouvert pour vérification`); } } catch (e) { setRoute("inbox"); showToast("Échec de l'extraction"); } }, [refreshCases, openCase]); const handleApprove = React.useCallback(async () => { if (!detail) return; try { await window.CN.api.approve(detail.ref); await refreshCases(); setDetail((d) => ({ ...d, status: "approved" })); setRoute("final"); showToast(`${detail.ref} validé`); } catch (e) { showToast("Échec de la validation"); } }, [detail, refreshCases]); const counts = { review: cases.filter((c) => c.status === "review").length, approved: cases.filter((c) => c.status === "approved").length, error: cases.filter((c) => c.status === "error").length, }; const nav = [ { id: "inbox", icon: "inbox", label: "Boîte de réception", count: cases.length, screen: "inbox" }, { id: "review", icon: "eye", label: "À vérifier", count: counts.review, screen: "review" }, { id: "approved", icon: "check", label: "Validés", count: counts.approved, screen: "inbox" }, { id: "error", icon: "alert", label: "Erreurs", count: counts.error, screen: "inbox" }, ]; const tools = [ { id: "new", icon: "plus", label: "Nouveau dossier", screen: "ingest" }, { id: "history", icon: "history", label: "Historique" }, { id: "settings", icon: "settings", label: "Paramètres" }, ]; if (route === "loading") { return