Checkpoint: Checkpoint saved: Ajout d'une page "Paramètres de réception" complète pour configurer les différentes méthodes d'import de factures.

Nouvelles fonctionnalités :
✅ Table importSettings dans la base de données (7 tables au total)
✅ Routes tRPC pour gérer les paramètres de réception (get, update)
✅ Page "Paramètres de réception" accessible via le menu de navigation

Configuration disponible :
1. **Import manuel** : Activation/désactivation de l'upload manuel de fichiers PDF via l'interface

2. **Import automatique depuis dossier** :
   - Activation/désactivation
   - Configuration du chemin du dossier source à surveiller
   - Fréquence de lecture du dossier (en minutes)

3. **Import par email** :
   - Activation/désactivation
   - Configuration du compte email (adresse + mot de passe)
   - Serveur IMAP (host + port)
   - Fréquence de lecture des emails (en minutes)

Interface utilisateur :
- 3 cartes distinctes avec icônes (Upload, FolderOpen, Mail)
- Switches pour activer/désactiver chaque méthode
- Champs de configuration qui apparaissent uniquement si la méthode est activée
- Bouton "Enregistrer les paramètres" avec feedback visuel
- Notifications toast pour confirmer la sauvegarde ou signaler les erreurs

Architecture :
- Schéma de base de données : table importSettings avec 14 colonnes
- Backend : Fonctions db.ts (getImportSettingsByUser, upsertImportSettings)
- Routes tRPC : importSettings.get et importSettings.update
- Frontend : Page ImportSettings.tsx avec gestion d'état React
- Navigation : Nouveau lien "Paramètres de réception" dans le menu latéral

La page est maintenant accessible via /import-settings et permet aux utilisateurs de configurer toutes les méthodes d'importation de factures de manière centralisée.
This commit is contained in:
Manus
2026-02-10 12:05:23 -05:00
parent 4690b16c9f
commit 35efbf632d
10 changed files with 1418 additions and 2 deletions

View File

@@ -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() {
<Route path="/invoices" component={Invoices} />
<Route path="/invoices/:id" component={InvoiceDetail} />
<Route path="/settings" component={Settings} />
<Route path="/import-settings" component={ImportSettings} />
<Route path="/history" component={History} />
<Route path="/users" component={Users} />
<Route path="/404" component={NotFound} />

View File

@@ -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 },
];

View File

@@ -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 (
<div className="flex items-center justify-center h-[calc(100vh-200px)]">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
);
}
return (
<div className="container max-w-4xl py-8">
<div className="mb-8">
<h1 className="text-3xl font-bold mb-2">Paramètres de réception</h1>
<p className="text-muted-foreground">
Configurez les différentes méthodes d'importation de factures
</p>
</div>
<div className="space-y-6">
{/* Manual Import */}
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg">
<Upload className="h-5 w-5 text-primary" />
</div>
<div>
<CardTitle>Import manuel</CardTitle>
<CardDescription>
Autoriser l'upload manuel de fichiers PDF via l'interface
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<Label htmlFor="manual-import" className="text-base">
Activer l'import manuel
</Label>
<Switch
id="manual-import"
checked={manualImportEnabled}
onCheckedChange={setManualImportEnabled}
/>
</div>
</CardContent>
</Card>
{/* Automatic Import from Folder */}
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded-lg">
<FolderOpen className="h-5 w-5 text-blue-500" />
</div>
<div>
<CardTitle>Import automatique depuis dossier</CardTitle>
<CardDescription>
Surveiller un dossier et importer automatiquement les nouveaux fichiers PDF
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="auto-import" className="text-base">
Activer l'import automatique
</Label>
<Switch
id="auto-import"
checked={autoImportEnabled}
onCheckedChange={setAutoImportEnabled}
/>
</div>
{autoImportEnabled && (
<>
<div className="space-y-2">
<Label htmlFor="source-path">Chemin du dossier source</Label>
<Input
id="source-path"
type="text"
placeholder="/chemin/vers/dossier/factures"
value={autoImportSourcePath}
onChange={(e) => setAutoImportSourcePath(e.target.value)}
/>
<p className="text-sm text-muted-foreground">
Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF
</p>
</div>
<div className="space-y-2">
<Label htmlFor="auto-frequency">Fréquence de lecture (minutes)</Label>
<Input
id="auto-frequency"
type="number"
min="1"
value={autoImportFrequency}
onChange={(e) => setAutoImportFrequency(parseInt(e.target.value) || 60)}
/>
<p className="text-sm text-muted-foreground">
Intervalle de temps entre chaque vérification du dossier (minimum: 1 minute)
</p>
</div>
</>
)}
</CardContent>
</Card>
{/* Email Import */}
<Card>
<CardHeader>
<div className="flex items-center gap-3">
<div className="p-2 bg-green-500/10 rounded-lg">
<Mail className="h-5 w-5 text-green-500" />
</div>
<div>
<CardTitle>Import par email</CardTitle>
<CardDescription>
Récupérer automatiquement les factures reçues par email
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="email-import" className="text-base">
Activer l'import par email
</Label>
<Switch
id="email-import"
checked={emailImportEnabled}
onCheckedChange={setEmailImportEnabled}
/>
</div>
{emailImportEnabled && (
<>
<div className="space-y-2">
<Label htmlFor="email-address">Adresse email</Label>
<Input
id="email-address"
type="email"
placeholder="factures@votreentreprise.com"
value={emailImportAddress}
onChange={(e) => setEmailImportAddress(e.target.value)}
/>
<p className="text-sm text-muted-foreground">
Adresse email à surveiller pour les factures
</p>
</div>
<div className="space-y-2">
<Label htmlFor="email-password">Mot de passe</Label>
<Input
id="email-password"
type="password"
placeholder="••••••••"
value={emailImportPassword}
onChange={(e) => setEmailImportPassword(e.target.value)}
/>
<p className="text-sm text-muted-foreground">
Mot de passe du compte email (stocké de manière sécurisée)
</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email-host">Serveur IMAP</Label>
<Input
id="email-host"
type="text"
placeholder="imap.gmail.com"
value={emailImportHost}
onChange={(e) => setEmailImportHost(e.target.value)}
/>
<p className="text-sm text-muted-foreground">
Adresse du serveur IMAP
</p>
</div>
<div className="space-y-2">
<Label htmlFor="email-port">Port IMAP</Label>
<Input
id="email-port"
type="number"
min="1"
max="65535"
value={emailImportPort}
onChange={(e) => setEmailImportPort(parseInt(e.target.value) || 993)}
/>
<p className="text-sm text-muted-foreground">
Port SSL (993 par défaut)
</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email-frequency">Fréquence de lecture (minutes)</Label>
<Input
id="email-frequency"
type="number"
min="1"
value={emailImportFrequency}
onChange={(e) => setEmailImportFrequency(parseInt(e.target.value) || 30)}
/>
<p className="text-sm text-muted-foreground">
Intervalle de temps entre chaque vérification des emails (minimum: 1 minute)
</p>
</div>
</>
)}
</CardContent>
</Card>
{/* Save Button */}
<div className="flex justify-end">
<Button
onClick={handleSave}
disabled={updateMutation.isPending}
size="lg"
>
{updateMutation.isPending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Enregistrement...
</>
) : (
<>
<Save className="mr-2 h-4 w-4" />
Enregistrer les paramètres
</>
)}
</Button>
</div>
</div>
</div>
);
}

View File

@@ -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`)
);

View File

@@ -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": {}
}
}

View File

@@ -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
}
]
}

View File

@@ -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
*/

View File

@@ -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<LlmLog[]>
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<ImportSettings | null> {
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<ImportSettings> {
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]!;
}
}

View File

@@ -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;

View File

@@ -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