1import { useState } from 'react'; 2import { X, RotateCcw, ShoppingBag, Pencil, Check, Undo2, AlertTriangle, ShieldAlert } from 'lucide-react'; 3import { ScoreBadge, VerdictBadge } from './ScoreBadge'; 4import type { Rapport, Evaluation } from '../lib/types'; 5import { centreLabel, grilleLabel, formatDate, clientName, rapportProduits, rapportOffreIds } from '../lib/format'; 6import { useDbState, useRefresh, correctRapport } from '../api/client'; 7import { siblingAttempts } from '../lib/attempts'; 8import { VERDICTS, scoreForVerdict, canonicalVerdict } from '../lib/scoring'; 9import { controlesIa, fraudeAlerte } from '../lib/controlesIa'; 10 11export function RapportViewer({ rapport, onClose }: { rapport: Rapport; onClose: () => void }) { 12 const { data } = useDbState({ tables: ['offres', 'criteres'] }); 13 const refresh = useRefresh(); 14 const canEdit = !data?.partner; // partner/demo mode stays read-only 15 16 // Local override so the panel reflects a correction immediately, before the 17 // background refetch lands (the parent still holds the pre-edit rapport). 18 const [localEvals, setLocalEvals] = useState<Evaluation[] | null>(null); 19 const [localScore, setLocalScore] = useState<number | null>(null); 20 const evals = localEvals ?? rapport.evaluations; 21 const scoreTotal = localScore ?? rapport.score_total; 22 23 // Edit state 24 const [editing, setEditing] = useState(false); 25 const [draft, setDraft] = useState<Record<string, { verdict: string; commentaire: string }>>({}); 26 const [name, setName] = useState<string>(() => localStorage.getItem('flexy_corrector_name') || ''); 27 const [saving, setSaving] = useState(false); 28 const [error, setError] = useState<string | null>(null); 29 const [showAllComments, setShowAllComments] = useState(false); 30 const [expandedComments, setExpandedComments] = useState<Record<string, boolean>>({}); 31 32 // Has this rapport been hand-corrected? (any line carries an audit stamp) 33 const correctedLine = [...evals].reverse().find((e) => e.corrected_at); 34 const controles = controlesIa(rapport); 35 const controlesAlertes = controles.filter((c) => c.status === 'ALERTE'); 36 const fraude = fraudeAlerte(rapport); 37 38 // Find siblings (other attempts of the same dossier) for this rapport's appel 39 const rapportCallIds = [rapport.appel_id, ...(rapport.appel_ids || [])].filter(Boolean); 40 const appel = data?.appels.find(a => rapportCallIds.includes(a.id)); 41 const siblings = appel && data ? siblingAttempts(appel, data.appels) : []; 42 const siblingRapports = (data?.rapports || []).filter(r => [r.appel_id, ...(r.appel_ids || [])].some(id => id && siblings.some(s => s.id === id))); 43 const totalAttempts = siblings.length; 44 const attemptNum = appel?.attempt_number || 1; 45 46 // Linked ventes — one rapport can cover multiple products / sheet rows 47 const offreById = new Map((data?.offres || []).map(o => [o.id, o])); 48 const linkedVentes = (data?.ventes || []).filter(v => rapport.vente_ids?.includes(v.id)); 49 const rapportOffres = rapportOffreIds(rapport, data?.ventes || []) 50 .map((id) => offreById.get(id)) 51 .filter(Boolean); 52 const critereByKey = new Map((data?.criteres || []).map((c) => [`${c.grille_id}::${c.code}`, c])); 53 54 // Apply the live draft to an evaluation (for display + totals while editing). 55 const applyLive = (e: Evaluation): Evaluation => { 56 if (!editing) return e; 57 const d = draft[e.critere_code]; 58 if (!d) return e; 59 const verdictChanged = canonicalVerdict(d.verdict) !== canonicalVerdict(e.verdict); 60 return {