/* Review workspace — the hero screen Left: source viewer (tabs + provenance highlights) Right: CMR form grouped + goods table + validation panel Sticky action bar. */ // ---- Source documents (rendered as faux paper pages with highlights) ---- const InvoiceDoc = ({ refs, activeKey, onActivate }) => (
SOREMED Industries SARL
Zone Industrielle Sidi Bernoussi · 20600 Casablanca · Maroc
ICE 001745829000034 · RC 132 445
FACTURE COMMERCIALE
N° INV-2026-77231
22 / 05 / 2026
SOREMED Industries SARL
Zone Industrielle Sidi Bernoussi
20600 Casablanca, Maroc
ICE: 001745829000034
Logística Ibérica Distribución S.A.
Calle del Comercio 142
28038 Madrid, España
CIF: A-87614520

Port de Casablanca — 24/05/2026
Entrepôt Vallecas — Madrid (ES)
Facture INV-2026-77231 · Liste de colisage PKL-77231 · Certificat d'origine EUR.1 N° A 4421987

Désignation Qté P.U. € Total €
Conserves de poisson — sardines12 pal.185,002 220,00
Textile coton — vêtements travail8 cart.62,50500,00
Pièces auto Renault — radiateurs3 cs.118,00354,00
Phosphates 70BPL — sacs OCP1 ctn.
Total HT3 074,00 €
Établi à Casablanca, le 22/05/2026 · H. El Khattabi, Service logistique
{/* Highlights — absolutely positioned over the page */} {refs.filter(r => r.doc === "invoice").map((r) => ( onActivate(r.id)} /> ))}
); const PackingDoc = ({ refs, activeKey, onActivate }) => (
LISTE DE COLISAGE / PACKING LIST
PKL-77231 · 22/05/2026
SOREMED Industries → Logística Ibérica

Marques Colis Emballage Nature de la marchandise SH kg
SRM/77231-A12Palettes EURConserves sardines à l'huile d'olive1604.134 8208,40
SRM/77231-B8CartonsTextile coton — vêtements travail6203.321 2403,10
SRM/77231-C3Caisses boisPièces auto — radiateurs Renault Clio8708.91 ?1860,90
SRM/77231-D1Conteneur 20'Phosphates en sacs 70BPL2510.2024,0
Note (manuscrit) : conteneur D — poids approximatif, à vérifier à l'embarquement
{refs.filter(r => r.doc === "packing").map((r) => ( onActivate(r.id)} /> ))}
); const EmailDoc = ({ refs, activeKey, onActivate }) => (
RE: Expédition #DK-88421 — Casablanca → Madrid
De · H. El Khattabi <logistique@soremed.ma>
À · ops+cargonote@atlas-maghreb.ma
Date · 22 mai 2026, 09:12
{`Bonjour, Veuillez trouver ci-joint les documents pour l'expédition de demain : facture commerciale, liste de colisage et photo du BL signé. Détails : — Transporteur retenu : Transports Atlas Maghreb (licence TIR-MA-2241) — Enlèvement : 24/05/2026 au Port de Casablanca — Livraison : Entrepôt Vallecas, Madrid — Contact destinataire : M. Martínez, +34 911 22 88 ●● (texte effacé) — Tarif : franco, 2 850 € forfait porte-à-porte (cf. devis DV-2026-118) — Température < 25 °C — livraison sur RDV uniquement Merci de confirmer la prise en charge. Cordialement, H. El Khattabi Service logistique, SOREMED Industries`}
{refs.filter(r => r.doc === "email").map((r) => ( onActivate(r.id)} /> ))}
); const PhotoDoc = ({ refs, activeKey, onActivate }) => (
IMG_8842.jpg — photo BL manuscrit 2 8 MB · 3024 × 4032
BORDEREAU DE LIVRAISON
Atlas Maghreb · BL-MAN-3318
Lot SRM/77231-C · 3 caisses bois · 168 kg
Lot SRM/77231-D · 1 conteneur 20' · 18 4__ kg (illisible)

Observations: caisses bois (C) légèrement
endommagées au chargement — coin sup. droit.

Signature chauffeur: Y. Benali
{refs.filter(r => r.doc === "photo").map((r) => ( onActivate(r.id)} /> ))}
); const Block = ({ label, children }) => (
{children}
); const Label = ({ children }) => (
{children}
); // ---- Real uploaded document (page image with persistent provenance overlays) ---- const RealDoc = ({ doc, refs, activeKey, onActivate }) => { if (!doc) return null; if (doc.kind === "image") { const pageRefs = (refs || []).filter((r) => r.doc === doc.id); return (
{doc.label} {pageRefs.map((r) => (
onActivate && onActivate(r.id)} title={r.id} /> ))}
); } if (doc.kind === "pdf") { return (
Ouvrir {doc.label}
); } return
{doc.label}
; }; // Build the flat list of provenance refs from a real case's fields + goods rows. // Matches the shape the seeded demo uses for InvoiceDoc/PackingDoc. const buildRealRefs = (fields) => { const refs = []; for (const [key, f] of Object.entries(fields || {})) { if (key === "goods") continue; if (!f || typeof f !== "object") continue; if (f.doc && Array.isArray(f.rect) && f.rect.length === 4) { refs.push({ id: key, doc: f.doc, rect: f.rect, conf: f.bucket }); } } for (const g of (fields && fields.goods) || []) { if (g.doc && Array.isArray(g.rect) && g.rect.length === 4) { const minConf = Math.min(g.marksConf, g.packagesConf, g.packingConf, g.natureConf, g.hsConf, g.weightConf, g.volumeConf); const bucket = minConf >= 0.9 ? "hi" : minConf >= 0.7 ? "med" : "lo"; refs.push({ id: `goods.${g.id}`, doc: g.doc, rect: g.rect, conf: bucket }); } } return refs; }; // Parse a French-formatted number ("4 820", "8,40") to a float. const parseFrNum = (s) => { const n = parseFloat(String(s == null ? "" : s).replace(/\s/g, "").replace(",", ".")); return Number.isFinite(n) ? n : 0; }; const goodsTotals = (rows) => rows.reduce((a, r) => ({ packages: a.packages + parseFrNum(r.packages), weight: a.weight + parseFrNum(r.weight), volume: a.volume + parseFrNum(r.volume), }), { packages: 0, weight: 0, volume: 0 }); const Highlight = ({ r, active, onClick }) => (
r.onHover && r.onHover(r.id)} /> ); // ---- The review workspace ---- const ReviewView = ({ data, onApprove, onBack }) => { const docList = data.heroCase.docs || []; const [activeField, setActiveField] = React.useState(null); const [edited, setEdited] = React.useState({}); const [fields, setFields] = React.useState(data.heroCase.fields); // Real-case provenance refs — re-derive from current (possibly edited) fields. const realRefs = React.useMemo(() => buildRealRefs(fields), [fields]); // Open on the doc that actually has the most provenance — otherwise the first. const [activeDoc, setActiveDoc] = React.useState(() => { if (realRefs.length) { const counts = realRefs.reduce((m, r) => ((m[r.doc] = (m[r.doc] || 0) + 1), m), {}); const best = Object.entries(counts).sort((a, b) => b[1] - a[1])[0]; if (best) return best[0]; } return (docList[0] && docList[0].id) || "invoice"; }); // Field activation also switches doc to its source const activate = (id, fromDoc) => { setActiveField(id); if (fromDoc) { setActiveDoc(fromDoc); } else { const f = fields[id]; if (f && f.doc) setActiveDoc(f.doc); } }; const editField = (id, val) => { setFields((prev) => ({ ...prev, [id]: { ...prev[id], value: val } })); setEdited((prev) => ({ ...prev, [id]: true })); }; // Build list of highlight refs from current fields (single-field provenance) const docRefs = Object.entries(fields) .filter(([id, f]) => f.doc && f.rect) .map(([id, f]) => ({ id, doc: f.doc, rect: f.rect, conf: f.bucket })); const renderConf = (id, score, conflictWith) => { if (edited[id]) return "edited"; if (conflictWith) return "conflict"; return null; }; return (
{/* LEFT — doc viewer */}
{docList.map((d) => { const label = d.url ? (d.label.length > 22 ? d.label.slice(0, 20) + "…" : d.label) : (d.kind === "email" ? "Email" : d.kind === "image" ? "Photo BL" : d.id === "invoice" ? "Facture" : "Colisage"); return (
setActiveDoc(d.id)} style={{whiteSpace:"nowrap", flexShrink:0}}> {label}
); })}
Cliquez un champ pour voir sa source. Cliquez une zone pour ouvrir le champ correspondant.
{(() => { const docObj = docList.find((d) => d.id === activeDoc); if (docObj && docObj.url) { return ; } return ( <> {activeDoc === "invoice" && } {activeDoc === "packing" && } {activeDoc === "email" && } {activeDoc === "photo" && } ); })()}
{/* RIGHT — CMR form */}
Dossier CMR
{data.heroCase.ref}
{Object.values(fields).filter(f => f.bucket === "hi").length + Object.keys(edited).length}/{Object.keys(fields).length} champs validés
{/* Validation panel on top */} activate(id)}/> {/* PARTIES */}
{/* ROUTE */}
{/* GOODS */} Ajouter une ligne }> { const row = fields.goods[rowIdx]; setActiveDoc(row.doc || "packing"); setActiveField(`goods.${row.id}`); }}/> {(() => { const tot = goodsTotals(fields.goods); const fr = (n) => n.toLocaleString("fr-FR", { maximumFractionDigits: 2 }); return (
Total colis : {fr(tot.packages)} Poids brut total : {fr(tot.weight)} kg Volume total : {fr(tot.volume)} m³
); })()}
{/* DOCUMENTS & INSTRUCTIONS */}
{/* PAYMENT & CHARGES */}
{/* RESERVATIONS */}
{/* ISSUE */}
  Signatures (boîtes 22–24) — apposées lors de l'enlèvement et de la livraison, laissées vierges sur le brouillon.
{/* Sticky action bar */}
5 champs à vérifier · {Object.keys(edited).length} modifications manuelles · prêt à 78 %
); }; // ---- Goods table component ---- const GoodsTable = ({ rows, onActivate }) => { return ( {rows.map((r, idx) => { const minConf = Math.min(r.marksConf, r.packagesConf, r.packingConf, r.natureConf, r.hsConf, r.weightConf, r.volumeConf); const cls = minConf >= 0.9 ? "hi" : minConf >= 0.7 ? "med" : "lo"; return ( onActivate(idx)} style={{cursor:"pointer"}}> ); })}
Marques6 Col.7 Emballage8 Nature de la marchandise9 SH10 kg11 12
{r.marks} {r.packages} {r.packing} {r.nature} {r.conflict && (
Conflit poids ↗
)}
{r.hs} {r.weight} {r.conflict && r.conflict.field === "weight" && (
{r.conflict.other}
)}
{r.volume}
); }; // ---- Charges table ---- const ChargesTable = ({ rows }) => ( {rows.map((r, i) => ( ))}
Libellé Payé par l'expéditeur Payé par le destinataire
{r.label} {r.sender} {r.consignee}
); // ---- Validation panel ---- const ValidationPanel = ({ issues, fields, onJump }) => { const [open, setOpen] = React.useState(true); return (
{issues.length} points à vérifier
· champs requis manquants & faibles confiances
{open && (
    {issues.map((it, i) => (
  • onJump(it.field.split(".")[0])} style={{display:"flex", alignItems:"start", gap:10, padding:"9px 12px", borderTop: i===0?"0":"1px solid var(--hairline)", cursor:"pointer", fontSize:12.5}} > {it.sev === "lo" ? "Faible" : "Moyenne"} {it.msg} {it.field}
  • ))}
)}
); }; window.ReviewView = ReviewView;