Checkpoint: Ajout de la page Factures abonnements, accessible après Factures BAP, partageant les recherches, filtres et actions de la page Factures. Filtres Année et Mois ajoutés aux deux listes, avec règle date de facture puis réception.
This commit is contained in:
@@ -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 <Invoices scope="subscriptions" />;
|
||||
}
|
||||
|
||||
function AllInvoices() {
|
||||
return <Invoices />;
|
||||
}
|
||||
|
||||
function RouteFallback() {
|
||||
return <div className="min-h-screen bg-background" aria-busy="true" aria-label="Chargement" />;
|
||||
}
|
||||
@@ -38,8 +46,9 @@ function Router() {
|
||||
<Route path="/login" component={Login} />
|
||||
<Route path="/dashboard" component={Dashboard} />
|
||||
<Route path="/upload" component={Upload} />
|
||||
<Route path="/invoices" component={Invoices} />
|
||||
<Route path="/invoices" component={AllInvoices} />
|
||||
<Route path="/invoices-bap" component={InvoicesBAP} />
|
||||
<Route path="/invoices-subscriptions" component={SubscriptionInvoices} />
|
||||
<Route path="/invoices/:id" component={InvoiceDetail} />
|
||||
<Route path="/settings" component={Settings} />
|
||||
<Route path="/import-settings" component={ImportSettings} />
|
||||
|
||||
@@ -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" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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<SortField>("createdAt");
|
||||
@@ -52,9 +63,11 @@ export default function Invoices() {
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const [recipientFilter, setRecipientFilter] = useState<string>("all");
|
||||
const [subscriptionFilter, setSubscriptionFilter] = useState<string>("all"); // all | yes | no
|
||||
const [subscriptionFilter, setSubscriptionFilter] = useState<string>(isSubscriptionPage ? "yes" : "all"); // all | yes | no
|
||||
const [entityFilter, setEntityFilter] = useState<string>("all"); // all | santinova | itinova
|
||||
const [ventilationFilter, setVentilationFilter] = useState<string>("all");
|
||||
const [selectedYear, setSelectedYear] = useState<string>(String(currentYear));
|
||||
const [selectedMonth, setSelectedMonth] = useState<string>("all");
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [invoiceToDelete, setInvoiceToDelete] = useState<number | null>(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" ? <ArrowUp className="w-3 h-3 ml-1" /> : <ArrowDown className="w-3 h-3 ml-1" />;
|
||||
};
|
||||
|
||||
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() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Factures</h1>
|
||||
<p className="text-gray-500 mt-1">Gérez toutes vos factures importées</p>
|
||||
<h1 className="text-3xl font-bold">{isSubscriptionPage ? "Factures abonnements" : "Factures"}</h1>
|
||||
<p className="text-gray-500 mt-1">
|
||||
{isSubscriptionPage ? "Gérez les factures identifiées comme abonnements" : "Gérez toutes vos factures importées"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
@@ -456,9 +472,30 @@ export default function Invoices() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Ligne 2 : Filtres destinataire, abonnement, entité, ventilation */}
|
||||
{/* Ligne 2 : Filtres de période, destinataire, abonnement, entité, ventilation */}
|
||||
<div className="flex gap-2 items-center flex-wrap mb-3">
|
||||
<Filter className="w-4 h-4 text-blue-400 shrink-0" />
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-blue-600">
|
||||
<CalendarDays className="w-4 h-4" />
|
||||
Période :
|
||||
</div>
|
||||
<Select value={selectedYear} onValueChange={(value) => { setSelectedYear(value); if (value === "all") setSelectedMonth("all"); }}>
|
||||
<SelectTrigger className="w-28 h-8 bg-white border-blue-200 text-sm"><SelectValue placeholder="Année" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Toute année</SelectItem>
|
||||
{Array.from({ length: 5 }, (_, index) => currentYear - index).map(year => <SelectItem key={year} value={String(year)}>{year}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={selectedMonth} onValueChange={setSelectedMonth} disabled={selectedYear === "all"}>
|
||||
<SelectTrigger className="w-36 h-8 bg-white border-blue-200 text-sm"><SelectValue placeholder="Mois" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Tous les mois</SelectItem>
|
||||
{MONTHS.map(([value, label]) => <SelectItem key={value} value={value}>{label}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{(selectedYear !== String(currentYear) || selectedMonth !== "all") && (
|
||||
<button onClick={() => { setSelectedYear(String(currentYear)); setSelectedMonth("all"); }} className="text-xs text-blue-600 hover:text-blue-800 underline">Réinitialiser</button>
|
||||
)}
|
||||
<Select value={recipientFilter} onValueChange={setRecipientFilter}>
|
||||
<SelectTrigger className="w-[180px] bg-white border-blue-200 text-sm">
|
||||
<SelectValue placeholder="Destinataire" />
|
||||
@@ -471,7 +508,7 @@ export default function Invoices() {
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={subscriptionFilter} onValueChange={setSubscriptionFilter}>
|
||||
<Select value={isSubscriptionPage ? "yes" : subscriptionFilter} onValueChange={setSubscriptionFilter} disabled={isSubscriptionPage}>
|
||||
<SelectTrigger className="w-[170px] bg-white border-blue-200 text-sm">
|
||||
<SelectValue placeholder="Abonnement" />
|
||||
</SelectTrigger>
|
||||
|
||||
Reference in New Issue
Block a user