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