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:
Manus
2026-01-08 10:19:05 -05:00
parent d24f7c5569
commit 63c2afea1f
7 changed files with 582 additions and 4 deletions

View File

@@ -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>