Preuve · code, extrait

src/dashboard/components/RapportViewer.tsx

Dépôt jeremy-flexylead/flexylead-crm · commit ccbbaa07870e0f786729003dd6f8862e4bcc8256 · dossier de production synchronisé le 14 septembre 2026 à 08 h 46

Extrait tel qu'à ce commit : lignes 1–60, sur 482 (numérotation d'origine, 30 lignes de contexte autour de chaque citation) · fichier intégral sur GitHub (dépôt privé, accès requis)

60 lignes reproduites · ← retour à l'audit · catalogue des preuves

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 {