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") && (