From 33a82e2ad219b2141dfc42c46057493f0af6ab17 Mon Sep 17 00:00:00 2001 From: Manus Date: Sat, 14 Mar 2026 18:52:47 -0400 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Ajout=20de=20l'onglet=20"Signatur?= =?UTF-8?q?es"=20dans=20la=20page=20Param=C3=A8tres=20:=20table=20DB=20sig?= =?UTF-8?q?natures=20(firstName,=20lastName,=20imageKey,=20imageUrl),=20mi?= =?UTF-8?q?gration=20appliqu=C3=A9e,=20routes=20tRPC=20(list,=20upload,=20?= =?UTF-8?q?create,=20delete),=20composant=20SignaturesSection=20avec=20for?= =?UTF-8?q?mulaire=20d'ajout=20(pr=C3=A9nom+nom+upload=20image),=20grille?= =?UTF-8?q?=20d'affichage=20des=20signatures=20avec=20aper=C3=A7u,=20bouto?= =?UTF-8?q?n=20de=20suppression=20au=20survol?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/pages/Settings.tsx | 243 ++++- drizzle/0013_absent_santa_claus.sql | 11 + drizzle/meta/0013_snapshot.json | 1379 +++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 17 + server/db.ts | 36 +- server/routers.ts | 63 ++ todo.md | 11 + 8 files changed, 1764 insertions(+), 3 deletions(-) create mode 100644 drizzle/0013_absent_santa_claus.sql create mode 100644 drizzle/meta/0013_snapshot.json diff --git a/client/src/pages/Settings.tsx b/client/src/pages/Settings.tsx index f18d21d..3773b9f 100644 --- a/client/src/pages/Settings.tsx +++ b/client/src/pages/Settings.tsx @@ -18,12 +18,239 @@ import { Server, FileCheck, AlertCircle, - Sparkles + Sparkles, + PenLine, + Plus, + Trash2, + Upload, + User } from "lucide-react"; +import { useRef, useState as useStateAlias } from "react"; import { toast } from "sonner"; import { Checkbox } from "@/components/ui/checkbox"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +function SignaturesSection() { + const utils = trpc.useUtils(); + const { data: signatures, isLoading } = trpc.signatures.list.useQuery(); + + const [firstName, setFirstName] = useStateAlias(""); + const [lastName, setLastName] = useStateAlias(""); + const [previewUrl, setPreviewUrl] = useStateAlias(null); + const [fileData, setFileData] = useStateAlias(null); + const [fileName, setFileName] = useStateAlias(""); + const [mimeType, setMimeType] = useStateAlias("image/png"); + const fileInputRef = useRef(null); + + const uploadMutation = trpc.signatures.upload.useMutation({ + onSuccess: () => { + toast.success("Signature ajoutée avec succès !"); + utils.signatures.list.invalidate(); + setFirstName(""); + setLastName(""); + setPreviewUrl(null); + setFileData(null); + setFileName(""); + }, + onError: (error) => { + toast.error(error.message || "Erreur lors de l'ajout de la signature"); + }, + }); + + const deleteMutation = trpc.signatures.delete.useMutation({ + onSuccess: () => { + toast.success("Signature supprimée"); + utils.signatures.list.invalidate(); + }, + onError: (error) => { + toast.error(error.message || "Erreur lors de la suppression"); + }, + }); + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + if (!file.type.startsWith("image/")) { + toast.error("Veuillez sélectionner une image (PNG, JPG, GIF...)"); + return; + } + if (file.size > 2 * 1024 * 1024) { + toast.error("L'image ne doit pas dépasser 2 Mo"); + return; + } + setFileName(file.name); + setMimeType(file.type); + const reader = new FileReader(); + reader.onload = (ev) => { + const result = ev.target?.result as string; + // result is "data:image/png;base64,XXXX" + const base64 = result.split(",")[1]; + setFileData(base64); + setPreviewUrl(result); + }; + reader.readAsDataURL(file); + }; + + const handleAdd = () => { + if (!firstName.trim() || !lastName.trim()) { + toast.error("Veuillez renseigner le prénom et le nom"); + return; + } + if (!fileData) { + toast.error("Veuillez sélectionner une image de signature"); + return; + } + uploadMutation.mutate({ firstName: firstName.trim(), lastName: lastName.trim(), fileName, fileData, mimeType }); + }; + + return ( +
+ {/* Header */} + + +
+
+ +
+
+ Gestion des signatures + + Ajoutez les signatures des responsables pour les documents officiels + +
+
+
+ + {/* Add form */} +
+

Ajouter une signature

+
+
+ + setFirstName(e.target.value)} + placeholder="Ex : Jean" + className="h-10" + /> +
+
+ + setLastName(e.target.value)} + placeholder="Ex : Dupont" + className="h-10" + /> +
+
+
+ +
fileInputRef.current?.click()} + > + {previewUrl ? ( + Aperçu signature + ) : ( +
+ + Cliquez pour sélectionner une image (PNG, JPG, GIF...) + Taille max : 2 Mo +
+ )} + +
+ {previewUrl && ( + + )} +
+ +
+ + {/* Signatures list */} + {isLoading ? ( +
+ +
+ ) : !signatures || signatures.length === 0 ? ( +
+ +

Aucune signature enregistrée

+
+ ) : ( +
+

+ {signatures.length} signature{signatures.length > 1 ? "s" : ""} enregistrée{signatures.length > 1 ? "s" : ""} +

+
+ {signatures.map((sig) => ( +
+ {/* Signature image */} +
+ {`Signature { (e.target as HTMLImageElement).src = ""; }} + /> +
+ {/* Name */} +
+
+ +
+ {sig.firstName} {sig.lastName} +
+ {/* Date */} +

+ Ajoutée le {new Date(sig.createdAt).toLocaleDateString("fr-FR")} +

+ {/* Delete button */} + +
+ ))} +
+
+ )} +
+
+
+ ); +} + function LlmFieldsConfigSection() { const { data: fields, isLoading } = trpc.llmFieldsConfig.getAll.useQuery(); const utils = trpc.useUtils(); @@ -245,7 +472,7 @@ export default function Settings() { {/* Tabs Navigation */} - + )} + + + Signatures + {/* LLM Tab */} @@ -579,6 +813,11 @@ export default function Settings() { + + {/* Signatures Tab */} + + + {/* Save Button */} diff --git a/drizzle/0013_absent_santa_claus.sql b/drizzle/0013_absent_santa_claus.sql new file mode 100644 index 0000000..39f7cfd --- /dev/null +++ b/drizzle/0013_absent_santa_claus.sql @@ -0,0 +1,11 @@ +CREATE TABLE `signatures` ( + `id` int AUTO_INCREMENT NOT NULL, + `userId` int NOT NULL, + `firstName` varchar(100) NOT NULL, + `lastName` varchar(100) NOT NULL, + `imageKey` text NOT NULL, + `imageUrl` text NOT NULL, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `signatures_id` PRIMARY KEY(`id`) +); diff --git a/drizzle/meta/0013_snapshot.json b/drizzle/meta/0013_snapshot.json new file mode 100644 index 0000000..05f572f --- /dev/null +++ b/drizzle/meta/0013_snapshot.json @@ -0,0 +1,1379 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "7e37dc16-31a4-4186-a33f-0122d861a640", + "prevId": "85be3a43-0ed8-440e-8462-e100c14d0a04", + "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": {} + }, + "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 30f96d1..7ae38d8 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -92,6 +92,13 @@ "when": 1772788503373, "tag": "0012_left_madame_web", "breakpoints": true + }, + { + "idx": 13, + "version": "5", + "when": 1773528566252, + "tag": "0013_absent_santa_claus", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index c6ac797..a73ef7c 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -306,3 +306,20 @@ export const llmFieldsConfig = mysqlTable("llmFieldsConfig", { export type LlmFieldConfig = typeof llmFieldsConfig.$inferSelect; export type InsertLlmFieldConfig = typeof llmFieldsConfig.$inferInsert; + +/** + * Signatures table storing user signature images with first/last name + */ +export const signatures = mysqlTable("signatures", { + id: int("id").autoincrement().primaryKey(), + userId: int("userId").notNull(), + firstName: varchar("firstName", { length: 100 }).notNull(), + lastName: varchar("lastName", { length: 100 }).notNull(), + imageKey: text("imageKey").notNull(), // Local storage key + imageUrl: text("imageUrl").notNull(), // Public URL to the signature image + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type Signature = typeof signatures.$inferSelect; +export type InsertSignature = typeof signatures.$inferInsert; diff --git a/server/db.ts b/server/db.ts index d43e2a0..b80217e 100644 --- a/server/db.ts +++ b/server/db.ts @@ -32,7 +32,10 @@ import { AutomationRule, llmFieldsConfig, InsertLlmFieldConfig, - LlmFieldConfig + LlmFieldConfig, + signatures, + InsertSignature, + Signature } from "../drizzle/schema"; import { ENV } from './_core/env'; @@ -667,3 +670,34 @@ export async function initializeDefaultLlmFields(userId: number): Promise } } } + +// ============= SIGNATURES HELPERS ============= + +export async function getSignaturesByUser(userId: number): Promise { + const db = await getDb(); + if (!db) return []; + return db.select().from(signatures).where(eq(signatures.userId, userId)); +} + +export async function getSignatureById(id: number): Promise { + const db = await getDb(); + if (!db) return undefined; + const results = await db.select().from(signatures).where(eq(signatures.id, id)); + return results[0]; +} + +export async function createSignature(data: InsertSignature): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(signatures).values(data); + const insertId = (result[0] as any).insertId; + const created = await getSignatureById(insertId); + if (!created) throw new Error("Failed to retrieve created signature"); + return created; +} + +export async function deleteSignature(id: number): Promise { + const db = await getDb(); + if (!db) return; + await db.delete(signatures).where(eq(signatures.id, id)); +} diff --git a/server/routers.ts b/server/routers.ts index 2149276..b39cc51 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -56,6 +56,10 @@ import { createAutomationRule, updateAutomationRule, deleteAutomationRule, + getSignaturesByUser, + getSignatureById, + createSignature, + deleteSignature, } from "./db"; import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; @@ -1170,6 +1174,65 @@ export const appRouter = router({ }); }), }), + + // ============= SIGNATURES ROUTES ============= + signatures: router({ + list: protectedProcedure.query(async ({ ctx }) => { + return await getSignaturesByUser(ctx.user.id); + }), + + upload: protectedProcedure + .input(z.object({ + firstName: z.string().min(1).max(100), + lastName: z.string().min(1).max(100), + fileName: z.string().min(1), + fileData: z.string(), // Base64 encoded image + mimeType: z.string().default("image/png"), + })) + .mutation(async ({ input, ctx }) => { + const userId = ctx.user.id; + const fileBuffer = Buffer.from(input.fileData, "base64"); + const safeFileName = `${input.firstName}-${input.lastName}-${Date.now()}-${input.fileName}` + .replace(/[^a-zA-Z0-9._-]/g, "_"); + const imageKey = generateStorageKey(userId, safeFileName); + const result = await localStoragePut(imageKey, fileBuffer, input.mimeType); + return await createSignature({ + userId, + firstName: input.firstName, + lastName: input.lastName, + imageKey, + imageUrl: result.url, + }); + }), + + create: protectedProcedure + .input(z.object({ + firstName: z.string().min(1).max(100), + lastName: z.string().min(1).max(100), + imageKey: z.string().min(1), + imageUrl: z.string().min(1), + })) + .mutation(async ({ input, ctx }) => { + return await createSignature({ + userId: ctx.user.id, + firstName: input.firstName, + lastName: input.lastName, + imageKey: input.imageKey, + imageUrl: input.imageUrl, + }); + }), + + delete: protectedProcedure + .input(z.object({ id: z.number() })) + .mutation(async ({ input, ctx }) => { + const sig = await getSignatureById(input.id); + if (!sig || sig.userId !== ctx.user.id) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + await deleteSignature(input.id); + return { success: true }; + }), + }), }); export type AppRouter = typeof appRouter; diff --git a/todo.md b/todo.md index ae462ce..601c53f 100644 --- a/todo.md +++ b/todo.md @@ -508,3 +508,14 @@ - [x] Désactiver le bouton avec tooltip explicatif pour les factures non éligibles - [x] Afficher un toast de confirmation après validation - [x] Rafraîchir la liste après validation + +## Onglet Signatures dans Paramètres +- [x] Créer la table `signatures` dans drizzle/schema.ts (id, userId, firstName, lastName, imageKey, imageUrl, createdAt) +- [x] Appliquer la migration DB (pnpm db:push) +- [x] Créer les routes tRPC : signatures.list, signatures.upload, signatures.create, signatures.delete +- [x] Ajouter la route d'upload d'image de signature (stockage local base64) +- [x] Ajouter l'onglet "Signatures" dans la page Settings.tsx +- [x] Créer le formulaire d'ajout de signature (prénom, nom, upload image) +- [x] Afficher la liste des signatures avec aperçu de l'image en grille +- [x] Ajouter le bouton de suppression par signature (hover) +- [ ] Tester l'upload et l'affichage des signatures sur le VPS