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