From 5f7c8c7c7f616775d7aef2361fe3563a40ed3443 Mon Sep 17 00:00:00 2001 From: Manus Date: Sun, 12 Apr 2026 05:57:02 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Am=C3=A9liorations=20Factures=20B?= =?UTF-8?q?AP=20:=20suppression=20colonne=20Abonnement,=20mode=20d'export?= =?UTF-8?q?=20configurable=20(navigateur/dossier),=20g=C3=A9n=C3=A9ration?= =?UTF-8?q?=20PDF=20annot=C3=A9=20avec=20zone=20blanche=20(CAPEX/OPEX=20|?= =?UTF-8?q?=20BAP=20|=20Destinataire=20+=20signature=20du=20service)=20lor?= =?UTF-8?q?s=20du=20clic=20BAP,=20historique=20BAP=20dans=20le=20menu=20Tr?= =?UTF-8?q?a=C3=A7abilit=C3=A9,=20table=20bapHistory=20en=20DB.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 2 + client/src/components/DashboardLayout.tsx | 3 +- client/src/pages/BapHistory.tsx | 281 ++++ client/src/pages/ImportSettings.tsx | 61 +- client/src/pages/InvoicesBAP.tsx | 27 +- drizzle/0017_clammy_toad.sql | 21 + drizzle/meta/0017_snapshot.json | 1610 +++++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 25 + server/db.ts | 37 +- server/routers.ts | 215 ++- todo.md | 11 + 12 files changed, 2261 insertions(+), 39 deletions(-) create mode 100644 client/src/pages/BapHistory.tsx create mode 100644 drizzle/0017_clammy_toad.sql create mode 100644 drizzle/meta/0017_snapshot.json diff --git a/client/src/App.tsx b/client/src/App.tsx index 29bff32..9602d46 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -17,6 +17,7 @@ import History from "./pages/History"; import Users from "./pages/Users"; import ListsAdmin from "./pages/ListsAdmin"; import AutomationRules from "./pages/AutomationRules"; +import BapHistory from "./pages/BapHistory"; function Router() { return ( @@ -34,6 +35,7 @@ function Router() { + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 81ecfab..f19413f 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -25,7 +25,7 @@ import { import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap, ChevronDown, Receipt, Cog, ClipboardList } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download, List, Zap, ChevronDown, Receipt, Cog, ClipboardList, CheckSquare } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; @@ -75,6 +75,7 @@ const menuStructure: MenuItem[] = [ color: "from-green-500 to-emerald-500", children: [ { icon: History, label: "Historiques", path: "/history" }, + { icon: CheckSquare, label: "Historique BAP", path: "/bap-history" }, ], }, ]; diff --git a/client/src/pages/BapHistory.tsx b/client/src/pages/BapHistory.tsx new file mode 100644 index 0000000..9556686 --- /dev/null +++ b/client/src/pages/BapHistory.tsx @@ -0,0 +1,281 @@ +import { useState } from "react"; +import { trpc } from "@/lib/trpc"; +import DashboardLayout from "@/components/DashboardLayout"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { toast } from "sonner"; +import { + Loader2, + CheckSquare, + Search, + Trash2, + ExternalLink, + FolderOpen, + Monitor, + Calendar, + Building2, + User, + FileText, +} from "lucide-react"; + +export default function BapHistory() { + const { data: entries, isLoading, refetch } = trpc.bapHistory.getAll.useQuery(); + const deleteMutation = trpc.bapHistory.delete.useMutation(); + const [search, setSearch] = useState(""); + + const filtered = (entries || []).filter((e) => { + const q = search.toLowerCase(); + return ( + !q || + (e.supplierName || "").toLowerCase().includes(q) || + (e.invoiceNumber || "").toLowerCase().includes(q) || + (e.serviceConcerne || "").toLowerCase().includes(q) || + (e.recipientName || "").toLowerCase().includes(q) || + (e.typeAchat || "").toLowerCase().includes(q) + ); + }); + + const handleDelete = async (id: number) => { + try { + await deleteMutation.mutateAsync({ id }); + toast.success("Entrée supprimée"); + refetch(); + } catch { + toast.error("Impossible de supprimer cette entrée"); + } + }; + + const formatDate = (d: Date | string | null) => { + if (!d) return "—"; + return new Date(d).toLocaleDateString("fr-FR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + }; + + const formatAmount = (a: string | null) => { + if (!a) return "—"; + const n = parseFloat(a); + return isNaN(n) ? a : n.toLocaleString("fr-FR", { style: "currency", currency: "EUR" }); + }; + + return ( + +
+ {/* Header */} +
+
+
+ +
+
+

+ Historique BAP +

+

+ Toutes les factures validées "Bon à Payer" avec leur PDF annoté +

+
+
+
+ + {/* Stats rapides */} +
+ + +
{(entries || []).length}
+
Total validations
+
+
+ + +
+ {(entries || []).filter((e) => e.exportMode === "browser").length} +
+
Ouverts navigateur
+
+
+ + +
+ {(entries || []).filter((e) => e.exportMode === "folder").length} +
+
Exportés dossier
+
+
+ + +
+ {(entries || []).filter((e) => e.signatureName).length} +
+
Avec signature
+
+
+
+ + {/* Tableau */} + + +
+
+ Validations BAP + + {filtered.length} résultat{filtered.length !== 1 ? "s" : ""} + {search ? ` pour "${search}"` : ""} + +
+
+ + setSearch(e.target.value)} + className="pl-9 h-9" + /> +
+
+
+ + {isLoading ? ( +
+ + Chargement... +
+ ) : filtered.length === 0 ? ( +
+ +

+ {search ? "Aucun résultat pour cette recherche" : "Aucune validation BAP enregistrée"} +

+
+ ) : ( +
+ + + + +
+ + Date validation +
+
+ +
+ + Fournisseur +
+
+ N° Facture + Montant + Type achat + +
+ + Destinataire +
+
+ Service + Signature + Export + Actions +
+
+ + {filtered.map((entry) => ( + + + {formatDate(entry.validatedAt)} + + + {entry.supplierName || "—"} + + + {entry.invoiceNumber || "—"} + + + {formatAmount(entry.totalAmount)} + + + {entry.typeAchat ? ( + + {entry.typeAchat.toUpperCase()} + + ) : ( + "—" + )} + + + {entry.recipientName || ( + TOUS + )} + + + {entry.serviceConcerne || "—"} + + + {entry.signatureName ? ( + + {entry.signatureName} + + ) : ( + + )} + + + {entry.exportMode === "browser" && entry.pdfUrl ? ( + + ) : entry.exportMode === "folder" && entry.exportPath ? ( +
+ + + Dossier + +
+ ) : ( + + )} +
+ + + +
+ ))} +
+
+
+ )} +
+
+
+
+ ); +} diff --git a/client/src/pages/ImportSettings.tsx b/client/src/pages/ImportSettings.tsx index 7c6e955..2a1a545 100644 --- a/client/src/pages/ImportSettings.tsx +++ b/client/src/pages/ImportSettings.tsx @@ -7,7 +7,7 @@ import { Label } from "@/components/ui/label"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { toast } from "sonner"; -import { Loader2, Save, Upload, FolderOpen, Mail, Play, Square, Download, Inbox, CheckCircle2 } from "lucide-react"; +import { Loader2, Save, Upload, FolderOpen, Mail, Play, Square, Download, Inbox, CheckCircle2, Monitor, FolderOutput } from "lucide-react"; import DashboardLayout from "@/components/DashboardLayout"; export default function ImportSettings() { @@ -40,6 +40,8 @@ export default function ImportSettings() { // Export folder const [exportFolder, setExportFolder] = useState(""); + // BAP export mode + const [bapExportMode, setBapExportMode] = useState<"browser" | "folder">("browser"); // Initialize form with settings from database useEffect(() => { @@ -55,6 +57,7 @@ export default function ImportSettings() { setEmailImportPort(settings.emailImportPort || 993); setEmailImportFrequency(settings.emailImportFrequency || 30); setExportFolder(settings.exportFolder || ""); + setBapExportMode((settings.bapExportMode as "browser" | "folder") || "browser"); } }, [settings]); @@ -72,6 +75,7 @@ export default function ImportSettings() { emailImportPort: emailImportPort, emailImportFrequency: emailImportFrequency, exportFolder: exportFolder || null, + bapExportMode: bapExportMode, }); toast.success("Paramètres enregistrés avec succès"); @@ -469,7 +473,7 @@ export default function ImportSettings() { - {/* Export Folder */} + {/* Export Folder & BAP Export Mode */}
@@ -477,16 +481,57 @@ export default function ImportSettings() {
- Dossier d'export + Export des factures BAP - Configurez le dossier de destination pour l'export des factures + Configurez le mode d'export et le dossier de destination pour les factures validées BAP
- + + {/* BAP Export Mode */} +
+ +
+ + +
+
+ + {/* Export Folder (shown for both modes but required for folder mode) */}
- +

- Spécifiez le chemin absolu du dossier où les factures seront exportées + {bapExportMode === "folder" + ? "Chemin absolu obligatoire du dossier où les PDF BAP seront enregistrés" + : "Chemin optionnel du dossier d'export (utilisé pour l'export SFTP et l'export groupé)"}

diff --git a/client/src/pages/InvoicesBAP.tsx b/client/src/pages/InvoicesBAP.tsx index 379ead5..3a28b67 100644 --- a/client/src/pages/InvoicesBAP.tsx +++ b/client/src/pages/InvoicesBAP.tsx @@ -132,8 +132,15 @@ export default function InvoicesBAP() { }); const validateBAPMutation = trpc.invoices.validateBAP.useMutation({ - onSuccess: () => { - toast.success("Facture validée BAP avec succès !"); + onSuccess: (data) => { + if (data.exportMode === 'browser' && data.pdfUrl) { + toast.success("Facture validée BAP ! Ouverture du PDF annoté...", { duration: 3000 }); + window.open(data.pdfUrl, '_blank'); + } else if (data.exportMode === 'folder' && data.exportPath) { + toast.success(`Facture validée BAP ! PDF enregistré dans : ${data.exportPath}`, { duration: 6000 }); + } else { + toast.success("Facture validée BAP avec succès !"); + } utils.invoices.list.invalidate(); }, onError: (error) => { @@ -475,7 +482,6 @@ export default function InvoicesBAP() { Service Type achat Ventilation - Abonnement Score Statut Actions @@ -596,21 +602,6 @@ export default function InvoicesBAP() { - - -
{getQualityBadge(invoice.qualityScore)} diff --git a/drizzle/0017_clammy_toad.sql b/drizzle/0017_clammy_toad.sql new file mode 100644 index 0000000..67a75c3 --- /dev/null +++ b/drizzle/0017_clammy_toad.sql @@ -0,0 +1,21 @@ +CREATE TABLE `bapHistory` ( + `id` int AUTO_INCREMENT NOT NULL, + `userId` int NOT NULL, + `invoiceId` int NOT NULL, + `supplierName` varchar(255), + `invoiceNumber` varchar(100), + `invoiceDate` timestamp, + `totalAmount` varchar(50), + `typeAchat` varchar(50), + `serviceConcerne` varchar(100), + `ventilationComptable` varchar(100), + `recipientName` varchar(255), + `exportMode` enum('browser','folder') NOT NULL DEFAULT 'browser', + `exportPath` text, + `pdfUrl` text, + `signatureName` varchar(255), + `validatedAt` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `bapHistory_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +ALTER TABLE `importSettings` ADD `bapExportMode` enum('browser','folder') DEFAULT 'browser' NOT NULL; \ No newline at end of file diff --git a/drizzle/meta/0017_snapshot.json b/drizzle/meta/0017_snapshot.json new file mode 100644 index 0000000..a44497b --- /dev/null +++ b/drizzle/meta/0017_snapshot.json @@ -0,0 +1,1610 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "3bc673f2-4726-44ab-b368-3d3e93107a78", + "prevId": "33ac6d99-ae1e-427b-aacd-8d9f81738c7c", + "tables": { + "accountingAllocationList": { + "name": "accountingAllocationList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_allocation_unique": { + "name": "user_allocation_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "accountingAllocationList_id": { + "name": "accountingAllocationList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "automationRules": { + "name": "automationRules", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "priority": { + "name": "priority", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "conditions": { + "name": "conditions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conditionsLogic": { + "name": "conditionsLogic", + "type": "enum('AND','OR')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'AND'" + }, + "actions": { + "name": "actions", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "automationRules_id": { + "name": "automationRules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "bapHistory": { + "name": "bapHistory", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientName": { + "name": "recipientName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('browser','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "exportPath": { + "name": "exportPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pdfUrl": { + "name": "pdfUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "signatureName": { + "name": "signatureName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "validatedAt": { + "name": "validatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "bapHistory_id": { + "name": "bapHistory_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "departmentList": { + "name": "departmentList", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": { + "user_department_unique": { + "name": "user_department_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "departmentList_id": { + "name": "departmentList_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importLogs": { + "name": "importLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "invoicesImported": { + "name": "invoicesImported", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicatesIgnored": { + "name": "duplicatesIgnored", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "errors": { + "name": "errors", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "duplicateDetails": { + "name": "duplicateDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "errorDetails": { + "name": "errorDetails", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importedAt": { + "name": "importedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importLogs_id": { + "name": "importLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "importSettings": { + "name": "importSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "manualImportEnabled": { + "name": "manualImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "autoImportEnabled": { + "name": "autoImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "autoImportSourcePath": { + "name": "autoImportSourcePath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoImportFrequency": { + "name": "autoImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 60 + }, + "emailImportEnabled": { + "name": "emailImportEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "emailImportAddress": { + "name": "emailImportAddress", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPassword": { + "name": "emailImportPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportHost": { + "name": "emailImportHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportPort": { + "name": "emailImportPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 993 + }, + "emailImportFrequency": { + "name": "emailImportFrequency", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 30 + }, + "exportFolder": { + "name": "exportFolder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "bapExportMode": { + "name": "bapExportMode", + "type": "enum('browser','folder')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "importSettings_id": { + "name": "importSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "importSettings_userId_unique": { + "name": "importSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "invoices": { + "name": "invoices", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceIndexInFile": { + "name": "invoiceIndexInFile", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceDate": { + "name": "invoiceDate", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteNumber": { + "name": "deliveryNoteNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumber": { + "name": "orderNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "decimal(10,2)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientName": { + "name": "recipientName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(20)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "qualityScore": { + "name": "qualityScore", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileKey": { + "name": "metadataFileKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadataFileUrl": { + "name": "metadataFileUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportStatus": { + "name": "exportStatus", + "type": "enum('not_exported','exported','export_error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'not_exported'" + }, + "manuallyEdited": { + "name": "manuallyEdited", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "serviceConcerne": { + "name": "serviceConcerne", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "typeAchat": { + "name": "typeAchat", + "type": "enum('CAPEX','OPEX')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "ventilationComptable": { + "name": "ventilationComptable", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoFilledFields": { + "name": "autoFilledFields", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "extractedText": { + "name": "extractedText", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "isSubscription": { + "name": "isSubscription", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bapValidated": { + "name": "bapValidated", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "bapValidatedAt": { + "name": "bapValidatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportedAt": { + "name": "exportedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportMode": { + "name": "exportMode", + "type": "enum('manual','automatic')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "supplier_invoice_date_unique": { + "name": "supplier_invoice_date_unique", + "columns": [ + "supplierName", + "invoiceNumber", + "invoiceDate" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoices_id": { + "name": "invoices_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmFieldsConfig": { + "name": "llmFieldsConfig", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldName": { + "name": "fieldName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "displayName": { + "name": "displayName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "isRequired": { + "name": "isRequired", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "displayOrder": { + "name": "displayOrder", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmFieldsConfig_id": { + "name": "llmFieldsConfig_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "llmLogs": { + "name": "llmLogs", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sourceFileId": { + "name": "sourceFileId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceId": { + "name": "invoiceId", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "operation": { + "name": "operation", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "promptSent": { + "name": "promptSent", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "rawResponse": { + "name": "rawResponse", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cleanedResponse": { + "name": "cleanedResponse", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "success": { + "name": "success", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "errorMessage": { + "name": "errorMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "processingTimeMs": { + "name": "processingTimeMs", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "pageRange": { + "name": "pageRange", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "llmLogs_id": { + "name": "llmLogs_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "serviceSignatures": { + "name": "serviceSignatures", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "serviceName": { + "name": "serviceName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signatureId": { + "name": "signatureId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": { + "user_service_unique": { + "name": "user_service_unique", + "columns": [ + "userId", + "serviceName" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "serviceSignatures_id": { + "name": "serviceSignatures_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "signatures": { + "name": "signatures", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "firstName": { + "name": "firstName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lastName": { + "name": "lastName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imageKey": { + "name": "imageKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "signatures_id": { + "name": "signatures_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "sourceFiles": { + "name": "sourceFiles", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileKey": { + "name": "fileKey", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fileUrl": { + "name": "fileUrl", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalInvoicesDetected": { + "name": "totalInvoicesDetected", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "processingStatus": { + "name": "processingStatus", + "type": "enum('processing','completed','error')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'processing'" + }, + "processingProgress": { + "name": "processingProgress", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "sourceFiles_id": { + "name": "sourceFiles_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "userSettings": { + "name": "userSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "llmModel": { + "name": "llmModel", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral-large-latest'" + }, + "orderNumberFormat": { + "name": "orderNumberFormat", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "invoiceNumberKeywords": { + "name": "invoiceNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deliveryNoteKeywords": { + "name": "deliveryNoteKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "orderNumberKeywords": { + "name": "orderNumberKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supplierKeywords": { + "name": "supplierKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "totalAmountKeywords": { + "name": "totalAmountKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subscriptionKeywords": { + "name": "subscriptionKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "recipientKeywords": { + "name": "recipientKeywords", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRecipientFilter": { + "name": "sftpRecipientFilter", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpHost": { + "name": "sftpHost", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPort": { + "name": "sftpPort", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 22 + }, + "sftpUsername": { + "name": "sftpUsername", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpPassword": { + "name": "sftpPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sftpRemotePath": { + "name": "sftpRemotePath", + "type": "varchar(500)", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'/'" + }, + "sftpAutoExport": { + "name": "sftpAutoExport", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "llmLogsRetentionMonths": { + "name": "llmLogsRetentionMonths", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "userSettings_id": { + "name": "userSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "userSettings_userId_unique": { + "name": "userSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "openId": { + "name": "openId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureAdId": { + "name": "azureAdId", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "passwordHash": { + "name": "passwordHash", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginMethod": { + "name": "loginMethod", + "type": "enum('manus','local','azure-ad')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "enum('user','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'user'" + }, + "isActive": { + "name": "isActive", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 1 + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "onUpdate": true, + "default": "(now())" + }, + "lastSignedIn": { + "name": "lastSignedIn", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "users_openId_unique": { + "name": "users_openId_unique", + "columns": [ + "openId" + ] + }, + "users_azureAdId_unique": { + "name": "users_azureAdId_unique", + "columns": [ + "azureAdId" + ] + }, + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 417af30..370618e 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -120,6 +120,13 @@ "when": 1775985531192, "tag": "0016_tricky_quasimodo", "breakpoints": true + }, + { + "idx": 17, + "version": "5", + "when": 1775987314108, + "tag": "0017_clammy_toad", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index c5c0319..5a5f7ba 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -178,6 +178,7 @@ export const importSettings = mysqlTable("importSettings", { // Export folder settings exportFolder: text("exportFolder"), // Path to folder for exporting invoices + bapExportMode: mysqlEnum("bapExportMode", ["browser", "folder"]).default("browser").notNull(), // BAP export mode: open in browser or save to folder createdAt: timestamp("createdAt").defaultNow().notNull(), updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), @@ -347,3 +348,27 @@ export const serviceSignatures = mysqlTable("serviceSignatures", { export type ServiceSignature = typeof serviceSignatures.$inferSelect; export type InsertServiceSignature = typeof serviceSignatures.$inferInsert; + +/** + * BAP History table - records every BAP validation with PDF export details + */ +export const bapHistory = mysqlTable("bapHistory", { + id: int("id").autoincrement().primaryKey(), + userId: int("userId").notNull(), + invoiceId: int("invoiceId").notNull(), + supplierName: varchar("supplierName", { length: 255 }), + invoiceNumber: varchar("invoiceNumber", { length: 100 }), + invoiceDate: timestamp("invoiceDate"), + totalAmount: varchar("totalAmount", { length: 50 }), + typeAchat: varchar("typeAchat", { length: 50 }), // CAPEX / OPEX + serviceConcerne: varchar("serviceConcerne", { length: 100 }), + ventilationComptable: varchar("ventilationComptable", { length: 100 }), + recipientName: varchar("recipientName", { length: 255 }), + exportMode: mysqlEnum("exportMode", ["browser", "folder"]).default("browser").notNull(), + exportPath: text("exportPath"), // null for browser mode + pdfUrl: text("pdfUrl"), // S3 URL for browser mode + signatureName: varchar("signatureName", { length: 255 }), // Signer name if applied + validatedAt: timestamp("validatedAt").defaultNow().notNull(), +}); +export type BapHistory = typeof bapHistory.$inferSelect; +export type InsertBapHistory = typeof bapHistory.$inferInsert; diff --git a/server/db.ts b/server/db.ts index 2a30f56..62303be 100644 --- a/server/db.ts +++ b/server/db.ts @@ -38,7 +38,10 @@ import { Signature, serviceSignatures, InsertServiceSignature, - ServiceSignature + ServiceSignature, + bapHistory, + InsertBapHistory, + BapHistory } from "../drizzle/schema"; import { ENV } from './_core/env'; @@ -776,3 +779,35 @@ export async function deleteServiceSignature(userId: number, serviceName: string await db.delete(serviceSignatures).where(eq(serviceSignatures.id, match.id)); } } + +// ============= BAP HISTORY HELPERS ============= +export async function createBapHistoryEntry(data: InsertBapHistory): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(bapHistory).values(data); + const insertId = (result[0] as any).insertId; + const created = await getBapHistoryById(insertId); + if (!created) throw new Error("Failed to retrieve created BAP history entry"); + return created; +} + +export async function getBapHistoryById(id: number): Promise { + const db = await getDb(); + if (!db) return undefined; + const results = await db.select().from(bapHistory).where(eq(bapHistory.id, id)); + return results[0]; +} + +export async function getBapHistoryByUser(userId: number): Promise { + const db = await getDb(); + if (!db) return []; + return db.select().from(bapHistory) + .where(eq(bapHistory.userId, userId)) + .orderBy(desc(bapHistory.validatedAt)); +} + +export async function deleteBapHistoryEntry(id: number): Promise { + const db = await getDb(); + if (!db) return; + await db.delete(bapHistory).where(eq(bapHistory.id, id)); +} diff --git a/server/routers.ts b/server/routers.ts index 65effde..e84aff0 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -63,6 +63,9 @@ import { getServiceSignaturesByUser, upsertServiceSignature, deleteServiceSignature, + createBapHistoryEntry, + getBapHistoryByUser, + deleteBapHistoryEntry, } from "./db"; import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; @@ -386,28 +389,22 @@ export const appRouter = router({ return { success: true }; }), - validateBAP: protectedProcedure + 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" }); } @@ -415,24 +412,216 @@ export const appRouter = router({ throw new TRPCError({ code: "BAD_REQUEST", message: "Les champs Service, Type d'achat et Ventilation doivent être remplis" }); } + // ── Génération du PDF annoté ────────────────────────────────────── + const fs = await import('fs/promises'); + const path = await import('path'); + const { PDFDocument, rgb, StandardFonts } = await import('pdf-lib'); + const { storagePut } = await import('./storage'); + + const importSettings = await getImportSettingsByUser(ctx.user.id); + const bapExportMode = importSettings?.bapExportMode || 'browser'; + const exportFolder = importSettings?.exportFolder || null; + const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), 'storage'); + + let pdfUrl: string | null = null; + let exportPath: string | null = null; + let signatureName: string | null = null; + + try { + if (!invoice.fileKey) throw new Error('Fichier PDF source introuvable'); + const sourcePath = path.join(STORAGE_BASE_PATH, invoice.fileKey); + const pdfBytes = await fs.readFile(sourcePath); + const pdfDoc = await PDFDocument.load(pdfBytes); + const pages = pdfDoc.getPages(); + const lastPage = pages[pages.length - 1]; + const { width, height } = lastPage.getSize(); + + // ── Zone blanche BAP (bas de page, hauteur 140pt) ──────────────── + const zoneHeight = 140; + const zoneX = 30; + const zoneY = 10; + const zoneW = width - 60; + + // Fond blanc + lastPage.drawRectangle({ + x: zoneX, + y: zoneY, + width: zoneW, + height: zoneHeight, + color: rgb(1, 1, 1), + borderColor: rgb(0.7, 0.7, 0.7), + borderWidth: 0.5, + }); + + const font = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + const fontNormal = await pdfDoc.embedFont(StandardFonts.Helvetica); + + // Ligne 1 : CAPEX/OPEX | BAP | Destinataire + const typeAchatText = (invoice.typeAchat || 'N/A').toUpperCase(); + const recipientRaw = (invoice as any).recipientName || ''; + const destinataireText = recipientRaw ? recipientRaw : 'TOUS'; + const line1 = `${typeAchatText} | BON À PAYER | ${destinataireText}`; + const line1Size = 11; + const line1W = font.widthOfTextAtSize(line1, line1Size); + lastPage.drawText(line1, { + x: zoneX + (zoneW - line1W) / 2, + y: zoneY + zoneHeight - 22, + size: line1Size, + font, + color: rgb(0.1, 0.1, 0.5), + }); + + // Séparateur + lastPage.drawLine({ + start: { x: zoneX + 10, y: zoneY + zoneHeight - 30 }, + end: { x: zoneX + zoneW - 10, y: zoneY + zoneHeight - 30 }, + thickness: 0.5, + color: rgb(0.7, 0.7, 0.7), + }); + + // Ligne 2 : Service + Ventilation + const line2 = `Service : ${invoice.serviceConcerne || '-'} | Ventilation : ${invoice.ventilationComptable || '-'}`; + lastPage.drawText(line2, { + x: zoneX + 10, + y: zoneY + zoneHeight - 48, + size: 9, + font: fontNormal, + color: rgb(0.2, 0.2, 0.2), + }); + + // Ligne 3 : Date de validation + const now = new Date(); + const dateStr = now.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric' }); + const timeStr = now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); + lastPage.drawText(`Validé le ${dateStr} à ${timeStr}`, { + x: zoneX + 10, + y: zoneY + zoneHeight - 64, + size: 8, + font: fontNormal, + color: rgb(0.4, 0.4, 0.4), + }); + + // ── Signature du service ────────────────────────────────────────── + const serviceAssociations = await getServiceSignaturesByUser(ctx.user.id); + const serviceName = invoice.serviceConcerne || ''; + const assoc = serviceAssociations.find( + a => a.serviceName.toLowerCase() === serviceName.toLowerCase() + ); + if (assoc) { + const sig = await getSignatureById(assoc.signatureId); + if (sig) { + signatureName = `${sig.firstName} ${sig.lastName}`; + try { + const sigImagePath = path.join(STORAGE_BASE_PATH, sig.imageKey); + const sigImageBytes = await fs.readFile(sigImagePath); + const mimeType = sig.imageKey.toLowerCase().endsWith('.png') ? 'image/png' : 'image/jpeg'; + let embeddedSig; + if (mimeType === 'image/png') { + embeddedSig = await pdfDoc.embedPng(sigImageBytes); + } else { + embeddedSig = await pdfDoc.embedJpg(sigImageBytes); + } + const sigWidth = 100; + const sigHeight = 45; + lastPage.drawImage(embeddedSig, { + x: zoneX + zoneW - sigWidth - 10, + y: zoneY + 20, + width: sigWidth, + height: sigHeight, + }); + lastPage.drawText(signatureName, { + x: zoneX + zoneW - sigWidth - 10, + y: zoneY + 12, + size: 8, + font: fontNormal, + color: rgb(0.3, 0.3, 0.3), + }); + } catch (_) { /* ignore signature errors */ } + } + } + + const signedPdfBytes = await pdfDoc.save(); + const filename = path.basename(invoice.fileKey); + const bapFilename = `BAP_${Date.now()}_${filename}`; + + if (bapExportMode === 'folder' && exportFolder) { + // Mode dossier : enregistrer sur le disque + await fs.mkdir(exportFolder, { recursive: true }); + exportPath = path.join(exportFolder, bapFilename); + await fs.writeFile(exportPath, signedPdfBytes); + } else { + // Mode navigateur : uploader sur S3 et retourner l'URL + const { storagePut: put } = await import('./storage'); + const { url } = await put(`bap-exports/${bapFilename}`, Buffer.from(signedPdfBytes), 'application/pdf'); + pdfUrl = url; + } + } catch (pdfError: any) { + console.warn('[BAP] Erreur génération PDF:', pdfError.message); + // On continue même si le PDF échoue — on valide quand même + } + + // ── Mise à jour de la facture ───────────────────────────────────── + const validatedAt = new Date(); await updateInvoice(input.id, { bapValidated: 1, - bapValidatedAt: new Date(), + bapValidatedAt: validatedAt, }); - return { success: true, validatedAt: new Date() }; + // ── Enregistrement dans l'historique BAP ───────────────────────── + await createBapHistoryEntry({ + userId: ctx.user.id, + invoiceId: invoice.id, + supplierName: invoice.supplierName || null, + invoiceNumber: invoice.invoiceNumber || null, + invoiceDate: invoice.invoiceDate || null, + totalAmount: invoice.totalAmount ? String(invoice.totalAmount) : null, + typeAchat: invoice.typeAchat || null, + serviceConcerne: invoice.serviceConcerne || null, + ventilationComptable: invoice.ventilationComptable || null, + recipientName: (invoice as any).recipientName || null, + exportMode: bapExportMode === 'folder' ? 'folder' : 'browser', + exportPath: exportPath || null, + pdfUrl: pdfUrl || null, + signatureName: signatureName || null, + validatedAt, + }); + + return { + success: true, + validatedAt, + pdfUrl, + exportPath, + exportMode: bapExportMode, + }; }), - - search: protectedProcedure + + // ── Historique BAP ──────────────────────────────────────────────────── + search: protectedProcedure .input(z.object({ query: z.string() })) .query(async ({ input, ctx }) => { return searchInvoices(ctx.user.id, input.query); }), - + getStats: protectedProcedure.query(async ({ ctx }) => { return getInvoiceStats(ctx.user.id); }), }), + + // ============= BAP HISTORY ROUTES ============= + bapHistory: router({ + getAll: protectedProcedure.query(async ({ ctx }) => { + return getBapHistoryByUser(ctx.user.id); + }), + delete: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input, ctx }) => { + const entries = await getBapHistoryByUser(ctx.user.id); + const entry = entries.find(e => e.id === input.id); + if (!entry) throw new TRPCError({ code: 'NOT_FOUND' }); + await deleteBapHistoryEntry(input.id); + return { success: true }; + }), + }), // ============= SOURCE FILES ROUTES ============= sourceFiles: router({ @@ -888,6 +1077,7 @@ export const appRouter = router({ emailImportPort: 993, emailImportFrequency: 30, exportFolder: null, + bapExportMode: "browser" as const, }; } @@ -907,6 +1097,7 @@ export const appRouter = router({ emailImportPort: z.number().min(1).max(65535).optional(), emailImportFrequency: z.number().min(1).optional(), exportFolder: z.string().nullable().optional(), + bapExportMode: z.enum(["browser", "folder"]).optional(), })) .mutation(async ({ input, ctx }) => { const settings = await upsertImportSettings({ diff --git a/todo.md b/todo.md index 8692a9e..33ea972 100644 --- a/todo.md +++ b/todo.md @@ -586,3 +586,14 @@ - [x] Ajouter graphique Top destinataires dans Dashboard.tsx - [x] Ajouter filtre destinataire dans la section SFTP de Settings.tsx - [x] Mettre à jour la route settings.upsert pour sftpRecipientFilter + +## Améliorations Factures BAP + Export PDF annoté + Historique BAP +- [x] Supprimer la colonne "Abonnement" de la fenêtre Factures BAP +- [x] Ajouter le choix du mode d'export dans les paramètres (dossier local ou ouverture navigateur) +- [x] Ajouter colonne bapExportMode dans importSettings (schéma DB + migration) +- [x] Générer PDF annoté lors du clic BAP : zone blanche avec CAPEX/OPEX, BAP, destinataire, signature du service +- [x] Exporter ou ouvrir le PDF selon le mode d'export configuré +- [x] Créer la page Historique BAP dans le menu Traçabilité +- [x] Ajouter table bapHistory en base de données (schéma + migration) +- [x] Enregistrer chaque validation BAP dans l'historique +- [x] Ajouter la route dans App.tsx et le lien dans le menu Traçabilité