Checkpoint: Création de la page Factures BAP et restructuration de l'interface

Nouvelles fonctionnalités :
 Nouvelle page "Factures BAP" qui affiche uniquement les factures avec Abonnement = NON (isSubscription = 0)
 La page Factures BAP inclut les colonnes Type d'achat et Ventilation comptable avec édition directe
 Les colonnes Type d'achat et Ventilation ont été supprimées de la page Factures principale
 Menu "Factures BAP" ajouté dans la navigation du DashboardLayout
 Route /invoices-bap configurée dans App.tsx

Architecture :
- client/src/pages/InvoicesBAP.tsx : Nouvelle page pour les factures BAP (non-abonnement)
- client/src/pages/Invoices.tsx : Page principale modifiée (colonnes Type d'achat et Ventilation supprimées)
- client/src/App.tsx : Route /invoices-bap ajoutée
- client/src/components/DashboardLayout.tsx : Menu "Factures BAP" ajouté

Fonctionnement :
1. La page "Factures" affiche toutes les factures avec les colonnes : Fournisseur, N° Facture, Date, Montant, Service, Abonnement, Score, Statut, Actions
2. La page "Factures BAP" affiche uniquement les factures non-abonnement (Abonnement = NON) avec les colonnes supplémentaires : Type d'achat et Ventilation comptable
3. Les deux pages conservent toutes les fonctionnalités d'édition directe, export, suppression, etc.
4. Le filtrage est automatique basé sur le champ isSubscription (0 = NON, 1 = OUI)

Cette restructuration permet une meilleure organisation des factures selon leur nature (abonnement ou BAP).
This commit is contained in:
Manus
2026-02-11 11:10:23 -05:00
parent 7cb2fee76e
commit e11d24cb94
5 changed files with 702 additions and 43 deletions

View File

@@ -9,6 +9,7 @@ import Login from "./pages/Login";
import Dashboard from "./pages/Dashboard";
import Upload from "./pages/Upload";
import Invoices from "./pages/Invoices";
import InvoicesBAP from "./pages/InvoicesBAP";
import InvoiceDetail from "./pages/InvoiceDetail";
import Settings from "./pages/Settings";
import ImportSettings from "./pages/ImportSettings";
@@ -24,6 +25,7 @@ function Router() {
<Route path="/dashboard" component={Dashboard} />
<Route path="/upload" component={Upload} />
<Route path="/invoices" component={Invoices} />
<Route path="/invoices-bap" component={InvoicesBAP} />
<Route path="/invoices/:id" component={InvoiceDetail} />
<Route path="/settings" component={Settings} />
<Route path="/import-settings" component={ImportSettings} />

View File

@@ -31,6 +31,7 @@ const menuItems = [
{ icon: LayoutDashboard, label: "Tableau de bord", path: "/dashboard" },
{ icon: Upload, label: "Importer", path: "/upload" },
{ icon: FileText, label: "Factures", path: "/invoices" },
{ icon: FileText, label: "Factures BAP", path: "/invoices-bap" },
{ icon: History, label: "Historique", path: "/history" },
{ icon: Settings, label: "Paramètres", path: "/settings" },
{ icon: Download, label: "Paramètres de réception", path: "/import-settings" },

View File

@@ -407,8 +407,6 @@ export default function Invoices() {
<TableHead>Date</TableHead>
<TableHead>Montant</TableHead>
<TableHead>Service</TableHead>
<TableHead>Type achat</TableHead>
<TableHead>Ventilation</TableHead>
<TableHead>Abonnement</TableHead>
<TableHead>Score</TableHead>
<TableHead>Statut</TableHead>
@@ -469,47 +467,6 @@ export default function Invoices() {
<option value="__ADD_NEW__" className="font-semibold text-blue-600"> Ajouter...</option>
</select>
</TableCell>
<TableCell className="text-sm">
<select
value={invoice.typeAchat || ""}
onChange={(e) => {
updateFieldMutation.mutate({
id: invoice.id,
data: { typeAchat: e.target.value as "CAPEX" | "OPEX" | undefined },
});
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">-</option>
<option value="CAPEX">CAPEX</option>
<option value="OPEX">OPEX</option>
</select>
</TableCell>
<TableCell className="text-sm">
<select
value={invoice.ventilationComptable || ""}
onChange={(e) => {
if (e.target.value === "__ADD_NEW__") {
setPendingInvoiceId(invoice.id);
setAddDialogType("ventilation");
setAddDialogOpen(true);
e.target.value = invoice.ventilationComptable || "";
} else {
updateFieldMutation.mutate({
id: invoice.id,
data: { ventilationComptable: e.target.value || undefined },
});
}
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">-</option>
{allocations?.map((alloc) => (
<option key={alloc.id} value={alloc.name}>{alloc.name}</option>
))}
<option value="__ADD_NEW__" className="font-semibold text-blue-600"> Ajouter...</option>
</select>
</TableCell>
<TableCell className="text-sm">
<select
value={invoice.isSubscription ? "OUI" : "NON"}

View File

@@ -0,0 +1,682 @@
import { useState } from "react";
import DashboardLayout from "@/components/DashboardLayout";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Checkbox } from "@/components/ui/checkbox";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { trpc } from "@/lib/trpc";
import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash } from "lucide-react";
import * as XLSX from 'xlsx';
import { toast } from "sonner";
import { useLocation } from "wouter";
export default function InvoicesBAP() {
const [, setLocation] = useLocation();
const [searchQuery, setSearchQuery] = useState("");
const [selectedIds, setSelectedIds] = useState<number[]>([]);
const [statusFilter, setStatusFilter] = useState<string>("all");
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [invoiceToDelete, setInvoiceToDelete] = useState<number | null>(null);
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [addDialogType, setAddDialogType] = useState<"service" | "ventilation" | null>(null);
const [newItemName, setNewItemName] = useState("");
const [pendingInvoiceId, setPendingInvoiceId] = useState<number | null>(null);
const [textDialogOpen, setTextDialogOpen] = useState(false);
const [selectedInvoiceText, setSelectedInvoiceText] = useState<string | null>(null);
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 { data: departments } = trpc.departments.getByUser.useQuery();
const { data: allocations } = trpc.accountingAllocations.getByUser.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`);
// Open PDFs in new tabs
data.invoices.forEach((inv, index) => {
// Add a small delay between each window to avoid popup blocking
setTimeout(() => {
window.open(inv.fileUrl, '_blank');
}, index * 100);
});
setSelectedIds([]);
utils.invoices.list.invalidate();
},
onError: (error) => {
toast.error(error.message || "Erreur lors de l'export");
},
});
const deleteMutation = trpc.invoices.delete.useMutation({
onSuccess: () => {
toast.success("Facture supprimée avec succès");
utils.invoices.list.invalidate();
setDeleteDialogOpen(false);
setInvoiceToDelete(null);
},
onError: (error) => {
toast.error(error.message || "Erreur lors de la suppression");
},
});
const updateFieldMutation = trpc.invoices.update.useMutation({
onSuccess: () => {
toast.success("Facture mise à jour");
utils.invoices.list.invalidate();
},
onError: (error) => {
toast.error(error.message || "Erreur lors de la mise à jour");
},
});
const createDepartmentMutation = trpc.departments.create.useMutation({
onSuccess: (newDept) => {
toast.success("Service ajouté avec succès");
utils.departments.getByUser.invalidate();
// Update the invoice with the new department
if (pendingInvoiceId) {
updateFieldMutation.mutate({
id: pendingInvoiceId,
data: { serviceConcerne: newDept.name },
});
}
setAddDialogOpen(false);
setNewItemName("");
setPendingInvoiceId(null);
},
onError: (error) => {
toast.error(error.message || "Erreur lors de l'ajout");
},
});
const createAllocationMutation = trpc.accountingAllocations.create.useMutation({
onSuccess: (newAlloc) => {
toast.success("Ventilation ajoutée avec succès");
utils.accountingAllocations.getByUser.invalidate();
// Update the invoice with the new allocation
if (pendingInvoiceId) {
updateFieldMutation.mutate({
id: pendingInvoiceId,
data: { ventilationComptable: newAlloc.name },
});
}
setAddDialogOpen(false);
setNewItemName("");
setPendingInvoiceId(null);
},
onError: (error) => {
toast.error(error.message || "Erreur lors de l'ajout");
},
});
const handleAddNewItem = () => {
if (!newItemName.trim()) {
toast.error("Le nom ne peut pas être vide");
return;
}
if (addDialogType === "service") {
createDepartmentMutation.mutate({ name: newItemName.trim() });
} else if (addDialogType === "ventilation") {
createAllocationMutation.mutate({ name: newItemName.trim() });
}
};
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 (
inv.supplierName?.toLowerCase().includes(query) ||
inv.invoiceNumber?.toLowerCase().includes(query)
);
});
const handleSelectAll = (checked: boolean) => {
if (checked) {
// Select all invoices (no restriction)
const allIds = (filteredInvoices || []).map(inv => inv.id);
setSelectedIds(allIds);
} else {
setSelectedIds([]);
}
};
const handleSelectOne = (id: number, checked: boolean) => {
if (checked) {
setSelectedIds([...selectedIds, id]);
} else {
setSelectedIds(selectedIds.filter(selectedId => selectedId !== id));
}
};
const handleExport = () => {
if (selectedIds.length === 0) {
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) {
case "exported":
return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">Exporté</Badge>;
case "not_exported":
return <Badge className="bg-blue-100 text-blue-800 hover:bg-blue-100">Non exporté</Badge>;
case "export_error":
return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">Erreur export</Badge>;
default:
return <Badge variant="outline">{status}</Badge>;
}
};
const getQualityBadge = (score: number | null) => {
if (score === null) return <Badge variant="outline">-</Badge>;
if (score === 100) return <Badge className="bg-green-100 text-green-800 hover:bg-green-100">{score}%</Badge>;
if (score >= 80) return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100">{score}%</Badge>;
return <Badge className="bg-red-100 text-red-800 hover:bg-red-100">{score}%</Badge>;
};
const isEligibleForExport = (invoice: any) => {
return (invoice.qualityScore || 0) === 100;
};
const hasOnlyEligibleSelected = selectedIds.length > 0 && selectedIds.every(id => {
const invoice = invoices?.find(inv => inv.id === id);
return invoice && isEligibleForExport(invoice);
});
const allEligibleSelected = (filteredInvoices?.length || 0) > 0 &&
(filteredInvoices || []).filter(inv => isEligibleForExport(inv)).every(inv => selectedIds.includes(inv.id));
return (
<DashboardLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Factures BAP</h1>
<p className="text-gray-500 mt-1">Factures non-abonnement (Abonnement = NON)</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 || !hasOnlyEligibleSelected || exportMutation.isPending}
className="bg-blue-600 hover:bg-blue-700"
title={!hasOnlyEligibleSelected && selectedIds.length > 0 ? "Seules les factures avec un score de 100% peuvent être exportées" : ""}
>
<Download className="w-4 h-4 mr-2" />
PDF ({selectedIds.length})
</Button>
<Button
onClick={async () => {
if (confirm(`Voulez-vous vraiment supprimer ${selectedIds.length} facture(s) ?`)) {
try {
for (const id of selectedIds) {
await utils.client.invoices.delete.mutate({ id });
}
toast.success(`${selectedIds.length} facture(s) supprimée(s)`);
setSelectedIds([]);
utils.invoices.list.invalidate();
} catch (error: any) {
toast.error(error.message || "Erreur lors de la suppression");
}
}
}}
disabled={selectedIds.length === 0}
variant="destructive"
>
<Trash2 className="w-4 h-4 mr-2" />
Supprimer ({selectedIds.length})
</Button>
<Button onClick={() => setLocation("/upload")}>
<FileText className="w-4 h-4 mr-2" />
Importer
</Button>
</div>
</div>
<Card>
<CardContent className="pt-6">
{/* Search */}
<div className="mb-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="Rechercher par fournisseur ou numéro..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10"
/>
</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 ? (
<div className="text-center py-8 text-gray-500">Chargement...</div>
) : filteredInvoices && filteredInvoices.length > 0 ? (
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={allEligibleSelected}
onCheckedChange={handleSelectAll}
/>
</TableHead>
<TableHead>Fournisseur</TableHead>
<TableHead>N° Facture</TableHead>
<TableHead>Date</TableHead>
<TableHead>Montant</TableHead>
<TableHead>Service</TableHead>
<TableHead>Type achat</TableHead>
<TableHead>Ventilation</TableHead>
<TableHead>Abonnement</TableHead>
<TableHead>Score</TableHead>
<TableHead>Statut</TableHead>
<TableHead className="w-32">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredInvoices.map((invoice) => {
return (
<TableRow key={invoice.id}>
<TableCell>
<Checkbox
checked={selectedIds.includes(invoice.id)}
onCheckedChange={(checked) => handleSelectOne(invoice.id, checked as boolean)}
/>
</TableCell>
<TableCell className="font-medium">
<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>
{invoice.invoiceDate
? new Date(invoice.invoiceDate).toLocaleDateString("fr-FR")
: "-"}
</TableCell>
<TableCell>
{invoice.totalAmount
? `${parseFloat(invoice.totalAmount as string).toFixed(2)}`
: "-"}
</TableCell>
<TableCell className="text-sm">
<select
value={invoice.serviceConcerne || ""}
onChange={(e) => {
if (e.target.value === "__ADD_NEW__") {
setPendingInvoiceId(invoice.id);
setAddDialogType("service");
setAddDialogOpen(true);
e.target.value = invoice.serviceConcerne || "";
} else {
updateFieldMutation.mutate({
id: invoice.id,
data: { serviceConcerne: e.target.value || undefined },
});
}
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">-</option>
{departments?.map((dept) => (
<option key={dept.id} value={dept.name}>{dept.name}</option>
))}
<option value="__ADD_NEW__" className="font-semibold text-blue-600"> Ajouter...</option>
</select>
</TableCell>
<TableCell className="text-sm">
<select
value={invoice.typeAchat || ""}
onChange={(e) => {
updateFieldMutation.mutate({
id: invoice.id,
data: { typeAchat: e.target.value as "CAPEX" | "OPEX" | undefined },
});
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">-</option>
<option value="CAPEX">CAPEX</option>
<option value="OPEX">OPEX</option>
</select>
</TableCell>
<TableCell className="text-sm">
<select
value={invoice.ventilationComptable || ""}
onChange={(e) => {
if (e.target.value === "__ADD_NEW__") {
setPendingInvoiceId(invoice.id);
setAddDialogType("ventilation");
setAddDialogOpen(true);
e.target.value = invoice.ventilationComptable || "";
} else {
updateFieldMutation.mutate({
id: invoice.id,
data: { ventilationComptable: e.target.value || undefined },
});
}
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">-</option>
{allocations?.map((alloc) => (
<option key={alloc.id} value={alloc.name}>{alloc.name}</option>
))}
<option value="__ADD_NEW__" className="font-semibold text-blue-600"> Ajouter...</option>
</select>
</TableCell>
<TableCell className="text-sm">
<select
value={invoice.isSubscription ? "OUI" : "NON"}
onChange={(e) => {
updateFieldMutation.mutate({
id: invoice.id,
data: { isSubscription: e.target.value === "OUI" ? 1 : 0 },
});
}}
className="w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="NON">NON</option>
<option value="OUI">OUI</option>
</select>
</TableCell>
<TableCell>{getQualityBadge(invoice.qualityScore)}</TableCell>
<TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell>
<TableCell>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => {
setSelectedInvoiceText(invoice.extractedText || "Aucun texte extrait disponible");
setTextDialogOpen(true);
}}
className="h-8 px-2"
title="Voir le texte extrait"
>
<FileText className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setLocation(`/invoices/${invoice.id}`)}
className="h-8 px-2"
title="Modifier"
>
<Edit className="h-4 w-4" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => {
setInvoiceToDelete(invoice.id);
setDeleteDialogOpen(true);
}}
className="h-8 px-2 text-red-600 hover:text-red-700 hover:bg-red-50"
title="Supprimer"
>
<Trash className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
) : (
<div className="text-center py-12">
<FileText className="w-12 h-12 mx-auto mb-3 text-gray-300" />
<p className="text-gray-500">Aucune facture trouvée</p>
<p className="text-sm text-gray-400 mt-1">
{searchQuery ? "Essayez une autre recherche" : "Commencez par importer un fichier PDF"}
</p>
</div>
)}
</CardContent>
</Card>
{/* Info message */}
{filteredInvoices && filteredInvoices.some(inv => (inv.qualityScore || 0) < 100) && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<p className="text-sm text-blue-800">
<strong>Note :</strong> Seules les factures avec un score de qualité de 100% peuvent être exportées.
Les factures avec un score inférieur sont grisées et ne peuvent pas être sélectionnées.
</p>
</div>
)}
</div>
{/* Delete Confirmation Dialog */}
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Êtes-vous sûr de vouloir supprimer cette facture ?</AlertDialogTitle>
<AlertDialogDescription>
Cette action est irréversible. La facture sera définitivement supprimée de la base de données.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setInvoiceToDelete(null)}>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
if (invoiceToDelete) {
deleteMutation.mutate({ id: invoiceToDelete });
}
}}
className="bg-red-600 hover:bg-red-700"
>
Supprimer
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* Add New Item Dialog */}
<Dialog open={addDialogOpen} onOpenChange={setAddDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>
Ajouter {addDialogType === "service" ? "un nouveau service" : "une nouvelle ventilation"}
</DialogTitle>
<DialogDescription>
Entrez le nom {addDialogType === "service" ? "du service" : "de la ventilation comptable"} à ajouter.
</DialogDescription>
</DialogHeader>
<div className="py-4">
<Input
placeholder="Nom..."
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleAddNewItem();
}
}}
autoFocus
/>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => {
setAddDialogOpen(false);
setNewItemName("");
setPendingInvoiceId(null);
}}
>
Annuler
</Button>
<Button onClick={handleAddNewItem}>Ajouter</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Extracted Text Dialog */}
<Dialog open={textDialogOpen} onOpenChange={setTextDialogOpen}>
<DialogContent className="max-w-3xl max-h-[80vh]">
<DialogHeader>
<DialogTitle>Texte extrait de la facture</DialogTitle>
<DialogDescription>
Texte complet extrait du PDF lors de l'import
</DialogDescription>
</DialogHeader>
<div className="overflow-y-auto max-h-[60vh] p-4 bg-gray-50 rounded border">
<pre className="whitespace-pre-wrap text-sm font-mono">{selectedInvoiceText}</pre>
</div>
<DialogFooter>
<Button onClick={() => setTextDialogOpen(false)}>Fermer</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</DashboardLayout>
);
}

17
todo.md
View File

@@ -266,3 +266,20 @@
- [x] Corriger le prompt pour utiliser UNIQUEMENT les mots-clés configurés dans subscriptionKeywords
- [x] Ajouter logique: si subscriptionKeywords est vide, isSubscription = false par défaut
- [ ] Tester avec une nouvelle facture importée et le mot-clé "BLOB" (ne devrait pas détecter)
## Nouvelle page Factures BAP
- [ ] Créer une nouvelle page InvoicesBAP.tsx affichant uniquement les factures avec Abonnement = NON
- [ ] Inclure les colonnes Type d'achat et Ventilation dans la page Factures BAP
- [ ] Ajouter la route /invoices-bap dans App.tsx
- [ ] Ajouter le lien "Factures BAP" dans le menu de navigation
- [ ] Supprimer les colonnes Type d'achat et Ventilation de la page Factures principale
- [ ] Tester le filtrage et l'affichage des factures BAP
## Page Factures BAP
- [x] Créer nouvelle page InvoicesBAP.tsx basée sur Invoices.tsx
- [x] Filtrer uniquement les factures avec Abonnement = NON (isSubscription = 0)
- [x] Inclure les colonnes Type d'achat et Ventilation dans Factures BAP
- [x] Supprimer les colonnes Type d'achat et Ventilation de la page Factures principale
- [x] Ajouter la route /invoices-bap dans App.tsx
- [x] Ajouter le menu "Factures BAP" dans DashboardLayout
- [ ] Tester le filtrage et l'édition directe dans la nouvelle page