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:
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import DashboardLayout from "@/components/DashboardLayout";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
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
|
||||
async function downloadBapZip(
|
||||
@@ -121,6 +121,28 @@ export default function InvoicesBAP() {
|
||||
const [textDialogOpen, setTextDialogOpen] = useState(false);
|
||||
const [selectedInvoiceText, setSelectedInvoiceText] = useState<string | null>(null);
|
||||
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();
|
||||
// Filter for BAP invoices only (Abonnement = NON, isSubscription = 0)
|
||||
const invoices = allInvoices?.filter(inv => inv.isSubscription === 0);
|
||||
@@ -653,7 +675,17 @@ export default function InvoicesBAP() {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<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
|
||||
checked={allEligibleSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
@@ -664,23 +696,31 @@ export default function InvoicesBAP() {
|
||||
<TableHead>N° Facture</TableHead>
|
||||
<TableHead>Date</TableHead>
|
||||
<TableHead>Montant</TableHead>
|
||||
<TableHead>Service</TableHead>
|
||||
<TableHead>Type achat</TableHead>
|
||||
<TableHead>Ventilation</TableHead>
|
||||
<TableHead>Score</TableHead>
|
||||
<TableHead>Statut</TableHead>
|
||||
<TableHead className="w-32">Actions</TableHead>
|
||||
<TableHead className="w-36">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredInvoices.map((invoice) => {
|
||||
const isExpanded = expandedIds.has(invoice.id);
|
||||
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>
|
||||
<Checkbox
|
||||
checked={selectedIds.includes(invoice.id)}
|
||||
onCheckedChange={(checked) => handleSelectOne(invoice.id, checked as boolean)}
|
||||
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
@@ -702,120 +742,6 @@ export default function InvoicesBAP() {
|
||||
? `${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 {
|
||||
// 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>
|
||||
<div className="flex items-center gap-2">
|
||||
{getQualityBadge(invoice.qualityScore)}
|
||||
@@ -913,6 +839,98 @@ export default function InvoicesBAP() {
|
||||
</div>
|
||||
</TableCell>
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user