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 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>
|
||||||
|
|||||||
Reference in New Issue
Block a user