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:
@@ -32,7 +32,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
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 { toast } from "sonner";
|
||||
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({
|
||||
onSuccess: () => {
|
||||
toast.success("Facture mise à jour");
|
||||
@@ -301,6 +311,36 @@ export default function InvoicesBAP() {
|
||||
);
|
||||
};
|
||||
|
||||
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 invoice = invoices?.find(inv => inv.id === id);
|
||||
return invoice && isEligibleForExport(invoice);
|
||||
@@ -583,7 +623,7 @@ export default function InvoicesBAP() {
|
||||
</TableCell>
|
||||
<TableCell>{getExportStatusBadge(invoice.exportStatus || "not_exported")}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -617,6 +657,31 @@ export default function InvoicesBAP() {
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</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>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
2
drizzle/0012_left_madame_web.sql
Normal file
2
drizzle/0012_left_madame_web.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE `invoices` ADD `bapValidated` int DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE `invoices` ADD `bapValidatedAt` timestamp;
|
||||
1303
drizzle/meta/0012_snapshot.json
Normal file
1303
drizzle/meta/0012_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -85,6 +85,13 @@
|
||||
"when": 1770971673626,
|
||||
"tag": "0011_woozy_sabretooth",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 12,
|
||||
"version": "5",
|
||||
"when": 1772788503373,
|
||||
"tag": "0012_left_madame_web",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -99,6 +99,10 @@ export const invoices = mysqlTable("invoices", {
|
||||
// Subscription flag
|
||||
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
|
||||
exportedAt: timestamp("exportedAt"),
|
||||
exportMode: mysqlEnum("exportMode", ["manual", "automatic"]),
|
||||
|
||||
@@ -376,6 +376,43 @@ export const appRouter = router({
|
||||
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
|
||||
.input(z.object({ query: z.string() }))
|
||||
.query(async ({ input, ctx }) => {
|
||||
|
||||
10
todo.md
10
todo.md
@@ -498,3 +498,13 @@
|
||||
- [x] Gérer les erreurs de copie de fichiers
|
||||
- [x] Afficher une notification de succès avec le chemin du dossier d'export
|
||||
- [ ] 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
|
||||
|
||||
Reference in New Issue
Block a user