From da19629ee007757e92c3c9255756eb2eacc366d7 Mon Sep 17 00:00:00 2001 From: Manus Date: Wed, 2 Sep 2026 11:52:20 +0000 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Factures=20BAP=20inclut=20d=C3=A9?= =?UTF-8?q?sormais,=20apr=C3=A8s=20Ann=C3=A9e=20et=20Mois,=20les=20filtres?= =?UTF-8?q?=20Destinataires,=20Entit=C3=A9s=20et=20Ventilations=20comptabl?= =?UTF-8?q?es=20de=20la=20page=20Factures.=20Leur=20logique=20est=20identi?= =?UTF-8?q?que,=20avec=20valeurs=20absentes=20et=20r=C3=A8gle=20SANTINOVA/?= =?UTF-8?q?ITINOVA=20fond=C3=A9e=20sur=20DSI=20SANTINOVA.=20Les=20filtres?= =?UTF-8?q?=20sont=20conserv=C3=A9s=20au=20retour=20depuis=20le=20d=C3=A9t?= =?UTF-8?q?ail.=20Tests,=20TypeScript,=20build=20et=20rendu=20visuel=20val?= =?UTF-8?q?id=C3=A9s=20en=20sandbox.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/lib/bapNavigation.ts | 15 ++++++- client/src/pages/InvoicesBAP.tsx | 69 ++++++++++++++++++++++++++++++++ server/bapNavigation.test.ts | 15 +++++-- todo.md | 5 +++ 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/client/src/lib/bapNavigation.ts b/client/src/lib/bapNavigation.ts index 4ed7852..04d4f28 100644 --- a/client/src/lib/bapNavigation.ts +++ b/client/src/lib/bapNavigation.ts @@ -8,6 +8,9 @@ export type BapFilters = { statusFilter: string; selectedYear: string; selectedMonth: string; + recipientFilter: string; + entityFilter: string; + ventilationFilter: string; sortField: "invoiceDate" | "createdAt"; sortDir: "asc" | "desc"; }; @@ -34,6 +37,10 @@ function isYear(value: string | null): value is string { return value === "all" || (value !== null && /^\d{4}$/.test(value)); } +function isSafeFilterValue(value: string | null): value is string { + return value !== null && value.length > 0 && value.length <= 200; +} + /** Lit et valide les filtres BAP transmis dans l’URL. */ export function parseBapFilters(search: string, defaultYear: string): BapFilters { const params = asSearchParams(search); @@ -48,6 +55,9 @@ export function parseBapFilters(search: string, defaultYear: string): BapFilters statusFilter: status && BAP_STATUSES.has(status) ? status : "all", selectedYear: isYear(year) ? year : defaultYear, selectedMonth: month === "all" || isMonth(month) ? month : "all", + recipientFilter: isSafeFilterValue(params.get("recipient")) ? params.get("recipient")! : "all", + entityFilter: ["all", "santinova", "itinova"].includes(params.get("entity") || "") ? params.get("entity")! : "all", + ventilationFilter: isSafeFilterValue(params.get("ventilation")) ? params.get("ventilation")! : "all", sortField: sort === "invoiceDate" ? "invoiceDate" : "createdAt", sortDir: direction === "asc" ? "asc" : "desc", }; @@ -61,6 +71,9 @@ export function buildBapDetailLocation(invoiceId: number, filters: BapFilters): status: filters.statusFilter, year: filters.selectedYear, month: filters.selectedMonth, + recipient: filters.recipientFilter, + entity: filters.entityFilter, + ventilation: filters.ventilationFilter, sort: filters.sortField, dir: filters.sortDir, }); @@ -74,7 +87,7 @@ export function getBapReturnLocation(search: string): string { if (params.get("returnTo") !== "bap") return "/invoices"; const allowed = new URLSearchParams(); - for (const key of ["q", "status", "year", "month", "sort", "dir"]) { + for (const key of ["q", "status", "year", "month", "recipient", "entity", "ventilation", "sort", "dir"]) { const value = params.get(key); if (value) allowed.set(key, value); } diff --git a/client/src/pages/InvoicesBAP.tsx b/client/src/pages/InvoicesBAP.tsx index d80c0af..2a61f5e 100644 --- a/client/src/pages/InvoicesBAP.tsx +++ b/client/src/pages/InvoicesBAP.tsx @@ -116,6 +116,9 @@ export default function InvoicesBAP() { const [searchQuery, setSearchQuery] = useState(initialFilters.searchQuery); const [selectedIds, setSelectedIds] = useState([]); const [statusFilter, setStatusFilter] = useState(initialFilters.statusFilter); + const [recipientFilter, setRecipientFilter] = useState(initialFilters.recipientFilter); + const [entityFilter, setEntityFilter] = useState(initialFilters.entityFilter); + const [ventilationFilter, setVentilationFilter] = useState(initialFilters.ventilationFilter); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [invoiceToDelete, setInvoiceToDelete] = useState(null); const [addDialogOpen, setAddDialogOpen] = useState(false); @@ -143,6 +146,9 @@ export default function InvoicesBAP() { statusFilter, selectedYear, selectedMonth, + recipientFilter, + entityFilter, + ventilationFilter, sortField, sortDir, })); @@ -169,6 +175,14 @@ export default function InvoicesBAP() { const { data: allInvoices, isLoading } = trpc.invoices.list.useQuery(); // Filter for BAP invoices only (Abonnement = NON, isSubscription = 0) const invoices = allInvoices?.filter(inv => inv.isSubscription === 0); + const uniqueRecipients = useMemo( + () => Array.from(new Set((invoices || []).map(inv => (inv as any).recipientName).filter(Boolean))).sort(), + [invoices], + ); + const uniqueVentilations = useMemo( + () => Array.from(new Set((invoices || []).map(inv => (inv as any).ventilationComptable).filter(Boolean))).sort(), + [invoices], + ); // IDs des factures déjà validées BAP (pour charger leurs pdfUrl depuis bapHistory) // Stabilisé avec useMemo pour éviter les re-renders infinis (anti-pattern tRPC) const validatedInvoiceIds = useMemo( @@ -440,6 +454,27 @@ export default function InvoicesBAP() { if (statusFilter === "bap_pending" && inv.bapValidated === 1) return false; if (statusFilter === "to_complete" && !isToComplete(inv)) return false; } + + // Même convention que la page Factures : valeur vide et entités déduites du service. + if (recipientFilter !== "all") { + if (recipientFilter === "__empty__") { + if ((inv as any).recipientName) return false; + } else if ((inv as any).recipientName !== recipientFilter) { + return false; + } + } + if (entityFilter !== "all") { + const service = ((inv as any).serviceConcerne || "").trim().toUpperCase(); + if (entityFilter === "santinova" && service !== "DSI SANTINOVA") return false; + if (entityFilter === "itinova" && service === "DSI SANTINOVA") return false; + } + if (ventilationFilter !== "all") { + if (ventilationFilter === "__empty__") { + if ((inv as any).ventilationComptable) return false; + } else if ((inv as any).ventilationComptable !== ventilationFilter) { + return false; + } + } return true; }); @@ -778,6 +813,40 @@ export default function InvoicesBAP() { ))} + + + {(selectedYear !== "all" || selectedMonth !== "all") && (