Checkpoint: Ajout du bouton Validation BAP dans la page Factures BAP : nouveau champ bapValidated/bapValidatedAt dans le schéma, migration DB, route tRPC validateBAP avec vérification des critères (Score 100%, non exportée, isSubscription=0, service+typeAchat+ventilation remplis), bouton vert actif/désactivé avec tooltip explicatif, badge "Validé" pour les factures déjà validées

This commit is contained in:
Manus
2026-03-06 04:17:53 -05:00
parent f16efa4197
commit 9868a1afda
7 changed files with 1430 additions and 2 deletions

View File

@@ -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 } from "lucide-react"; import { Search, FileText, Download, FileSpreadsheet, Trash2, Edit, Trash, CheckCircle, CheckCircle2 } from "lucide-react";
import * as XLSX from 'xlsx'; import * as XLSX from 'xlsx';
import { toast } from "sonner"; import { toast } from "sonner";
import { useLocation } from "wouter"; import { useLocation } from "wouter";
@@ -131,6 +131,16 @@ export default function InvoicesBAP() {
}, },
}); });
const validateBAPMutation = trpc.invoices.validateBAP.useMutation({
onSuccess: () => {
toast.success("Facture validée BAP avec succès !");
utils.invoices.list.invalidate();
},
onError: (error) => {
toast.error(error.message || "Erreur lors de la validation BAP");
},
});
const updateFieldMutation = trpc.invoices.update.useMutation({ const updateFieldMutation = trpc.invoices.update.useMutation({
onSuccess: () => { onSuccess: () => {
toast.success("Facture mise à jour"); toast.success("Facture mise à jour");
@@ -300,6 +310,36 @@ export default function InvoicesBAP() {
invoice.ventilationComptable invoice.ventilationComptable
); );
}; };
const isEligibleForBAPValidation = (invoice: any) => {
// Une facture peut être validée BAP si :
// 1. Score = 100%
// 2. Non encore exportée (exportStatus !== 'exported')
// 3. Abonnement = NON (isSubscription = 0)
// 4. Service concerné rempli
// 5. Type d'achat rempli
// 6. Ventilation comptable remplie
return (
(invoice.qualityScore || 0) === 100 &&
invoice.exportStatus !== "exported" &&
invoice.isSubscription === 0 &&
invoice.serviceConcerne &&
invoice.typeAchat &&
invoice.ventilationComptable
);
};
const getBAPValidationTooltip = (invoice: any): string => {
const reasons: string[] = [];
if ((invoice.qualityScore || 0) < 100) reasons.push("Score < 100%");
if (invoice.exportStatus === "exported") reasons.push("Déjà exportée");
if (invoice.isSubscription !== 0) reasons.push("Marquée comme abonnement");
if (!invoice.serviceConcerne) reasons.push("Service manquant");
if (!invoice.typeAchat) reasons.push("Type d'achat manquant");
if (!invoice.ventilationComptable) reasons.push("Ventilation manquante");
if (reasons.length === 0) return "Valider cette facture BAP";
return `Non éligible : ${reasons.join(", ")}`;
};
const hasOnlyEligibleSelected = selectedIds.length > 0 && selectedIds.every(id => { const hasOnlyEligibleSelected = selectedIds.length > 0 && selectedIds.every(id => {
const invoice = invoices?.find(inv => inv.id === id); const invoice = invoices?.find(inv => inv.id === id);
@@ -583,7 +623,7 @@ export default function InvoicesBAP() {
</TableCell> </TableCell>
<TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell> <TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell>
<TableCell> <TableCell>
<div className="flex gap-2"> <div className="flex gap-1">
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@@ -617,6 +657,31 @@ export default function InvoicesBAP() {
> >
<Trash className="h-4 w-4" /> <Trash className="h-4 w-4" />
</Button> </Button>
{invoice.bapValidated === 1 ? (
<div
className="h-8 px-2 flex items-center justify-center rounded border border-green-300 bg-green-50 text-green-700 text-xs font-semibold gap-1"
title={`Validé BAP le ${invoice.bapValidatedAt ? new Date(invoice.bapValidatedAt).toLocaleDateString('fr-FR') : ''}`}
>
<CheckCircle2 className="h-4 w-4" />
<span>Validé</span>
</div>
) : (
<Button
size="sm"
variant="outline"
onClick={() => validateBAPMutation.mutate({ id: invoice.id })}
disabled={!isEligibleForBAPValidation(invoice) || validateBAPMutation.isPending}
className={`h-8 px-2 gap-1 ${
isEligibleForBAPValidation(invoice)
? "text-emerald-700 border-emerald-400 hover:bg-emerald-50 hover:border-emerald-500"
: "text-gray-400 border-gray-200 cursor-not-allowed opacity-50"
}`}
title={getBAPValidationTooltip(invoice)}
>
<CheckCircle2 className="h-4 w-4" />
<span className="text-xs">BAP</span>
</Button>
)}
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -0,0 +1,2 @@
ALTER TABLE `invoices` ADD `bapValidated` int DEFAULT 0 NOT NULL;--> statement-breakpoint
ALTER TABLE `invoices` ADD `bapValidatedAt` timestamp;

File diff suppressed because it is too large Load Diff

View File

@@ -85,6 +85,13 @@
"when": 1770971673626, "when": 1770971673626,
"tag": "0011_woozy_sabretooth", "tag": "0011_woozy_sabretooth",
"breakpoints": true "breakpoints": true
},
{
"idx": 12,
"version": "5",
"when": 1772788503373,
"tag": "0012_left_madame_web",
"breakpoints": true
} }
] ]
} }

View File

@@ -99,6 +99,10 @@ export const invoices = mysqlTable("invoices", {
// Subscription flag // Subscription flag
isSubscription: int("isSubscription").default(0).notNull(), // 0 = NON, 1 = OUI isSubscription: int("isSubscription").default(0).notNull(), // 0 = NON, 1 = OUI
// BAP Validation
bapValidated: int("bapValidated").default(0).notNull(), // 0 = non validé, 1 = validé BAP
bapValidatedAt: timestamp("bapValidatedAt"), // Date de validation BAP
// SFTP Export tracking // SFTP Export tracking
exportedAt: timestamp("exportedAt"), exportedAt: timestamp("exportedAt"),
exportMode: mysqlEnum("exportMode", ["manual", "automatic"]), exportMode: mysqlEnum("exportMode", ["manual", "automatic"]),

View File

@@ -375,6 +375,43 @@ export const appRouter = router({
await deleteInvoice(input.id); await deleteInvoice(input.id);
return { success: true }; return { success: true };
}), }),
validateBAP: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(async ({ input, ctx }) => {
const invoice = await getInvoiceById(input.id);
if (!invoice || invoice.userId !== ctx.user.id) {
throw new TRPCError({ code: "NOT_FOUND" });
}
// Vérifier les critères de validation BAP
const score = invoice.qualityScore || 0;
const isNotExported = invoice.exportStatus !== "exported";
const isNotSubscription = invoice.isSubscription === 0;
const hasService = !!invoice.serviceConcerne;
const hasTypeAchat = !!invoice.typeAchat;
const hasVentilation = !!invoice.ventilationComptable;
if (score < 100) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Le score de qualité doit être à 100% pour valider" });
}
if (!isNotExported) {
throw new TRPCError({ code: "BAD_REQUEST", message: "La facture a déjà été exportée" });
}
if (!isNotSubscription) {
throw new TRPCError({ code: "BAD_REQUEST", message: "La facture est marquée comme abonnement" });
}
if (!hasService || !hasTypeAchat || !hasVentilation) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Les champs Service, Type d'achat et Ventilation doivent être remplis" });
}
await updateInvoice(input.id, {
bapValidated: 1,
bapValidatedAt: new Date(),
});
return { success: true, validatedAt: new Date() };
}),
search: protectedProcedure search: protectedProcedure
.input(z.object({ query: z.string() })) .input(z.object({ query: z.string() }))

10
todo.md
View File

@@ -498,3 +498,13 @@
- [x] Gérer les erreurs de copie de fichiers - [x] Gérer les erreurs de copie de fichiers
- [x] Afficher une notification de succès avec le chemin du dossier d'export - [x] Afficher une notification de succès avec le chemin du dossier d'export
- [ ] Tester l'export vers le dossier configuré - [ ] Tester l'export vers le dossier configuré
## Bouton Validation BAP
- [x] Analyser la structure de la page InvoicesBAP.tsx
- [x] Créer la route tRPC validateBAP dans routers.ts (mise à jour du statut en "validated")
- [x] Ajouter la colonne "Actions" en fin de chaque ligne du tableau
- [x] Ajouter le bouton "Validation BAP" avec icône CheckCircle2
- [x] Implémenter la logique d'activation : Score 100% + non exportée + isSubscription=0 + service + typeAchat + ventilation remplis
- [x] Désactiver le bouton avec tooltip explicatif pour les factures non éligibles
- [x] Afficher un toast de confirmation après validation
- [x] Rafraîchir la liste après validation