From 582ca77f2867eb7c071571a384ba6455e65c3acb Mon Sep 17 00:00:00 2001 From: Manus Date: Wed, 2 Sep 2026 12:13:23 +0000 Subject: [PATCH] =?UTF-8?q?Checkpoint:=20Automatismes=20s=C3=A9par=C3=A9?= =?UTF-8?q?=20en=20onglets=20Import=20et=20Export.=20Les=20r=C3=A8gles=20d?= =?UTF-8?q?=E2=80=99export=20ciblent=20un=20destinataire=20ou=20une=20vent?= =?UTF-8?q?ilation,=20avec=20destination=20locale,=20Teams=20ou=20SharePoi?= =?UTF-8?q?nt=20et=20ouverture=20navigateur=20optionnelle.=20La=20validati?= =?UTF-8?q?on=20BAP=20unique=20ou=20en=20masse=20applique=20la=20premi?= =?UTF-8?q?=C3=A8re=20r=C3=A8gle=20active=20par=20priorit=C3=A9=20puis=20c?= =?UTF-8?q?onserve=20le=20param=C3=A9trage=20historique=20comme=20repli.?= =?UTF-8?q?=20Param=C3=A8tres=20d=E2=80=99export=20d=C3=A9plac=C3=A9s=20ve?= =?UTF-8?q?rs=20Automatismes=20et=20page=20renomm=C3=A9e=20Param=C3=A8tres?= =?UTF-8?q?.=20Migration=20additive,=20tests,=20TypeScript,=20build=20et?= =?UTF-8?q?=20contr=C3=B4les=20visuels=20valid=C3=A9s.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/DashboardLayout.tsx | 2 +- .../components/ExportAutomationRulesPanel.tsx | 186 ++ client/src/pages/AutomationRules.tsx | 15 +- client/src/pages/ImportSettings.tsx | 14 +- drizzle/0039_good_carmella_unuscione.sql | 17 + drizzle/meta/0039_snapshot.json | 2496 +++++++++++++++++ drizzle/meta/_journal.json | 7 + drizzle/schema.ts | 27 +- server/db.ts | 44 + server/exportAutomation.test.ts | 26 + server/exportDestinationResolver.ts | 42 + server/routers.ts | 79 +- shared/exportAutomation.ts | 32 + todo.md | 8 + verification_notes.md | 6 + 15 files changed, 2978 insertions(+), 23 deletions(-) create mode 100644 client/src/components/ExportAutomationRulesPanel.tsx create mode 100644 drizzle/0039_good_carmella_unuscione.sql create mode 100644 drizzle/meta/0039_snapshot.json create mode 100644 server/exportAutomation.test.ts create mode 100644 server/exportDestinationResolver.ts create mode 100644 shared/exportAutomation.ts diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index 3f89d3e..fcf5043 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -71,7 +71,7 @@ const menuStructure: MenuItem[] = [ color: "from-orange-500 to-amber-500", children: [ { icon: Settings, label: "Paramètres IA et Signatures", path: "/settings" }, - { icon: Download, label: "Paramètres import / export", path: "/import-settings" }, + { icon: Download, label: "Paramètres", path: "/import-settings" }, { icon: List, label: "Administration des listes", path: "/lists-admin" }, { icon: Zap, label: "Automatismes", path: "/automation-rules" }, { icon: Brain, label: "Apprentissages IA", path: "/learning-settings" }, diff --git a/client/src/components/ExportAutomationRulesPanel.tsx b/client/src/components/ExportAutomationRulesPanel.tsx new file mode 100644 index 0000000..5fc7a24 --- /dev/null +++ b/client/src/components/ExportAutomationRulesPanel.tsx @@ -0,0 +1,186 @@ +import { useEffect, useMemo, useState } from "react"; +import { FolderOpen, Monitor, Pencil, Plus, Power, PowerOff, Save, Trash2 } from "lucide-react"; +import { trpc } from "@/lib/trpc"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Switch } from "@/components/ui/switch"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { toast } from "sonner"; + +type ConditionField = "recipientName" | "ventilationComptable"; +type DestinationType = "local" | "teams" | "sharepoint"; + +const LABELS: Record = { + recipientName: "Destinataire", + ventilationComptable: "Ventilation", +}; + +const DESTINATION_LABELS: Record = { + local: "Dossier local", + teams: "Teams", + sharepoint: "SharePoint", +}; + +export default function ExportAutomationRulesPanel() { + const utils = trpc.useUtils(); + const { data: rules = [], isLoading } = trpc.exportAutomationRules.list.useQuery(); + const { data: invoices = [] } = trpc.invoices.list.useQuery(); + const { data: importSettings } = trpc.importSettings.get.useQuery(); + const [editingId, setEditingId] = useState(null); + const [name, setName] = useState(""); + const [conditionField, setConditionField] = useState("recipientName"); + const [conditionValue, setConditionValue] = useState(""); + const [destinationType, setDestinationType] = useState("local"); + const [destinationPath, setDestinationPath] = useState(""); + const [openInBrowser, setOpenInBrowser] = useState(false); + const [defaultExportMode, setDefaultExportMode] = useState<"browser" | "folder" | "both">("browser"); + const [defaultDestinationType, setDefaultDestinationType] = useState("local"); + const [defaultDestinationPath, setDefaultDestinationPath] = useState(""); + const [azureTenantId, setAzureTenantId] = useState(""); + const [azureClientId, setAzureClientId] = useState(""); + const [azureClientSecret, setAzureClientSecret] = useState(""); + + useEffect(() => { + if (!importSettings) return; + setDefaultExportMode(importSettings.bapExportMode || "browser"); + setDefaultDestinationType((importSettings.exportFolderType || "local") as DestinationType); + setDefaultDestinationPath(importSettings.exportFolder || ""); + setAzureTenantId(importSettings.azureTenantId || ""); + setAzureClientId(importSettings.azureClientId || ""); + }, [importSettings]); + + const conditionValues = useMemo(() => { + const field = conditionField === "recipientName" ? "recipientName" : "ventilationComptable"; + return Array.from(new Set(invoices.map((invoice: any) => invoice[field]).filter(Boolean))).sort(); + }, [conditionField, invoices]); + + const resetForm = () => { + setEditingId(null); + setName(""); + setConditionField("recipientName"); + setConditionValue(""); + setDestinationType("local"); + setDestinationPath(""); + setOpenInBrowser(false); + }; + + const createMutation = trpc.exportAutomationRules.create.useMutation({ + onSuccess: () => { + toast.success("Règle d’export créée"); + utils.exportAutomationRules.list.invalidate(); + resetForm(); + }, + onError: (error) => toast.error(error.message || "Impossible de créer la règle"), + }); + const updateMutation = trpc.exportAutomationRules.update.useMutation({ + onSuccess: () => { + toast.success("Règle d’export mise à jour"); + utils.exportAutomationRules.list.invalidate(); + resetForm(); + }, + onError: (error) => toast.error(error.message || "Impossible de mettre à jour la règle"), + }); + const deleteMutation = trpc.exportAutomationRules.delete.useMutation({ + onSuccess: () => { + toast.success("Règle d’export supprimée"); + utils.exportAutomationRules.list.invalidate(); + }, + onError: (error) => toast.error(error.message || "Impossible de supprimer la règle"), + }); + const updateDefaultExportMutation = trpc.importSettings.update.useMutation({ + onSuccess: () => { + toast.success("Paramètres d’export enregistrés"); + setAzureClientSecret(""); + utils.importSettings.get.invalidate(); + }, + onError: (error) => toast.error(error.message || "Impossible d’enregistrer les paramètres d’export"), + }); + + const saveRule = () => { + if (!name.trim() || !conditionValue.trim() || !destinationPath.trim()) { + toast.error("Nom, valeur de condition et destination sont obligatoires"); + return; + } + const payload = { + name: name.trim(), + conditionField, + conditionValue: conditionValue.trim(), + destinationType, + destinationPath: destinationPath.trim(), + openInBrowser: openInBrowser ? 1 : 0, + }; + if (editingId) updateMutation.mutate({ id: editingId, ...payload }); + else createMutation.mutate({ ...payload, isActive: 1, priority: rules.length }); + }; + + const editRule = (rule: any) => { + setEditingId(rule.id); + setName(rule.name); + setConditionField(rule.conditionField); + setConditionValue(rule.conditionValue); + setDestinationType(rule.destinationType); + setDestinationPath(rule.destinationPath); + setOpenInBrowser(rule.openInBrowser === 1); + }; + + const isSaving = createMutation.isPending || updateMutation.isPending; + const isMicrosoftDestination = destinationType === "teams" || destinationType === "sharepoint"; + + const saveDefaultExport = () => { + if (!importSettings) return; + if (defaultExportMode !== "browser" && !defaultDestinationPath.trim()) { + toast.error("Une destination est obligatoire lorsque l’export vers un dossier est activé"); + return; + } + updateDefaultExportMutation.mutate({ + manualImportEnabled: importSettings.manualImportEnabled, + autoImportEnabled: importSettings.autoImportEnabled, + emailImportEnabled: importSettings.emailImportEnabled, + exportFolder: defaultDestinationPath.trim() || null, + exportFolderType: defaultDestinationType, + bapExportMode: defaultExportMode, + azureTenantId: azureTenantId.trim() || null, + azureClientId: azureClientId.trim() || null, + ...(azureClientSecret ? { azureClientSecret } : {}), + }); + }; + + return ( +
+ + Paramètres d’export par défautIls s’appliquent lorsqu’aucun automatisme d’export ne correspond à la facture BAP. + +
+
setDefaultDestinationPath(event.target.value)} placeholder={defaultDestinationType === "local" ? "/chemin/serveur/factures-bap" : "https://…sharepoint.com/sites/…/Documents/Factures"} />
+ {(defaultDestinationType === "teams" || defaultDestinationType === "sharepoint") &&
setAzureTenantId(event.target.value)} />
setAzureClientId(event.target.value)} />
setAzureClientSecret(event.target.value)} placeholder="Conserver le secret actuel" />

Teams utilise l’URL SharePoint du dossier Fichiers du canal. Laissez le secret vide pour conserver celui déjà enregistré.

} + +
+
+ + + Nouvelle règle de destination + La première règle active par priorité qui correspond à la facture est appliquée lors de la validation BAP. + + +
+
setName(event.target.value)} placeholder="Ex. DSI SANTINOVA vers SharePoint" />
+
+
+
setConditionValue(event.target.value)} placeholder={conditionField === "recipientName" ? "Choisir ou saisir un destinataire" : "Choisir ou saisir une ventilation"} />{conditionValues.map((value) =>
+
{(["local", "teams", "sharepoint"] as DestinationType[]).map((type) => )}
+
setDestinationPath(event.target.value)} placeholder={isMicrosoftDestination ? "https://…sharepoint.com/sites/…/Documents/Factures" : "/chemin/serveur/factures-bap"} />

{isMicrosoftDestination ? "Pour Teams, indiquez l’URL SharePoint du dossier Fichiers du canal. Les droits Microsoft existants sont réutilisés." : "Ce chemin est créé si nécessaire sur le serveur qui héberge l’application."}

+

Conserve le téléchargement dans le navigateur en complément de la destination définie.

+
{editingId && }
+
+
+ + Règles d’exportLes règles inactives restent enregistrées mais ne sont jamais appliquées. + {isLoading ?

Chargement…

: rules.length === 0 ?

Aucune règle de destination. Les paramètres existants restent le comportement de repli.

: NomConditionDestinationStatutActions{rules.map((rule: any) => {rule.name}{rule.openInBrowser === 1 && Navigateur}{LABELS[rule.conditionField as ConditionField]} : {rule.conditionValue}{DESTINATION_LABELS[rule.destinationType as DestinationType]}
{rule.destinationPath}
{rule.isActive === 1 ? Actif : Inactif}
)}
}
+
+
+ ); +} diff --git a/client/src/pages/AutomationRules.tsx b/client/src/pages/AutomationRules.tsx index c4021d4..f022866 100644 --- a/client/src/pages/AutomationRules.tsx +++ b/client/src/pages/AutomationRules.tsx @@ -33,6 +33,8 @@ import { } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { trpc } from "@/lib/trpc"; +import ExportAutomationRulesPanel from "@/components/ExportAutomationRulesPanel"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { AUTOMATION_ACTION_FILTERS, type AutomationActionFilter, @@ -337,7 +339,7 @@ export default function AutomationRules() {

Automatismes

-

Gérez les règles de remplissage automatique des champs

+

Gérez séparément le classement à l’import et les destinations d’export

+ + + Automatismes d’import + Automatismes d’export + + Règles d'automatisme @@ -480,6 +488,11 @@ export default function AutomationRules() { )} + + + + + {/* Create/Edit Dialog */} diff --git a/client/src/pages/ImportSettings.tsx b/client/src/pages/ImportSettings.tsx index 5a6b9d3..51782b5 100644 --- a/client/src/pages/ImportSettings.tsx +++ b/client/src/pages/ImportSettings.tsx @@ -333,25 +333,21 @@ export default function ImportSettings() {

- Paramètres import / export + Paramètres

- Configurez les méthodes d'importation et les options d'export des factures BAP + Configurez les méthodes d’importation et la sauvegarde de la base de données

- {/* Onglets Import / Export */} + {/* Les destinations d’export sont administrées dans Automatismes d’export. */} - + - + Paramètres d'import - - - Paramètres d'export - Sauvegarde DB diff --git a/drizzle/0039_good_carmella_unuscione.sql b/drizzle/0039_good_carmella_unuscione.sql new file mode 100644 index 0000000..83461fd --- /dev/null +++ b/drizzle/0039_good_carmella_unuscione.sql @@ -0,0 +1,17 @@ +CREATE TABLE `exportAutomationRules` ( + `id` int AUTO_INCREMENT NOT NULL, + `userId` int NOT NULL, + `name` varchar(255) NOT NULL, + `isActive` int NOT NULL DEFAULT 1, + `priority` int NOT NULL DEFAULT 0, + `conditionField` enum('recipientName','ventilationComptable') NOT NULL, + `conditionValue` varchar(255) NOT NULL, + `destinationType` enum('local','teams','sharepoint') NOT NULL, + `destinationPath` text NOT NULL, + `openInBrowser` int NOT NULL DEFAULT 0, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `exportAutomationRules_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE INDEX `export_rule_user_priority_idx` ON `exportAutomationRules` (`userId`,`priority`); \ No newline at end of file diff --git a/drizzle/meta/0039_snapshot.json b/drizzle/meta/0039_snapshot.json new file mode 100644 index 0000000..83c10b5 --- /dev/null +++ b/drizzle/meta/0039_snapshot.json @@ -0,0 +1,2496 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "bc3f9af1-faa6-46bf-85fd-2e9906fa34d1", + "prevId": "be9b9ca2-8898-431d-9945-887bff1f16bb", + "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 + }, + "sharepointUploadStatus": { + "name": "sharepointUploadStatus", + "type": "enum('success','error','skipped')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadPath": { + "name": "sharepointUploadPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadError": { + "name": "sharepointUploadError", + "type": "text", + "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": {} + }, + "deletedInvoices": { + "name": "deletedInvoices", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "invoiceNumber": { + "name": "invoiceNumber", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "totalAmount": { + "name": "totalAmount", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supplierName": { + "name": "supplierName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deletedAt": { + "name": "deletedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "deletedInvoices_id": { + "name": "deletedInvoices_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": {} + }, + "exportAutomationRules": { + "name": "exportAutomationRules", + "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 + }, + "conditionField": { + "name": "conditionField", + "type": "enum('recipientName','ventilationComptable')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "conditionValue": { + "name": "conditionValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destinationType": { + "name": "destinationType", + "type": "enum('local','teams','sharepoint')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "destinationPath": { + "name": "destinationPath", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "openInBrowser": { + "name": "openInBrowser", + "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": { + "export_rule_user_priority_idx": { + "name": "export_rule_user_priority_idx", + "columns": [ + "userId", + "priority" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": { + "exportAutomationRules_id": { + "name": "exportAutomationRules_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "freeproImports": { + "name": "freeproImports", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "moisLabel": { + "name": "moisLabel", + "type": "varchar(10)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "annee": { + "name": "annee", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mois": { + "name": "mois", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refPiece": { + "name": "refPiece", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "nbLignes": { + "name": "nbLignes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "totalTtc": { + "name": "totalTtc", + "type": "varchar(30)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadStatus": { + "name": "sharepointUploadStatus", + "type": "enum('success','error')", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadPath": { + "name": "sharepointUploadPath", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointUploadError": { + "name": "sharepointUploadError", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sharepointExportedAt": { + "name": "sharepointExportedAt", + "type": "timestamp", + "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": { + "freeproImports_id": { + "name": "freeproImports_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "freeproSettings": { + "name": "freeproSettings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "portalUrl": { + "name": "portalUrl", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'https://pro.free.fr'" + }, + "loginEmail": { + "name": "loginEmail", + "type": "varchar(320)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "loginPassword": { + "name": "loginPassword", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "frequency": { + "name": "frequency", + "type": "enum('manual','daily','weekly','monthly')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'manual'" + }, + "maxAnteriority": { + "name": "maxAnteriority", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "autoEnabled": { + "name": "autoEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lastSuccessAt": { + "name": "lastSuccessAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastStatus": { + "name": "lastStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastImportCount": { + "name": "lastImportCount", + "type": "int", + "primaryKey": false, + "notNull": false, + "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": { + "freeproSettings_id": { + "name": "freeproSettings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": { + "freeproSettings_userId_unique": { + "name": "freeproSettings_userId_unique", + "columns": [ + "userId" + ] + } + }, + "checkConstraint": {} + }, + "freeproVentilationLines": { + "name": "freeproVentilationLines", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "importId": { + "name": "importId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "structure": { + "name": "structure", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "montantCentimes": { + "name": "montantCentimes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "freeproVentilationLines_id": { + "name": "freeproVentilationLines_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 + }, + "warningMessage": { + "name": "warningMessage", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "importSource": { + "name": "importSource", + "type": "enum('file','folder','email')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'file'" + }, + "importTrigger": { + "name": "importTrigger", + "type": "enum('manual','automatic','unknown')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "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 + }, + "emailImportSinceDate": { + "name": "emailImportSinceDate", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "emailImportAuthMode": { + "name": "emailImportAuthMode", + "type": "enum('basic','oauth2')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'basic'" + }, + "exportFolder": { + "name": "exportFolder", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "exportFolderType": { + "name": "exportFolderType", + "type": "enum('local','teams','sharepoint')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'local'" + }, + "bapExportMode": { + "name": "bapExportMode", + "type": "enum('browser','folder','both')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'browser'" + }, + "azureTenantId": { + "name": "azureTenantId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureClientId": { + "name": "azureClientId", + "type": "varchar(100)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureClientSecret": { + "name": "azureClientSecret", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "azureSecretExpiresAt": { + "name": "azureSecretExpiresAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "aiProvider": { + "name": "aiProvider", + "type": "enum('mistral','manus','gemini')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral'" + }, + "mistralApiKey": { + "name": "mistralApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiKey": { + "name": "manusForgeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiUrl": { + "name": "manusForgeApiUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "geminiApiKey": { + "name": "geminiApiKey", + "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": {} + }, + "invoiceLearnings": { + "name": "invoiceLearnings", + "columns": { + "id": { + "name": "id", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": true + }, + "userId": { + "name": "userId", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "supplierKey": { + "name": "supplierKey", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "fieldName": { + "name": "fieldName", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "originalValue": { + "name": "originalValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "correctedValue": { + "name": "correctedValue", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "applyCount": { + "name": "applyCount", + "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())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "invoiceLearnings_id": { + "name": "invoiceLearnings_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "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 + }, + "contentHash": { + "name": "contentHash", + "type": "varchar(64)", + "primaryKey": false, + "notNull": false, + "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": { + "source_file_content_hash_unique": { + "name": "source_file_content_hash_unique", + "columns": [ + "contentHash" + ], + "isUnique": true + } + }, + "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 + }, + "learningConfidenceThreshold": { + "name": "learningConfidenceThreshold", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 2 + }, + "aiProvider": { + "name": "aiProvider", + "type": "enum('mistral','manus','gemini')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'mistral'" + }, + "mistralApiKey": { + "name": "mistralApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiKey": { + "name": "manusForgeApiKey", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "manusForgeApiUrl": { + "name": "manusForgeApiUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "geminiApiKey": { + "name": "geminiApiKey", + "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": { + "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(128)", + "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": {} + }, + "webImportSources": { + "name": "webImportSources", + "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 + }, + "connectorType": { + "name": "connectorType", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "portalUrl": { + "name": "portalUrl", + "type": "varchar(500)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "loginEmail": { + "name": "loginEmail", + "type": "varchar(320)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "loginPassword": { + "name": "loginPassword", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "frequency": { + "name": "frequency", + "type": "enum('manual','daily','weekly','monthly')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'monthly'" + }, + "autoEnabled": { + "name": "autoEnabled", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "lastSuccessAt": { + "name": "lastSuccessAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastStatus": { + "name": "lastStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lastImportCount": { + "name": "lastImportCount", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": 0 + }, + "apiToken": { + "name": "apiToken", + "type": "varchar(128)", + "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": { + "webImportSources_id": { + "name": "webImportSources_id", + "columns": [ + "id" + ] + } + }, + "uniqueConstraints": {}, + "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 9e54473..21c4b0c 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -274,6 +274,13 @@ "when": 1788349114141, "tag": "0038_late_thunderbolt", "breakpoints": true + }, + { + "idx": 39, + "version": "5", + "when": 1788350633569, + "tag": "0039_good_carmella_unuscione", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 81c723c..09b5340 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -1,4 +1,4 @@ -import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, uniqueIndex, decimal } from "drizzle-orm/mysql-core"; +import { decimal, index, int, mysqlEnum, mysqlTable, text, timestamp, uniqueIndex, varchar } from "drizzle-orm/mysql-core"; /** * Core user table backing auth flow. @@ -324,6 +324,31 @@ export const automationRules = mysqlTable("automationRules", { export type AutomationRule = typeof automationRules.$inferSelect; export type InsertAutomationRule = typeof automationRules.$inferInsert; +/** + * Règles de destination appliquées lors de la validation BAP. + * Elles sont séparées des règles d’import, afin de ne jamais modifier les + * champs d’une facture au moment de son export. + */ +export const exportAutomationRules = mysqlTable("exportAutomationRules", { + id: int("id").autoincrement().primaryKey(), + userId: int("userId").notNull(), + name: varchar("name", { length: 255 }).notNull(), + isActive: int("isActive").default(1).notNull(), + priority: int("priority").default(0).notNull(), + conditionField: mysqlEnum("conditionField", ["recipientName", "ventilationComptable"]).notNull(), + conditionValue: varchar("conditionValue", { length: 255 }).notNull(), + destinationType: mysqlEnum("destinationType", ["local", "teams", "sharepoint"]).notNull(), + destinationPath: text("destinationPath").notNull(), + openInBrowser: int("openInBrowser").default(0).notNull(), + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}, (table) => ({ + userPriorityIdx: index("export_rule_user_priority_idx").on(table.userId, table.priority), +})); + +export type ExportAutomationRule = typeof exportAutomationRules.$inferSelect; +export type InsertExportAutomationRule = typeof exportAutomationRules.$inferInsert; + /** * LLM Fields Configuration table * Stores configuration for each field used in invoice extraction diff --git a/server/db.ts b/server/db.ts index 6f135c8..8347d5d 100644 --- a/server/db.ts +++ b/server/db.ts @@ -30,6 +30,9 @@ import { automationRules, InsertAutomationRule, AutomationRule, + exportAutomationRules, + InsertExportAutomationRule, + ExportAutomationRule, llmFieldsConfig, InsertLlmFieldConfig, LlmFieldConfig, @@ -747,6 +750,47 @@ export async function deleteAutomationRule(id: number): Promise { await db.delete(automationRules).where(eq(automationRules.id, id)); } +// ============= EXPORT AUTOMATION RULES OPERATIONS ============= + +export async function getExportAutomationRulesByUser(userId: number): Promise { + const db = await getDb(); + if (!db) return []; + return db.select().from(exportAutomationRules) + .where(eq(exportAutomationRules.userId, userId)) + .orderBy(exportAutomationRules.priority, exportAutomationRules.id); +} + +export async function getExportAutomationRuleById(id: number): Promise { + const db = await getDb(); + if (!db) return null; + const [rule] = await db.select().from(exportAutomationRules).where(eq(exportAutomationRules.id, id)); + return rule || null; +} + +export async function createExportAutomationRule(data: InsertExportAutomationRule): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + const result = await db.insert(exportAutomationRules).values(data); + const rule = await getExportAutomationRuleById(result[0].insertId); + if (!rule) throw new Error("Export automation rule could not be created"); + return rule; +} + +export async function updateExportAutomationRule(id: number, data: Partial): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.update(exportAutomationRules).set(data).where(eq(exportAutomationRules.id, id)); + const rule = await getExportAutomationRuleById(id); + if (!rule) throw new Error("Export automation rule not found"); + return rule; +} + +export async function deleteExportAutomationRule(id: number): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + await db.delete(exportAutomationRules).where(eq(exportAutomationRules.id, id)); +} + // ============= INITIALIZE DEFAULT VALUES ============= export async function initializeDefaultLists(userId: number): Promise { diff --git a/server/exportAutomation.test.ts b/server/exportAutomation.test.ts new file mode 100644 index 0000000..44395e7 --- /dev/null +++ b/server/exportAutomation.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { findMatchingExportRule } from "../shared/exportAutomation"; + +const rules = [ + { isActive: 1, priority: 2, conditionField: "recipientName" as const, conditionValue: "Direction générale" }, + { isActive: 1, priority: 1, conditionField: "ventilationComptable" as const, conditionValue: "615200" }, + { isActive: 0, priority: 0, conditionField: "recipientName" as const, conditionValue: "Direction générale" }, +]; + +describe("règles de destination d’export", () => { + it("sélectionne une règle de ventilation active", () => { + expect(findMatchingExportRule(rules, { recipientName: "Direction générale", ventilationComptable: "615200" })).toBe(rules[1]); + }); + + it("compare les destinataires sans tenir compte de la casse et des espaces", () => { + expect(findMatchingExportRule(rules, { recipientName: " direction GÉNÉRALE " })).toBe(rules[0]); + }); + + it("ignore les règles inactives", () => { + expect(findMatchingExportRule([rules[2]], { recipientName: "Direction générale" })).toBeUndefined(); + }); + + it("ne retourne aucune règle si la facture ne correspond à aucun critère", () => { + expect(findMatchingExportRule(rules, { recipientName: "Service achats", ventilationComptable: "606000" })).toBeUndefined(); + }); +}); diff --git a/server/exportDestinationResolver.ts b/server/exportDestinationResolver.ts new file mode 100644 index 0000000..28b88a4 --- /dev/null +++ b/server/exportDestinationResolver.ts @@ -0,0 +1,42 @@ +import type { ImportSettings, Invoice } from "../drizzle/schema"; +import { findMatchingExportRule } from "@shared/exportAutomation"; +import { getExportAutomationRulesByUser } from "./db"; + +export type ResolvedExportDestination = { + exportMode: "browser" | "folder" | "both"; + destinationPath: string | null; + destinationType: "local" | "teams" | "sharepoint"; + source: "rule" | "default"; + ruleName?: string; +}; + +/** + * Résout une destination pour une validation BAP. Une règle active ciblant le + * destinataire ou la ventilation est prioritaire sur les anciens paramètres + * globaux, qui restent le comportement de repli pour préserver l’existant. + */ +export async function resolveBapExportDestination( + userId: number, + invoice: Pick, + settings: ImportSettings | null, +): Promise { + const rules = await getExportAutomationRulesByUser(userId); + const matchingRule = findMatchingExportRule(rules, invoice); + + if (matchingRule) { + return { + exportMode: matchingRule.openInBrowser === 1 ? "both" : "folder", + destinationPath: matchingRule.destinationPath, + destinationType: matchingRule.destinationType, + source: "rule", + ruleName: matchingRule.name, + }; + } + + return { + exportMode: settings?.bapExportMode || "browser", + destinationPath: settings?.exportFolder || null, + destinationType: settings?.exportFolderType || "local", + source: "default", + }; +} diff --git a/server/routers.ts b/server/routers.ts index 961d201..fba4422 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -66,6 +66,11 @@ import { createAutomationRule, updateAutomationRule, deleteAutomationRule, + getExportAutomationRulesByUser, + getExportAutomationRuleById, + createExportAutomationRule, + updateExportAutomationRule, + deleteExportAutomationRule, getSignaturesByUser, getSignatureById, createSignature, @@ -97,6 +102,7 @@ import { calculateFileSha256 } from "./fileFingerprint"; import { localStoragePut, generateStorageKey } from "./localStorage"; import { testSftpConnection, exportInvoiceToSftp, getUserSftpConfig } from "./sftpExport"; import { drawBapCartouche } from "./bapCartouche"; +import { resolveBapExportDestination } from "./exportDestinationResolver"; import { startEmailImportService, stopEmailImportService, isEmailImportServiceRunning, triggerEmailCheck, triggerManualEmailCheck, testImapConnection } from "./emailImportService"; import { startFolderImportService, stopFolderImportService, isFolderImportServiceRunning } from "./folderImportService"; import { TRPCError } from "@trpc/server"; @@ -521,9 +527,10 @@ export const appRouter = router({ const { localStoragePut, generateStorageKey } = await import('./localStorage'); const importSettings = await getImportSettingsByUser(ctx.user.id); - const bapExportMode = importSettings?.bapExportMode || 'browser'; - const exportFolder = importSettings?.exportFolder || null; - const exportFolderType = (importSettings as any)?.exportFolderType || 'local'; + const resolvedDestination = await resolveBapExportDestination(ctx.user.id, invoice, importSettings); + const bapExportMode = resolvedDestination.exportMode; + const exportFolder = resolvedDestination.destinationPath; + const exportFolderType = resolvedDestination.destinationType; const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), 'storage'); let pdfUrl: string | null = null; @@ -609,11 +616,11 @@ export const appRouter = router({ const _bapNumber = (invoice.invoiceNumber || '').replace(/[^a-zA-Z0-9\-]/g, '').trim(); const bapFilename = [_bapDateStr, _bapSupplier, _bapNumber].filter(Boolean).join(' - ') + '.pdf'; - console.log(`[BAP] Mode export: ${bapExportMode}, type: ${exportFolderType}, dossier: ${exportFolder ? 'configuré' : 'non configuré'}`); + console.log(`[BAP] Mode export: ${bapExportMode}, type: ${exportFolderType}, source: ${resolvedDestination.source}, dossier: ${exportFolder ? 'configuré' : 'non configuré'}`); if ((bapExportMode === 'folder' || bapExportMode === 'both') && exportFolder) { - if (exportFolderType === 'sharepoint') { - // Mode SharePoint : upload via Microsoft Graph - console.log('[BAP] Démarrage upload SharePoint pour:', bapFilename); + if (exportFolderType === 'sharepoint' || exportFolderType === 'teams') { + // Les fichiers Teams sont déposés via le site SharePoint de l’équipe. + console.log('[BAP] Démarrage upload Microsoft 365 pour:', bapFilename); const { uploadToSharePoint } = await import('./sharepoint'); const spResult = await uploadToSharePoint( { @@ -740,9 +747,6 @@ export const appRouter = router({ const { PDFDocument, rgb, StandardFonts } = await import('pdf-lib'); const { localStoragePut, generateStorageKey } = await import('./localStorage'); const importSettings = await getImportSettingsByUser(ctx.user.id); - const bapExportMode = importSettings?.bapExportMode || 'browser'; - const exportFolder = importSettings?.exportFolder || null; - const exportFolderType = (importSettings as any)?.exportFolderType || 'local'; const STORAGE_BASE_PATH = process.env.STORAGE_BASE_PATH || path.join(process.cwd(), 'storage'); const serviceSignaturesList = await getServiceSignaturesByUser(ctx.user.id); const results: Array<{ id: number; success: boolean; pdfUrl?: string | null; exportPath?: string | null; error?: string }> = []; @@ -756,6 +760,10 @@ export const appRouter = router({ let sharepointUploadStatus: 'success' | 'error' | 'skipped' | null = null; let sharepointUploadPath: string | null = null; let sharepointUploadError: string | null = null; + const resolvedDestination = await resolveBapExportDestination(ctx.user.id, invoice, importSettings); + const bapExportMode = resolvedDestination.exportMode; + const exportFolder = resolvedDestination.destinationPath; + const exportFolderType = resolvedDestination.destinationType; try { // ─ Lecture du PDF source ─ let sourcePdfBytes: Buffer; @@ -826,7 +834,7 @@ export const appRouter = router({ const _bapNumber2 = (invoice.invoiceNumber || '').replace(/[^a-zA-Z0-9\-]/g, '').trim(); const bapFilename = [_bapDateStr2, _bapSupplier2, _bapNumber2].filter(Boolean).join(' - ') + '.pdf'; if ((bapExportMode === 'folder' || bapExportMode === 'both') && exportFolder) { - if (exportFolderType === 'sharepoint') { + if (exportFolderType === 'sharepoint' || exportFolderType === 'teams') { const { uploadToSharePoint } = await import('./sharepoint'); const spResult = await uploadToSharePoint( { @@ -2276,6 +2284,55 @@ export const appRouter = router({ }), }), + // ============= EXPORT AUTOMATION RULES ============= + exportAutomationRules: router({ + list: protectedProcedure.query(async ({ ctx }) => { + return getExportAutomationRulesByUser(ctx.user.id); + }), + create: protectedProcedure + .input(z.object({ + name: z.string().trim().min(1).max(255), + conditionField: z.enum(["recipientName", "ventilationComptable"]), + conditionValue: z.string().trim().min(1).max(255), + destinationType: z.enum(["local", "teams", "sharepoint"]), + destinationPath: z.string().trim().min(1), + openInBrowser: z.number().int().min(0).max(1).default(0), + isActive: z.number().int().min(0).max(1).default(1), + priority: z.number().int().min(0).default(0), + })) + .mutation(async ({ input, ctx }) => createExportAutomationRule({ userId: ctx.user.id, ...input })), + update: protectedProcedure + .input(z.object({ + id: z.number().int().positive(), + name: z.string().trim().min(1).max(255).optional(), + conditionField: z.enum(["recipientName", "ventilationComptable"]).optional(), + conditionValue: z.string().trim().min(1).max(255).optional(), + destinationType: z.enum(["local", "teams", "sharepoint"]).optional(), + destinationPath: z.string().trim().min(1).optional(), + openInBrowser: z.number().int().min(0).max(1).optional(), + isActive: z.number().int().min(0).max(1).optional(), + priority: z.number().int().min(0).optional(), + })) + .mutation(async ({ input, ctx }) => { + const existing = await getExportAutomationRuleById(input.id); + if (!existing || (ctx.user.role !== "admin" && existing.userId !== ctx.user.id)) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + const { id, ...data } = input; + return updateExportAutomationRule(id, data); + }), + delete: protectedProcedure + .input(z.object({ id: z.number().int().positive() })) + .mutation(async ({ input, ctx }) => { + const existing = await getExportAutomationRuleById(input.id); + if (!existing || (ctx.user.role !== "admin" && existing.userId !== ctx.user.id)) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + await deleteExportAutomationRule(input.id); + return { success: true }; + }), + }), + // ============= SIGNATURES ROUTES ============= signatures: router({ list: protectedProcedure.query(async ({ ctx }) => { diff --git a/shared/exportAutomation.ts b/shared/exportAutomation.ts new file mode 100644 index 0000000..a30db31 --- /dev/null +++ b/shared/exportAutomation.ts @@ -0,0 +1,32 @@ +/** Valeurs nécessaires pour déterminer une destination d’export. */ +export type ExportableInvoice = { + recipientName?: string | null; + ventilationComptable?: string | null; +}; + +export type ExportRuleCandidate = { + isActive: number; + priority: number; + conditionField: "recipientName" | "ventilationComptable"; + conditionValue: string; +}; + +/** + * Sélectionne la première règle active par priorité. Les comparaisons sont + * volontairement insensibles à la casse et aux espaces superflus. + */ +export function findMatchingExportRule( + rules: T[], + invoice: ExportableInvoice, +): T | undefined { + const normalized = (value: string | null | undefined) => value?.trim().toLocaleLowerCase("fr-FR") || ""; + const ordered = [...rules].sort((a, b) => a.priority - b.priority || a.conditionValue.localeCompare(b.conditionValue, "fr-FR")); + + return ordered.find((rule) => { + if (rule.isActive !== 1) return false; + const invoiceValue = rule.conditionField === "recipientName" + ? invoice.recipientName + : invoice.ventilationComptable; + return normalized(invoiceValue) === normalized(rule.conditionValue); + }); +} diff --git a/todo.md b/todo.md index 27c191d..d069a22 100644 --- a/todo.md +++ b/todo.md @@ -858,3 +858,11 @@ - [x] Ajouter les filtres Destinataires, Entités et Ventilations après le filtre Mois - [x] Appliquer ces filtres à la liste BAP en préservant les filtres et le tri existants - [x] Ajouter les tests et valider l’affichage de Factures BAP en sandbox + +## Automatismes d’export et simplification des Paramètres +- [x] Ajouter les onglets Automatismes d’import et Automatismes d’export +- [x] Créer des règles de destination d’export par destinataire ou ventilation comptable +- [x] Prendre en charge les destinations dossier local, Teams et SharePoint sans perdre les réglages existants +- [x] Appliquer les règles de destination lors des exports de factures BAP +- [x] Déplacer les paramètres d’export dans Automatismes et renommer la page en Paramètres +- [x] Ajouter les tests et valider les nouveaux parcours en sandbox diff --git a/verification_notes.md b/verification_notes.md index 405ded2..4e68279 100644 --- a/verification_notes.md +++ b/verification_notes.md @@ -4,3 +4,9 @@ - Le cartouche administrateur de lecture e-mail ponctuelle est visible, explicite sur l’absence de planification créée et ne propose aucune activation automatique. - Les filtres et indicateurs restent lisibles sur mobile. - Le tableau est enveloppé dans un conteneur à défilement horizontal afin de préserver toutes les colonnes de traçabilité sur les écrans étroits. + +## Vérification visuelle — Automatismes et Paramètres + +- Le 2 septembre 2026, les routes `/automation-rules` et `/import-settings` ont été vérifiées en format bureau. +- Les onglets « Automatismes d’import » et « Automatismes d’export » sont visibles dans la page Automatismes. +- La navigation et le titre affichent désormais « Paramètres » ; seuls les onglets Paramètres d’import et Sauvegarde DB restent accessibles depuis cette page.