From ee49992530d25facf9289e32513cea5e8eb6de0e Mon Sep 17 00:00:00 2001 From: Manus Date: Sat, 14 Mar 2026 19:28:50 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20de=20l'association=20Serv?= =?UTF-8?q?ice=E2=86=92Signature=20dans=20l'onglet=20Signatures=20des=20Pa?= =?UTF-8?q?ram=C3=A8tres=20et=20apposition=20automatique=20de=20la=20signa?= =?UTF-8?q?ture=20sur=20les=20PDFs=20export=C3=A9s=20lors=20de=20l'export?= =?UTF-8?q?=20BAP.=20Inclut=20:=20table=20serviceSignatures,=20routes=20tR?= =?UTF-8?q?PC,=20interface=20de=20configuration,=20et=20modification=20de?= =?UTF-8?q?=20exportToPdf=20pour=20apposer=20la=20signature=20en=20bas=20?= =?UTF-8?q?=C3=A0=20droite=20du=20PDF=20avec=20le=20nom=20du=20signataire.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Settings.tsx | 145 ++++ drizzle/0014_watery_talos.sql | 10 + drizzle/meta/0014_snapshot.json | 1450 +++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 21 + server/db.ts | 42 +- server/routers.ts | 111 ++- todo.md | 10 + 8 files changed, 1790 insertions(+), 6 deletions(-) create mode 100644 drizzle/0014_watery_talos.sql create mode 100644 drizzle/meta/0014_snapshot.json diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index 903e78c..36c1e10 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -402,10 +402,155 @@ function SignaturesSection() { )} + + {/* Service → Signature associations */} + ); } +function ServiceSignaturesSection({ signatures }: { signatures: Array<{ id: number; firstName: string; lastName: string; imageUrl: string }> }) { + const utils = trpc.useUtils(); + const { data: departments, isLoading: depsLoading } = trpc.departments.getByUser.useQuery(); + const { data: associations, isLoading: assocLoading } = trpc.serviceSignatures.list.useQuery(); + + const upsertMutation = trpc.serviceSignatures.upsert.useMutation({ + onSuccess: () => { + toast.success("Association enregistrée"); + utils.serviceSignatures.list.invalidate(); + }, + onError: (e) => toast.error(e.message || "Erreur lors de l'enregistrement"), + }); + + const deleteMutation = trpc.serviceSignatures.delete.useMutation({ + onSuccess: () => { + toast.success("Association supprimée"); + utils.serviceSignatures.list.invalidate(); + }, + onError: (e) => toast.error(e.message || "Erreur lors de la suppression"), + }); + + const getAssociation = (serviceName: string) => + associations?.find(a => a.serviceName.toLowerCase() === serviceName.toLowerCase()); + + if (depsLoading || assocLoading) { + return ( + + Association Service → Signature +
+
+ ); + } + + if (!departments || departments.length === 0) { + return ( + + + Association Service → Signature + Associez une signature à chaque service pour l'apposer automatiquement lors de l'export BAP + + +

Aucun service configuré. Ajoutez des services dans l'onglet Listes d'abord.

+
+
+ ); + } + + if (!signatures || signatures.length === 0) { + return ( + + + Association Service → Signature + Associez une signature à chaque service pour l'apposer automatiquement lors de l'export BAP + + +

Aucune signature enregistrée. Ajoutez des signatures ci-dessus d'abord.

+
+
+ ); + } + + return ( + + + Association Service → Signature + Associez une signature à chaque service pour l'apposer automatiquement lors de l'export PDF BAP + + + + + + Service + Signature associée + Aperçu + Action + + + + {departments.map((dept: { id: number; name: string; userId: number; createdAt: Date }) => { + const assoc = getAssociation(dept.name); + const currentSigId = assoc?.signatureId ?? 0; + return ( + + {dept.name} + + + + + {assoc && signatures.find(s => s.id === assoc.signatureId) ? ( +
+ s.id === assoc.signatureId)!.imageUrl} + alt="aperçu" + className="max-h-full max-w-full object-contain" + /> +
+ ) : ( + Aucune + )} +
+ + {assoc && ( + + )} + +
+ ); + })} +
+
+
+
+ ); +} + function LlmFieldsConfigSection() { const { data: fields, isLoading } = trpc.llmFieldsConfig.getAll.useQuery(); const utils = trpc.useUtils(); diff --git a/drizzle/0014_watery_talos.sql b/drizzle/0014_watery_talos.sql new file mode 100644 index 0000000..dd82c2f --- /dev/null +++ b/drizzle/0014_watery_talos.sql @@ -0,0 +1,10 @@ +CREATE TABLE `serviceSignatures` ( + `id` int AUTO_INCREMENT NOT NULL, + `userId` int NOT NULL, + `serviceName` varchar(100) NOT NULL, + `signatureId` int NOT NULL, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `serviceSignatures_id` PRIMARY KEY(`id`), + CONSTRAINT `user_service_unique` UNIQUE(`userId`,`serviceName`) +); diff --git a/drizzle/meta/0014_snapshot.json b/drizzle/meta/0014_snapshot.json new file mode 100644 index 0000000..d837e59 --- /dev/null +++ b/drizzle/meta/0014_snapshot.json @@ -0,0 +1,1450 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "879f5656-bb98-4ea3-ae6f-d3d82b4d420c", + "prevId": "7e37dc16-31a4-4186-a33f-0122d861a640", + "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": {} + }, + "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 + }, + "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 + }, + "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 + }, + "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 7ae38d8..c0ffae9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -99,6 +99,13 @@ "when": 1773528566252, "tag": "0013_absent_santa_claus", "breakpoints": true + }, + { + "idx": 14, + "version": "5", + "when": 1773530695041, + "tag": "0014_watery_talos", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index a73ef7c..cdeee7e 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -323,3 +323,24 @@ export const signatures = mysqlTable("signatures", { export type Signature = typeof signatures.$inferSelect; export type InsertSignature = typeof signatures.$inferInsert; + +/** + * Service-Signature association table + * Associates a department/service with a specific signature for PDF exports + */ +export const serviceSignatures = mysqlTable("serviceSignatures", { + id: int("id").autoincrement().primaryKey(), + userId: int("userId").notNull(), + serviceName: varchar("serviceName", { length: 100 }).notNull(), // Department name (e.g., "DSI", "TRAVAUX") + signatureId: int("signatureId").notNull(), // FK to signatures.id + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}, (table) => { + return { + // One association per service per user + userServiceIdx: uniqueIndex("user_service_unique").on(table.userId, table.serviceName), + }; +}); + +export type ServiceSignature = typeof serviceSignatures.$inferSelect; +export type InsertServiceSignature = typeof serviceSignatures.$inferInsert; diff --git a/server/db.ts b/server/db.ts index b80217e..0f3d0e9 100644 --- a/server/db.ts +++ b/server/db.ts @@ -35,7 +35,10 @@ import { LlmFieldConfig, signatures, InsertSignature, - Signature + Signature, + serviceSignatures, + InsertServiceSignature, + ServiceSignature } from "../drizzle/schema"; import { ENV } from './_core/env'; @@ -701,3 +704,40 @@ export async function deleteSignature(id: number): Promise { if (!db) return; await db.delete(signatures).where(eq(signatures.id, id)); } + +// ============= SERVICE SIGNATURES HELPERS ============= + +export async function getServiceSignaturesByUser(userId: number): Promise { + const db = await getDb(); + if (!db) return []; + return db.select().from(serviceSignatures).where(eq(serviceSignatures.userId, userId)); +} + +export async function getServiceSignatureByService(userId: number, serviceName: string): Promise { + const db = await getDb(); + if (!db) return undefined; + const results = await db.select().from(serviceSignatures) + .where(eq(serviceSignatures.userId, userId)) + .limit(1); + // Filter in JS to avoid SQL case sensitivity issues + return results.find(r => r.serviceName.toLowerCase() === serviceName.toLowerCase()); +} + +export async function upsertServiceSignature(data: InsertServiceSignature): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.insert(serviceSignatures).values(data) + .onDuplicateKeyUpdate({ + set: { signatureId: data.signatureId, updatedAt: new Date() } + }); +} + +export async function deleteServiceSignature(userId: number, serviceName: string): Promise { + const db = await getDb(); + if (!db) return; + const all = await db.select().from(serviceSignatures).where(eq(serviceSignatures.userId, userId)); + const match = all.find(r => r.serviceName.toLowerCase() === serviceName.toLowerCase()); + if (match) { + await db.delete(serviceSignatures).where(eq(serviceSignatures.id, match.id)); + } +} diff --git a/server/routers.ts b/server/routers.ts index b39cc51..59aeb19 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -60,6 +60,9 @@ import { getSignatureById, createSignature, deleteSignature, + getServiceSignaturesByUser, + upsertServiceSignature, + deleteServiceSignature, } from "./db"; import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; @@ -568,11 +571,15 @@ export const appRouter = router({ .mutation(async ({ input, ctx }) => { const fs = await import('fs/promises'); const path = await import('path'); + const { PDFDocument } = await import('pdf-lib'); // Get export folder from settings const settings = await getImportSettingsByUser(ctx.user.id); const exportFolder = settings?.exportFolder; + // Load service→signature associations + const serviceAssociations = await getServiceSignaturesByUser(ctx.user.id); + if (!exportFolder) { throw new TRPCError({ code: "BAD_REQUEST", @@ -622,9 +629,76 @@ export const appRouter = router({ const filename = path.basename(invoice.fileKey); const destPath = path.join(exportFolder, filename); - // Copy file - await fs.copyFile(sourcePath, destPath); - copiedFiles.push(destPath); + // Find signature associated with invoice service + const serviceName = invoice.serviceConcerne || ''; + const assoc = serviceAssociations.find( + a => a.serviceName.toLowerCase() === serviceName.toLowerCase() + ); + + if (assoc) { + // Load signature image and embed it in the PDF + try { + const sig = await getSignatureById(assoc.signatureId); + if (sig) { + const sigImagePath = path.join(STORAGE_BASE_PATH, sig.imageKey); + 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(); + + // Read signature image + const sigImageBytes = await fs.readFile(sigImagePath); + const sigImageBase64 = sigImageBytes.toString('base64'); + 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); + } + + // Draw signature in bottom-right corner + const sigWidth = 120; + const sigHeight = 50; + const margin = 30; + lastPage.drawImage(embeddedSig, { + x: width - sigWidth - margin, + y: margin, + width: sigWidth, + height: sigHeight, + }); + + // Add signer name below signature + const { StandardFonts } = await import('pdf-lib'); + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); + lastPage.drawText(`${sig.firstName} ${sig.lastName}`, { + x: width - sigWidth - margin, + y: margin - 14, + size: 9, + font, + }); + + const signedPdfBytes = await pdfDoc.save(); + await fs.writeFile(destPath, signedPdfBytes); + copiedFiles.push(destPath); + console.log(`[Export] Signature apposée pour ${serviceName} sur ${filename}`); + } else { + // Signature not found, copy without signature + await fs.copyFile(sourcePath, destPath); + copiedFiles.push(destPath); + } + } catch (sigError: any) { + console.warn(`[Export] Impossible d'apposer la signature: ${sigError.message}. Copie sans signature.`); + await fs.copyFile(sourcePath, destPath); + copiedFiles.push(destPath); + } + } else { + // No signature association, copy as-is + await fs.copyFile(sourcePath, destPath); + copiedFiles.push(destPath); + } // Update export status await updateInvoice(invoice.id, { @@ -1222,7 +1296,7 @@ export const appRouter = router({ }); }), - delete: protectedProcedure + delete: protectedProcedure .input(z.object({ id: z.number() })) .mutation(async ({ input, ctx }) => { const sig = await getSignatureById(input.id); @@ -1233,6 +1307,33 @@ export const appRouter = router({ return { success: true }; }), }), -}); + // ============= SERVICE SIGNATURES ROUTES ============= + serviceSignatures: router({ + list: protectedProcedure.query(async ({ ctx }) => { + return await getServiceSignaturesByUser(ctx.user.id); + }), + + upsert: protectedProcedure + .input(z.object({ + serviceName: z.string().min(1).max(100), + signatureId: z.number(), + })) + .mutation(async ({ input, ctx }) => { + await upsertServiceSignature({ + userId: ctx.user.id, + serviceName: input.serviceName, + signatureId: input.signatureId, + }); + return { success: true }; + }), + + delete: protectedProcedure + .input(z.object({ serviceName: z.string() })) + .mutation(async ({ input, ctx }) => { + await deleteServiceSignature(ctx.user.id, input.serviceName); + return { success: true }; + }), + }), +}); export type AppRouter = typeof appRouter; diff --git a/todo.md b/todo.md index c31b1a5..e431759 100644 --- a/todo.md +++ b/todo.md @@ -531,3 +531,13 @@ ## Correction affichage canvas signature - [x] Corriger le problème d'initialisation du canvas (fond blanc non affiché) - [x] S'assurer que le canvas est visible et fonctionnel dans l'onglet Signatures + +## Association Service → Signature et export PDF avec signature +- [x] Créer la table `serviceSignatures` dans drizzle/schema.ts (serviceName → signatureId) +- [x] Appliquer la migration DB (pnpm db:push) +- [x] Créer les routes tRPC : serviceSignatures.list, serviceSignatures.upsert, serviceSignatures.delete +- [x] Ajouter une section "Association Service → Signature" dans l'onglet Signatures des Paramètres +- [x] Afficher la liste des services disponibles avec un sélecteur de signature pour chacun +- [x] Modifier la route exportToPdf pour récupérer la signature associée au service de la facture +- [x] Apposer la signature sur le PDF exporté (image en bas à droite + nom du signataire) +- [ ] Tester l'export PDF avec signature automatique