diff --git a/client/src/App.tsx b/client/src/App.tsx index 96a19d5..b5cad5b 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -11,6 +11,7 @@ import Upload from "./pages/Upload"; import Invoices from "./pages/Invoices"; import InvoiceDetail from "./pages/InvoiceDetail"; import Settings from "./pages/Settings"; +import ImportSettings from "./pages/ImportSettings"; import History from "./pages/History"; import Users from "./pages/Users"; @@ -24,6 +25,7 @@ function Router() { + diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index b601e07..7011e39 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -21,7 +21,7 @@ import { } from "@/components/ui/sidebar"; import { getLoginUrl } from "@/const"; import { useIsMobile } from "@/hooks/useMobile"; -import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings } from "lucide-react"; +import { LayoutDashboard, LogOut, PanelLeft, Users, FileText, Upload, History, Settings, Download } from "lucide-react"; import { CSSProperties, useEffect, useRef, useState } from "react"; import { useLocation } from "wouter"; import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton'; @@ -33,6 +33,7 @@ const menuItems = [ { icon: FileText, label: "Factures", path: "/invoices" }, { icon: History, label: "Historique", path: "/history" }, { icon: Settings, label: "Param\u00e8tres", path: "/settings" }, + { icon: Download, label: "Param\u00e8tres de r\u00e9ception", path: "/import-settings" }, { icon: Users, label: "Utilisateurs", path: "/users", adminOnly: true }, ]; diff --git a/client/src/pages/ImportSettings.tsx b/client/src/pages/ImportSettings.tsx new file mode 100644 index 0000000..21a05ac --- /dev/null +++ b/client/src/pages/ImportSettings.tsx @@ -0,0 +1,309 @@ +import { useState, useEffect } from "react"; +import { trpc } from "@/lib/trpc"; +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 { Switch } from "@/components/ui/switch"; +import { toast } from "sonner"; +import { Loader2, Save, Upload, FolderOpen, Mail } from "lucide-react"; + +export default function ImportSettings() { + const { data: settings, isLoading } = trpc.importSettings.get.useQuery(); + const updateMutation = trpc.importSettings.update.useMutation(); + + // Manual import + const [manualImportEnabled, setManualImportEnabled] = useState(true); + + // Automatic import from folder + const [autoImportEnabled, setAutoImportEnabled] = useState(false); + const [autoImportSourcePath, setAutoImportSourcePath] = useState(""); + const [autoImportFrequency, setAutoImportFrequency] = useState(60); + + // Email import + const [emailImportEnabled, setEmailImportEnabled] = useState(false); + const [emailImportAddress, setEmailImportAddress] = useState(""); + const [emailImportPassword, setEmailImportPassword] = useState(""); + const [emailImportHost, setEmailImportHost] = useState(""); + const [emailImportPort, setEmailImportPort] = useState(993); + const [emailImportFrequency, setEmailImportFrequency] = useState(30); + + // Initialize form with settings from database + useEffect(() => { + if (settings) { + setManualImportEnabled(settings.manualImportEnabled === 1); + setAutoImportEnabled(settings.autoImportEnabled === 1); + setAutoImportSourcePath(settings.autoImportSourcePath || ""); + setAutoImportFrequency(settings.autoImportFrequency || 60); + setEmailImportEnabled(settings.emailImportEnabled === 1); + setEmailImportAddress(settings.emailImportAddress || ""); + setEmailImportPassword(settings.emailImportPassword || ""); + setEmailImportHost(settings.emailImportHost || ""); + setEmailImportPort(settings.emailImportPort || 993); + setEmailImportFrequency(settings.emailImportFrequency || 30); + } + }, [settings]); + + const handleSave = async () => { + try { + await updateMutation.mutateAsync({ + manualImportEnabled: manualImportEnabled ? 1 : 0, + autoImportEnabled: autoImportEnabled ? 1 : 0, + autoImportSourcePath: autoImportSourcePath || null, + autoImportFrequency: autoImportFrequency, + emailImportEnabled: emailImportEnabled ? 1 : 0, + emailImportAddress: emailImportAddress || null, + emailImportPassword: emailImportPassword || null, + emailImportHost: emailImportHost || null, + emailImportPort: emailImportPort, + emailImportFrequency: emailImportFrequency, + }); + + toast.success("Paramètres enregistrés", { + description: "Vos paramètres de réception ont été mis à jour avec succès.", + }); + } catch (error) { + toast.error("Erreur", { + description: "Impossible d'enregistrer les paramètres. Veuillez réessayer.", + }); + } + }; + + if (isLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+

Paramètres de réception

+

+ Configurez les différentes méthodes d'importation de factures +

+
+ +
+ {/* Manual Import */} + + +
+
+ +
+
+ Import manuel + + Autoriser l'upload manuel de fichiers PDF via l'interface + +
+
+
+ +
+ + +
+
+
+ + {/* Automatic Import from Folder */} + + +
+
+ +
+
+ Import automatique depuis dossier + + Surveiller un dossier et importer automatiquement les nouveaux fichiers PDF + +
+
+
+ +
+ + +
+ + {autoImportEnabled && ( + <> +
+ + setAutoImportSourcePath(e.target.value)} + /> +

+ Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF +

+
+ +
+ + setAutoImportFrequency(parseInt(e.target.value) || 60)} + /> +

+ Intervalle de temps entre chaque vérification du dossier (minimum: 1 minute) +

+
+ + )} +
+
+ + {/* Email Import */} + + +
+
+ +
+
+ Import par email + + Récupérer automatiquement les factures reçues par email + +
+
+
+ +
+ + +
+ + {emailImportEnabled && ( + <> +
+ + setEmailImportAddress(e.target.value)} + /> +

+ Adresse email à surveiller pour les factures +

+
+ +
+ + setEmailImportPassword(e.target.value)} + /> +

+ Mot de passe du compte email (stocké de manière sécurisée) +

+
+ +
+
+ + setEmailImportHost(e.target.value)} + /> +

+ Adresse du serveur IMAP +

+
+ +
+ + setEmailImportPort(parseInt(e.target.value) || 993)} + /> +

+ Port SSL (993 par défaut) +

+
+
+ +
+ + setEmailImportFrequency(parseInt(e.target.value) || 30)} + /> +

+ Intervalle de temps entre chaque vérification des emails (minimum: 1 minute) +

+
+ + )} +
+
+ + {/* Save Button */} +
+ +
+
+
+ ); +} diff --git a/drizzle/0003_previous_killraven.sql b/drizzle/0003_previous_killraven.sql new file mode 100644 index 0000000..207520e --- /dev/null +++ b/drizzle/0003_previous_killraven.sql @@ -0,0 +1,18 @@ +CREATE TABLE `importSettings` ( + `id` int AUTO_INCREMENT NOT NULL, + `userId` int NOT NULL, + `manualImportEnabled` int NOT NULL DEFAULT 1, + `autoImportEnabled` int NOT NULL DEFAULT 0, + `autoImportSourcePath` text, + `autoImportFrequency` int NOT NULL DEFAULT 60, + `emailImportEnabled` int NOT NULL DEFAULT 0, + `emailImportAddress` varchar(320), + `emailImportPassword` text, + `emailImportHost` varchar(255), + `emailImportPort` int DEFAULT 993, + `emailImportFrequency` int NOT NULL DEFAULT 30, + `createdAt` timestamp NOT NULL DEFAULT (now()), + `updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT `importSettings_id` PRIMARY KEY(`id`), + CONSTRAINT `importSettings_userId_unique` UNIQUE(`userId`) +); diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..4beff40 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,950 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "1e89df9d-47b6-47da-8afc-6024d8bdc8ba", + "prevId": "5a495536-d69b-4bd9-9618-0644c5a001bd", + "tables": { + "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 + }, + "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 + }, + "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": {} + }, + "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": {} + }, + "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 + }, + "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 938f13a..3a5ce16 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1767881720296, "tag": "0002_striped_red_skull", "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1770742897031, + "tag": "0003_previous_killraven", + "breakpoints": true } ] } \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 721e5ac..0cbd47f 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -130,6 +130,36 @@ export const userSettings = mysqlTable("userSettings", { export type UserSettings = typeof userSettings.$inferSelect; export type InsertUserSettings = typeof userSettings.$inferInsert; +/** + * Import settings table for configuring different import methods + */ +export const importSettings = mysqlTable("importSettings", { + id: int("id").autoincrement().primaryKey(), + userId: int("userId").notNull().unique(), // One settings record per user + + // Manual import settings + manualImportEnabled: int("manualImportEnabled").default(1).notNull(), // 0 = disabled, 1 = enabled + + // Automatic import from folder settings + autoImportEnabled: int("autoImportEnabled").default(0).notNull(), // 0 = disabled, 1 = enabled + autoImportSourcePath: text("autoImportSourcePath"), // Path to folder to watch + autoImportFrequency: int("autoImportFrequency").default(60).notNull(), // Frequency in minutes (default: 60 = 1 hour) + + // Email import settings + emailImportEnabled: int("emailImportEnabled").default(0).notNull(), // 0 = disabled, 1 = enabled + emailImportAddress: varchar("emailImportAddress", { length: 320 }), // Email address to monitor + emailImportPassword: text("emailImportPassword"), // Encrypted password + emailImportHost: varchar("emailImportHost", { length: 255 }), // IMAP server host + emailImportPort: int("emailImportPort").default(993), // IMAP port (default: 993 for SSL) + emailImportFrequency: int("emailImportFrequency").default(30).notNull(), // Frequency in minutes (default: 30) + + createdAt: timestamp("createdAt").defaultNow().notNull(), + updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(), +}); + +export type ImportSettings = typeof importSettings.$inferSelect; +export type InsertImportSettings = typeof importSettings.$inferInsert; + /** * Import logs table for tracking all import operations */ diff --git a/server/db.ts b/server/db.ts index 1ab2bfb..dca3085 100644 --- a/server/db.ts +++ b/server/db.ts @@ -17,7 +17,10 @@ import { ImportLog, llmLogs, InsertLlmLog, - LlmLog + LlmLog, + importSettings, + InsertImportSettings, + ImportSettings } from "../drizzle/schema"; import { ENV } from './_core/env'; @@ -434,3 +437,40 @@ export async function getLlmLogsByInvoice(invoiceId: number): Promise if (!db) return []; return db.select().from(llmLogs).where(eq(llmLogs.invoiceId, invoiceId)).orderBy(desc(llmLogs.createdAt)); } + +// ============= IMPORT SETTINGS OPERATIONS ============= + +export async function getImportSettingsByUser(userId: number): Promise { + const db = await getDb(); + if (!db) return null; + const result = await db.select().from(importSettings).where(eq(importSettings.userId, userId)).limit(1); + return result[0] || null; +} + +export async function upsertImportSettings(data: InsertImportSettings): Promise { + const db = await getDb(); + if (!db) throw new Error("Database not available"); + + // Check if settings exist for this user + const existing = await getImportSettingsByUser(data.userId!); + + if (existing) { + // Update existing settings + await db.update(importSettings) + .set({ + ...data, + updatedAt: new Date(), + }) + .where(eq(importSettings.userId, data.userId!)); + + const updated = await getImportSettingsByUser(data.userId!); + return updated!; + } else { + // Insert new settings + const result = await db.insert(importSettings).values(data); + const insertedId = Number(result[0].insertId); + + const inserted = await db.select().from(importSettings).where(eq(importSettings.id, insertedId)).limit(1); + return inserted[0]!; + } +} diff --git a/server/routers.ts b/server/routers.ts index 7c69712..ca206b9 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -26,6 +26,8 @@ import { getImportLogsByUser, getLlmLogsBySourceFile, getLlmLogsByInvoice, + getImportSettingsByUser, + upsertImportSettings, } from "./db"; import { loginLocal, hashPassword, getAzureAuthUrl, isAzureAdConfigured, generateToken } from "./auth"; import { extractInvoicesWithMistral, generateMetadataJSON } from "./invoiceExtractor"; @@ -572,6 +574,54 @@ export const appRouter = router({ return getLlmLogsByInvoice(input.invoiceId); }), }), + + // ============= IMPORT SETTINGS ROUTES ============= + importSettings: router({ + get: protectedProcedure.query(async ({ ctx }) => { + const settings = await getImportSettingsByUser(ctx.user.id); + + // Return default settings if none exist + if (!settings) { + return { + userId: ctx.user.id, + manualImportEnabled: 1, + autoImportEnabled: 0, + autoImportSourcePath: null, + autoImportFrequency: 60, + emailImportEnabled: 0, + emailImportAddress: null, + emailImportPassword: null, + emailImportHost: null, + emailImportPort: 993, + emailImportFrequency: 30, + }; + } + + return settings; + }), + + update: protectedProcedure + .input(z.object({ + manualImportEnabled: z.number().min(0).max(1), + autoImportEnabled: z.number().min(0).max(1), + autoImportSourcePath: z.string().nullable().optional(), + autoImportFrequency: z.number().min(1).optional(), + emailImportEnabled: z.number().min(0).max(1), + emailImportAddress: z.string().email().nullable().optional(), + emailImportPassword: z.string().nullable().optional(), + emailImportHost: z.string().nullable().optional(), + emailImportPort: z.number().min(1).max(65535).optional(), + emailImportFrequency: z.number().min(1).optional(), + })) + .mutation(async ({ input, ctx }) => { + const settings = await upsertImportSettings({ + userId: ctx.user.id, + ...input, + }); + + return settings; + }), + }), }); export type AppRouter = typeof appRouter; diff --git a/todo.md b/todo.md index 599d9ca..9f97375 100644 --- a/todo.md +++ b/todo.md @@ -136,3 +136,12 @@ - [x] Identifier le bug: l'URL de localStoragePut était écrasée par une URL locale - [x] Corriger la route upload pour utiliser l'URL retournée par localStoragePut - [ ] Tester le stockage avec un upload réel + +## Paramètres de réception +- [x] Créer table importSettings dans le schéma de base de données +- [x] Ajouter routes tRPC pour gérer les paramètres de réception (get, update) +- [x] Créer page Paramètres de réception avec configuration import manuel/automatique/email +- [x] Implémenter activation/désactivation import manuel +- [x] Implémenter activation/désactivation import automatique avec dossier source et fréquence +- [x] Implémenter activation/désactivation import par email avec compte mail et mot de passe +- [x] Tester la sauvegarde et récupération des paramètres