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>
);
}