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,
|
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");
|
||||||
@@ -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 hasOnlyEligibleSelected = selectedIds.length > 0 && selectedIds.every(id => {
|
||||||
const invoice = invoices?.find(inv => inv.id === id);
|
const invoice = invoices?.find(inv => inv.id === id);
|
||||||
return invoice && isEligibleForExport(invoice);
|
return invoice && isEligibleForExport(invoice);
|
||||||
@@ -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>
|
||||||
|
|||||||
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,
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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"]),
|
||||||
|
|||||||
@@ -376,6 +376,43 @@ export const appRouter = router({
|
|||||||
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() }))
|
||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
|||||||
10
todo.md
10
todo.md
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user