diff --git a/client/src/App.tsx b/client/src/App.tsx index 17a0bc3..2f4325a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -27,6 +27,14 @@ const LearningSettings = lazy(() => import("./pages/LearningSettings")); const VentilationFreePro = lazy(() => import("./pages/VentilationFreePro")); const WebImportSources = lazy(() => import("./pages/WebImportSources")); +function SubscriptionInvoices() { + return ; +} + +function AllInvoices() { + return ; +} + function RouteFallback() { return
; } @@ -38,8 +46,9 @@ function Router() { - + + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 643a5db..a9fa311 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -53,6 +53,7 @@ const menuStructure: MenuItem[] = [ { icon: Upload, label: "Import", path: "/upload" }, { icon: FileText, label: "Factures", path: "/invoices" }, { icon: FileText, label: "Factures BAP", path: "/invoices-bap" }, + { icon: FileText, label: "Factures abonnements", path: "/invoices-subscriptions" }, ], }, { diff --git a/client/src/pages/Invoices.tsx b/client/src/pages/Invoices.tsx index 0089aa4..db06cda 100644 --- a/client/src/pages/Invoices.tsx +++ b/client/src/pages/Invoices.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import DashboardLayout from "@/components/DashboardLayout"; import { useAuth } from "@/_core/hooks/useAuth"; import { Card, CardContent } from "@/components/ui/card"; @@ -34,17 +34,28 @@ import { } from "@/components/ui/table"; import { trpc } from "@/lib/trpc"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, Filter, LayoutList, LayoutGrid, ArrowUpDown, ArrowUp, ArrowDown } from "lucide-react"; +import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, Filter, LayoutList, LayoutGrid, ArrowUpDown, ArrowUp, ArrowDown, CalendarDays } from "lucide-react"; import * as XLSX from 'xlsx'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { toast } from "sonner"; import { useLocation } from "wouter"; +import { matchesInvoicePeriod } from "@shared/invoicePeriod"; type SortField = "invoiceDate" | "createdAt"; type SortDir = "asc" | "desc"; -export default function Invoices() { +type InvoiceScope = "all" | "subscriptions"; + +const MONTHS = [ + ["01", "Janvier"], ["02", "Février"], ["03", "Mars"], ["04", "Avril"], + ["05", "Mai"], ["06", "Juin"], ["07", "Août"], ["08", "Septembre"], + ["09", "Septembre"], ["10", "Octobre"], ["11", "Novembre"], ["12", "Décembre"], +] as const; + +export default function Invoices({ scope = "all" }: { scope?: InvoiceScope }) { const [, setLocation] = useLocation(); + const currentYear = new Date().getFullYear(); + const isSubscriptionPage = scope === "subscriptions"; const [searchQuery, setSearchQuery] = useState(""); const [compactMode, setCompactMode] = useState(true); const [sortField, setSortField] = useState("createdAt"); @@ -52,9 +63,11 @@ export default function Invoices() { const [selectedIds, setSelectedIds] = useState([]); const [statusFilter, setStatusFilter] = useState("all"); const [recipientFilter, setRecipientFilter] = useState("all"); - const [subscriptionFilter, setSubscriptionFilter] = useState("all"); // all | yes | no + const [subscriptionFilter, setSubscriptionFilter] = useState(isSubscriptionPage ? "yes" : "all"); // all | yes | no const [entityFilter, setEntityFilter] = useState("all"); // all | santinova | itinova const [ventilationFilter, setVentilationFilter] = useState("all"); + const [selectedYear, setSelectedYear] = useState(String(currentYear)); + const [selectedMonth, setSelectedMonth] = useState("all"); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [invoiceToDelete, setInvoiceToDelete] = useState(null); const [addDialogOpen, setAddDialogOpen] = useState(false); @@ -194,17 +207,20 @@ export default function Invoices() { }; // Collect unique recipients for the filter dropdown - const uniqueRecipients = Array.from( - new Set( - (invoices || []).map(inv => (inv as any).recipientName).filter(Boolean) - ) - ).sort(); + const scopedInvoices = useMemo( + () => (invoices || []).filter(inv => !isSubscriptionPage || inv.isSubscription === 1), + [invoices, isSubscriptionPage], + ); - const uniqueVentilations = Array.from( - new Set( - (invoices || []).map(inv => (inv as any).ventilationComptable).filter(Boolean) - ) - ).sort(); + const uniqueRecipients = useMemo( + () => Array.from(new Set(scopedInvoices.map(inv => (inv as any).recipientName).filter(Boolean))).sort(), + [scopedInvoices], + ); + + const uniqueVentilations = useMemo( + () => Array.from(new Set(scopedInvoices.map(inv => (inv as any).ventilationComptable).filter(Boolean))).sort(), + [scopedInvoices], + ); const handleSort = (field: SortField) => { if (sortField === field) { @@ -220,7 +236,7 @@ export default function Invoices() { return sortDir === "asc" ? : ; }; - const filteredInvoices = invoices?.filter((inv) => { + const filteredInvoices = scopedInvoices.filter((inv) => { // Filter by search query if (searchQuery) { const query = searchQuery.toLowerCase(); @@ -230,6 +246,9 @@ export default function Invoices() { if (!matchesSearch) return false; } + // Même convention que Factures BAP : date de facture, sinon réception. + if (!matchesInvoicePeriod(inv, selectedYear, selectedMonth)) return false; + // Filter by export status if (statusFilter !== "all") { if (statusFilter === "exported" && inv.exportStatus !== "exported") return false; @@ -268,22 +287,17 @@ export default function Invoices() { return true; }); + const invoicesInPeriod = useMemo( + () => scopedInvoices.filter(inv => matchesInvoicePeriod(inv, selectedYear, selectedMonth)), + [scopedInvoices, selectedYear, selectedMonth], + ); + const statusCounts = { - all: invoices?.length || 0, - exported: invoices?.filter(inv => inv.exportStatus === "exported").length || 0, - not_exported: invoices?.filter(inv => inv.exportStatus === "not_exported").length || 0, - export_error: invoices?.filter(inv => inv.exportStatus === "export_error").length || 0, + all: invoicesInPeriod.length, + exported: invoicesInPeriod.filter(inv => inv.exportStatus === "exported").length, + not_exported: invoicesInPeriod.filter(inv => inv.exportStatus === "not_exported").length, + export_error: invoicesInPeriod.filter(inv => inv.exportStatus === "export_error").length, }; - - // Old filter logic (to be removed) - const _oldFilteredInvoices = invoices?.filter((inv) => { - if (!searchQuery) return true; - const query = searchQuery.toLowerCase(); - return ( - inv.supplierName?.toLowerCase().includes(query) || - inv.invoiceNumber?.toLowerCase().includes(query) - ); - }); const handleSelectAll = (checked: boolean) => { if (checked) { @@ -370,8 +384,10 @@ export default function Invoices() {
-

Factures

-

Gérez toutes vos factures importées

+

{isSubscriptionPage ? "Factures abonnements" : "Factures"}

+

+ {isSubscriptionPage ? "Gérez les factures identifiées comme abonnements" : "Gérez toutes vos factures importées"} +

- {/* Ligne 2 : Filtres destinataire, abonnement, entité, ventilation */} + {/* Ligne 2 : Filtres de période, destinataire, abonnement, entité, ventilation */}
+
+ + Période : +
+ + + {(selectedYear !== String(currentYear) || selectedMonth !== "all") && ( + + )} - diff --git a/server/invoicePeriod.test.ts b/server/invoicePeriod.test.ts new file mode 100644 index 0000000..f81a244 --- /dev/null +++ b/server/invoicePeriod.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from "vitest"; +import { getInvoicePeriodDate, matchesInvoicePeriod } from "@shared/invoicePeriod"; + +describe("filtres de période des factures", () => { + it("privilégie la date de facture et accepte le mois demandé", () => { + const invoice = { invoiceDate: "2026-05-11T00:00:00.000Z", createdAt: "2026-06-02T00:00:00.000Z" }; + expect(getInvoicePeriodDate(invoice)?.getFullYear()).toBe(2026); + expect(matchesInvoicePeriod(invoice, "2026", "05")).toBe(true); + expect(matchesInvoicePeriod(invoice, "2026", "06")).toBe(false); + }); + + it("utilise la date de réception lorsque la date de facture est absente", () => { + const invoice = { invoiceDate: null, createdAt: "2025-01-31T00:00:00.000Z" }; + expect(matchesInvoicePeriod(invoice, "2025", "01")).toBe(true); + expect(matchesInvoicePeriod(invoice, "2026", "all")).toBe(false); + }); +}); diff --git a/shared/invoicePeriod.ts b/shared/invoicePeriod.ts new file mode 100644 index 0000000..998a473 --- /dev/null +++ b/shared/invoicePeriod.ts @@ -0,0 +1,27 @@ +/** + * Date métier utilisée pour les listes de factures : date de facture si elle + * est connue, sinon date de réception. Cette règle est partagée avec BAP. + */ +export function getInvoicePeriodDate(invoice: { + invoiceDate?: Date | string | number | null; + createdAt?: Date | string | number | null; +}): Date | null { + const value = invoice.invoiceDate ?? invoice.createdAt; + if (!value) return null; + + const date = value instanceof Date ? value : new Date(value); + return Number.isNaN(date.getTime()) ? null : date; +} + +/** Indique si une facture correspond au filtre Année/Mois sélectionné. */ +export function matchesInvoicePeriod( + invoice: Parameters[0], + year: string, + month: string, +): boolean { + if (year === "all") return true; + + const date = getInvoicePeriodDate(invoice); + if (!date || date.getFullYear() !== Number(year)) return false; + return month === "all" || date.getMonth() + 1 === Number(month); +} diff --git a/todo.md b/todo.md index f26f830..bb44e79 100644 --- a/todo.md +++ b/todo.md @@ -797,3 +797,13 @@ ## Rétablissement du serveur de recette - [x] Redémarrer le serveur de recette autorisé par l’utilisateur - [x] Rétablir le conteneur applicatif et terminer le déploiement BAP à 90 % + +## Factures abonnements et filtres de période +- [x] Analyser les listes Factures, Factures BAP et le menu Facturation +- [x] Ajouter les filtres Année et Mois à la page Factures +- [x] Créer la page Factures abonnements affichant uniquement les abonnements +- [x] Reprendre les recherches, filtres et actions de la page Factures +- [x] Ajouter l’entrée Factures abonnements après Factures BAP dans le menu +- [x] Ajouter les tests et valider TypeScript, build et affichage en sandbox +- [ ] Déployer et vérifier le correctif en recette +- [ ] Déployer et vérifier le correctif en production