Checkpoint: Accordéon +/- dans Factures BAP : bouton global ChevronsUpDown en en-tête, bouton individuel ChevronRight/Down par ligne, seconde ligne dépliable avec Service/Type achat/Ventilation, colonnes retirées de la ligne principale

This commit is contained in:
Manus
2026-04-12 18:31:22 -04:00
parent 5d0c8fbda9
commit 4090678f6f

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import React, { useState } from "react";
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@@ -32,7 +32,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { trpc } from "@/lib/trpc"; import { trpc } from "@/lib/trpc";
import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck, RefreshCw, FolderDown } from "lucide-react"; import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2, ShieldCheck, RefreshCw, FolderDown, ChevronDown, ChevronRight, ChevronsUpDown } from "lucide-react";
// Helper : télécharge un ZIP de plusieurs PDFs annotés BAP // Helper : télécharge un ZIP de plusieurs PDFs annotés BAP
async function downloadBapZip( async function downloadBapZip(
@@ -121,6 +121,28 @@ export default function InvoicesBAP() {
const [textDialogOpen, setTextDialogOpen] = useState(false); const [textDialogOpen, setTextDialogOpen] = useState(false);
const [selectedInvoiceText, setSelectedInvoiceText] = useState<string | null>(null); const [selectedInvoiceText, setSelectedInvoiceText] = useState<string | null>(null);
const [isZipDownloading, setIsZipDownloading] = useState(false); const [isZipDownloading, setIsZipDownloading] = useState(false);
// Accordéon : lignes dépliées
const [expandedIds, setExpandedIds] = useState<Set<number>>(new Set());
const [allExpanded, setAllExpanded] = useState(false);
const toggleRow = (id: number) => {
setExpandedIds(prev => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
const toggleAllRows = () => {
if (allExpanded) {
setExpandedIds(new Set());
setAllExpanded(false);
} else {
setExpandedIds(new Set((filteredInvoices || []).map(inv => inv.id)));
setAllExpanded(true);
}
};
const { data: allInvoices, isLoading } = trpc.invoices.list.useQuery(); const { data: allInvoices, isLoading } = trpc.invoices.list.useQuery();
// Filter for BAP invoices only (Abonnement = NON, isSubscription = 0) // Filter for BAP invoices only (Abonnement = NON, isSubscription = 0)
const invoices = allInvoices?.filter(inv => inv.isSubscription === 0); const invoices = allInvoices?.filter(inv => inv.isSubscription === 0);
@@ -653,7 +675,17 @@ export default function InvoicesBAP() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead className="w-12"> <TableHead className="w-8">
{/* Bouton global +/- */}
<button
onClick={toggleAllRows}
className="flex items-center justify-center w-6 h-6 rounded border border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-600"
title={allExpanded ? "Replier toutes les lignes" : "Déplier toutes les lignes"}
>
<ChevronsUpDown className="w-3 h-3" />
</button>
</TableHead>
<TableHead className="w-10">
<Checkbox <Checkbox
checked={allEligibleSelected} checked={allEligibleSelected}
onCheckedChange={handleSelectAll} onCheckedChange={handleSelectAll}
@@ -664,23 +696,31 @@ export default function InvoicesBAP() {
<TableHead>N° Facture</TableHead> <TableHead>N° Facture</TableHead>
<TableHead>Date</TableHead> <TableHead>Date</TableHead>
<TableHead>Montant</TableHead> <TableHead>Montant</TableHead>
<TableHead>Service</TableHead>
<TableHead>Type achat</TableHead>
<TableHead>Ventilation</TableHead>
<TableHead>Score</TableHead> <TableHead>Score</TableHead>
<TableHead>Statut</TableHead> <TableHead>Statut</TableHead>
<TableHead className="w-32">Actions</TableHead> <TableHead className="w-36">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{filteredInvoices.map((invoice) => { {filteredInvoices.map((invoice) => {
const isExpanded = expandedIds.has(invoice.id);
return ( return (
<TableRow key={invoice.id}> <React.Fragment key={invoice.id}>
<TableRow className="hover:bg-gray-50">
{/* Bouton +/- individuel */}
<TableCell className="p-1">
<button
onClick={() => toggleRow(invoice.id)}
className="flex items-center justify-center w-6 h-6 rounded border border-gray-300 bg-white hover:bg-gray-100 text-gray-500"
title={isExpanded ? "Replier" : "Déplier"}
>
{isExpanded ? <ChevronDown className="w-3 h-3" /> : <ChevronRight className="w-3 h-3" />}
</button>
</TableCell>
<TableCell> <TableCell>
<Checkbox <Checkbox
checked={selectedIds.includes(invoice.id)} checked={selectedIds.includes(invoice.id)}
onCheckedChange={(checked) => handleSelectOne(invoice.id, checked as boolean)} onCheckedChange={(checked) => handleSelectOne(invoice.id, checked as boolean)}
/> />
</TableCell> </TableCell>
<TableCell className="font-medium"> <TableCell className="font-medium">
@@ -702,120 +742,6 @@ export default function InvoicesBAP() {
? `${parseFloat(invoice.totalAmount as string).toFixed(2)}` ? `${parseFloat(invoice.totalAmount as string).toFixed(2)}`
: "-"} : "-"}
</TableCell> </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 {
// Remove field from autoFilledFields when manually edited
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "serviceConcerne");
const newVal = e.target.value || undefined;
// Apprentissage : mémoriser la correction manuelle
if (invoice.supplierName && newVal !== (invoice.serviceConcerne || undefined)) {
upsertLearningMutation.mutate({
supplierName: invoice.supplierName,
fieldName: 'serviceConcerne',
originalValue: invoice.serviceConcerne || undefined,
correctedValue: newVal || '',
});
}
updateFieldMutation.mutate({
id: invoice.id,
data: {
serviceConcerne: newVal,
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
},
});
}
}}
className={`w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "serviceConcerne")}`}
>
<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) => {
// Remove field from autoFilledFields when manually edited
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "typeAchat");
const newTypeAchat = e.target.value as "CAPEX" | "OPEX" | undefined;
// Apprentissage : mémoriser la correction manuelle
if (invoice.supplierName && newTypeAchat !== (invoice.typeAchat || undefined)) {
upsertLearningMutation.mutate({
supplierName: invoice.supplierName,
fieldName: 'typeAchat',
originalValue: invoice.typeAchat || undefined,
correctedValue: newTypeAchat || '',
});
}
updateFieldMutation.mutate({
id: invoice.id,
data: {
typeAchat: newTypeAchat,
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
},
});
}}
className={`w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "typeAchat")}`}
>
<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 {
// Remove field from autoFilledFields when manually edited
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "ventilationComptable");
const newVentil = e.target.value || undefined;
// Apprentissage : mémoriser la correction manuelle
if (invoice.supplierName && newVentil !== (invoice.ventilationComptable || undefined)) {
upsertLearningMutation.mutate({
supplierName: invoice.supplierName,
fieldName: 'ventilationComptable',
originalValue: invoice.ventilationComptable || undefined,
correctedValue: newVentil || '',
});
}
updateFieldMutation.mutate({
id: invoice.id,
data: {
ventilationComptable: newVentil,
autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null
},
});
}
}}
className={`w-full px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "ventilationComptable")}`}
>
<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> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{getQualityBadge(invoice.qualityScore)} {getQualityBadge(invoice.qualityScore)}
@@ -913,6 +839,98 @@ export default function InvoicesBAP() {
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
{/* Ligne 2 : détails dépliables (Service, Type achat, Ventilation) */}
{isExpanded && (
<TableRow key={`${invoice.id}-details`} className="bg-blue-50/40 border-t-0">
{/* col +/- vide */}
<TableCell className="p-1" />
{/* col checkbox vide */}
<TableCell />
{/* colspan 6 pour les 3 champs */}
<TableCell colSpan={6} className="py-2 px-4">
<div className="flex flex-wrap gap-4 items-center">
{/* Service */}
<div className="flex items-center gap-2 min-w-[200px]">
<span className="text-xs font-medium text-gray-500 whitespace-nowrap">Service :</span>
<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 {
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "serviceConcerne");
const newVal = e.target.value || undefined;
if (invoice.supplierName && newVal !== (invoice.serviceConcerne || undefined)) {
upsertLearningMutation.mutate({ supplierName: invoice.supplierName, fieldName: 'serviceConcerne', originalValue: invoice.serviceConcerne || undefined, correctedValue: newVal || '' });
}
updateFieldMutation.mutate({ id: invoice.id, data: { serviceConcerne: newVal, autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null } });
}
}}
className={`flex-1 px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "serviceConcerne")}`}
>
<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>
</div>
{/* Type achat */}
<div className="flex items-center gap-2 min-w-[160px]">
<span className="text-xs font-medium text-gray-500 whitespace-nowrap">Type achat :</span>
<select
value={invoice.typeAchat || ""}
onChange={(e) => {
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "typeAchat");
const newTypeAchat = e.target.value as "CAPEX" | "OPEX" | undefined;
if (invoice.supplierName && newTypeAchat !== (invoice.typeAchat || undefined)) {
upsertLearningMutation.mutate({ supplierName: invoice.supplierName, fieldName: 'typeAchat', originalValue: invoice.typeAchat || undefined, correctedValue: newTypeAchat || '' });
}
updateFieldMutation.mutate({ id: invoice.id, data: { typeAchat: newTypeAchat, autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null } });
}}
className={`flex-1 px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "typeAchat")}`}
>
<option value="">-</option>
<option value="CAPEX">CAPEX</option>
<option value="OPEX">OPEX</option>
</select>
</div>
{/* Ventilation */}
<div className="flex items-center gap-2 min-w-[220px]">
<span className="text-xs font-medium text-gray-500 whitespace-nowrap">Ventilation :</span>
<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 {
const autoFilledFields = invoice.autoFilledFields ? JSON.parse(invoice.autoFilledFields) : [];
const updatedAutoFields = autoFilledFields.filter((f: string) => f !== "ventilationComptable");
const newVentil = e.target.value || undefined;
if (invoice.supplierName && newVentil !== (invoice.ventilationComptable || undefined)) {
upsertLearningMutation.mutate({ supplierName: invoice.supplierName, fieldName: 'ventilationComptable', originalValue: invoice.ventilationComptable || undefined, correctedValue: newVentil || '' });
}
updateFieldMutation.mutate({ id: invoice.id, data: { ventilationComptable: newVentil, autoFilledFields: updatedAutoFields.length > 0 ? JSON.stringify(updatedAutoFields) : null } });
}
}}
className={`flex-1 px-2 py-1 border rounded text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 ${getFieldColor(invoice, "ventilationComptable")}`}
>
<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>
</div>
</div>
</TableCell>
</TableRow>
)}
</React.Fragment>
); );
})} })}
</TableBody> </TableBody>