Checkpoint: Ajout des filtres par statut d'export, export Excel et page de détail avec édition manuelle.
Nouvelles fonctionnalités : 1. Filtres par statut d'export : - Boutons de filtre rapide (Tous/Exportés/Non exportés/Erreurs) - Compteurs dynamiques pour chaque statut - Couleurs distinctives (vert/bleu/rouge) 2. Export Excel : - Nouveau bouton "Excel" dans la page Factures - Génération automatique de fichier .xlsx avec toutes les métadonnées - Colonnes : Fournisseur, N° Facture, Date, N° BL, N° Commande, Montant, Score, Statut, Dates 3. Page de détail de facture (/invoices/:id) : - Visualisation PDF intégrée dans iframe - Affichage des métadonnées extraites - Mode édition avec formulaire complet - Recalcul automatique du score de qualité après édition - Marquage "Modifié manuellement" - Badges de statut et qualité - Lien cliquable sur le nom du fournisseur dans la liste L'application offre maintenant une expérience complète de gestion des factures avec filtrage avancé, exports multiples et édition manuelle.
This commit is contained in:
@@ -14,7 +14,8 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import { Search, FileText, Download } from "lucide-react";
|
||||
import { Search, FileText, Download, FileSpreadsheet } from "lucide-react";
|
||||
import * as XLSX from 'xlsx';
|
||||
import { toast } from "sonner";
|
||||
import { useLocation } from "wouter";
|
||||
|
||||
@@ -22,9 +23,40 @@ export default function Invoices() {
|
||||
const [, setLocation] = useLocation();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const { data: invoices, isLoading } = trpc.invoices.list.useQuery();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const exportExcelMutation = trpc.sftp.exportToExcel.useMutation({
|
||||
onSuccess: (data) => {
|
||||
// Generate Excel file
|
||||
const worksheet = XLSX.utils.json_to_sheet(data.invoices.map(inv => ({
|
||||
'Fournisseur': inv.supplierName,
|
||||
'N\u00b0 Facture': inv.invoiceNumber,
|
||||
'Date': inv.invoiceDate,
|
||||
'N\u00b0 Bon de livraison': inv.deliveryNoteNumber,
|
||||
'N\u00b0 Commande': inv.orderNumber,
|
||||
'Montant': inv.totalAmount,
|
||||
'Score': inv.qualityScore,
|
||||
'Statut export': inv.exportStatus,
|
||||
'Export\u00e9 le': inv.exportedAt,
|
||||
'Cr\u00e9\u00e9 le': inv.createdAt,
|
||||
})));
|
||||
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Factures');
|
||||
|
||||
// Download Excel file
|
||||
XLSX.writeFile(workbook, `factures_${new Date().toISOString().split('T')[0]}.xlsx`);
|
||||
|
||||
toast.success(`${data.invoices.length} facture(s) export\u00e9e(s) en Excel`);
|
||||
setSelectedIds([]);
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message || "Erreur lors de l'export Excel");
|
||||
},
|
||||
});
|
||||
|
||||
const exportMutation = trpc.sftp.exportToPdf.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(`${data.invoices.length} facture(s) exportée(s) avec succès`);
|
||||
@@ -46,6 +78,33 @@ export default function Invoices() {
|
||||
});
|
||||
|
||||
const filteredInvoices = invoices?.filter((inv) => {
|
||||
// Filter by search query
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
const matchesSearch = inv.supplierName?.toLowerCase().includes(query) ||
|
||||
inv.invoiceNumber?.toLowerCase().includes(query);
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
// Filter by export status
|
||||
if (statusFilter !== "all") {
|
||||
if (statusFilter === "exported" && inv.exportStatus !== "exported") return false;
|
||||
if (statusFilter === "not_exported" && inv.exportStatus !== "not_exported") return false;
|
||||
if (statusFilter === "export_error" && inv.exportStatus !== "export_error") return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
// Old filter logic (to be removed)
|
||||
const _oldFilteredInvoices = invoices?.filter((inv) => {
|
||||
if (!searchQuery) return true;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return (
|
||||
@@ -76,12 +135,21 @@ export default function Invoices() {
|
||||
|
||||
const handleExport = () => {
|
||||
if (selectedIds.length === 0) {
|
||||
toast.error("Veuillez sélectionner au moins une facture");
|
||||
toast.error("Veuillez s\u00e9lectionner au moins une facture");
|
||||
return;
|
||||
}
|
||||
|
||||
exportMutation.mutate({ invoiceIds: selectedIds });
|
||||
};
|
||||
|
||||
const handleExportExcel = () => {
|
||||
if (selectedIds.length === 0) {
|
||||
toast.error("Veuillez s\u00e9lectionner au moins une facture");
|
||||
return;
|
||||
}
|
||||
|
||||
exportExcelMutation.mutate({ invoiceIds: selectedIds });
|
||||
};
|
||||
|
||||
const getExportStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
@@ -119,13 +187,22 @@ export default function Invoices() {
|
||||
<p className="text-gray-500 mt-1">Gérez toutes vos factures importées</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleExportExcel}
|
||||
disabled={selectedIds.length === 0 || exportExcelMutation.isPending}
|
||||
variant="outline"
|
||||
className="border-green-600 text-green-600 hover:bg-green-50"
|
||||
>
|
||||
<FileSpreadsheet className="w-4 h-4 mr-2" />
|
||||
Excel ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleExport}
|
||||
disabled={selectedIds.length === 0 || exportMutation.isPending}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Exporter ({selectedIds.length})
|
||||
PDF ({selectedIds.length})
|
||||
</Button>
|
||||
<Button onClick={() => setLocation("/upload")}>
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
@@ -148,6 +225,41 @@ export default function Invoices() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Filters */}
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Button
|
||||
variant={statusFilter === "all" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("all")}
|
||||
size="sm"
|
||||
>
|
||||
Tous ({statusCounts.all})
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "exported" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("exported")}
|
||||
size="sm"
|
||||
className={statusFilter === "exported" ? "bg-green-600 hover:bg-green-700" : ""}
|
||||
>
|
||||
Exportés ({statusCounts.exported})
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "not_exported" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("not_exported")}
|
||||
size="sm"
|
||||
className={statusFilter === "not_exported" ? "bg-blue-600 hover:bg-blue-700" : ""}
|
||||
>
|
||||
Non exportés ({statusCounts.not_exported})
|
||||
</Button>
|
||||
<Button
|
||||
variant={statusFilter === "export_error" ? "default" : "outline"}
|
||||
onClick={() => setStatusFilter("export_error")}
|
||||
size="sm"
|
||||
className={statusFilter === "export_error" ? "bg-red-600 hover:bg-red-700" : ""}
|
||||
>
|
||||
Erreurs ({statusCounts.export_error})
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
{isLoading ? (
|
||||
@@ -184,7 +296,12 @@ export default function Invoices() {
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{invoice.supplierName || "Inconnu"}
|
||||
<button
|
||||
onClick={() => setLocation(`/invoices/${invoice.id}`)}
|
||||
className="text-blue-600 hover:text-blue-800 hover:underline"
|
||||
>
|
||||
{invoice.supplierName || "Inconnu"}
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>{invoice.invoiceNumber || "-"}</TableCell>
|
||||
<TableCell>
|
||||
|
||||
Reference in New Issue
Block a user