Checkpoint: Modernisation de l'interface de la page Paramètres

Refonte complète de l'UI/UX de la page Settings pour une expérience plus moderne et ergonomique.

Améliorations apportées:
- Système de Tabs pour organiser les sections (Intelligence AI, Mots-clés, Export SFTP)
- Icônes colorées plus grandes pour chaque section
- Cards modernes avec dégradés de couleurs et bordures
- Badges de statut visuels (Obligatoire/Optionnel, Configuré)
- Animations de transition fluides entre les onglets
- Typographie améliorée avec hiérarchie claire
- Espacements optimisés pour une meilleure lisibilité
- Design responsive amélioré pour mobile
- Boutons et inputs plus grands pour une meilleure ergonomie

L'interface est maintenant plus intuitive, visuellement attractive et facile à utiliser.
This commit is contained in:
Manus
2026-02-13 03:58:53 -05:00
parent d32359fc10
commit f7f82fc68a
2 changed files with 468 additions and 261 deletions

View File

@@ -6,8 +6,20 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge";
import { trpc } from "@/lib/trpc";
import { Loader2, Save, CheckCircle } from "lucide-react";
import {
Loader2,
Save,
CheckCircle,
Brain,
Key,
Server,
FileCheck,
AlertCircle,
Sparkles
} from "lucide-react";
import { toast } from "sonner";
import { Checkbox } from "@/components/ui/checkbox";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -35,42 +47,82 @@ function LlmFieldsConfigSection() {
if (isLoading) {
return (
<div className="flex items-center justify-center py-8">
<Loader2 className="w-6 h-6 animate-spin text-gray-400" />
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-primary" />
</div>
);
}
const requiredCount = fields?.filter(f => f.isRequired === 1).length || 0;
const totalCount = fields?.length || 0;
return (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
<div className="space-y-6">
<div className="flex items-center justify-between p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/20 dark:to-indigo-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500 rounded-lg">
<FileCheck className="w-5 h-5 text-white" />
</div>
<div>
<p className="font-medium text-blue-900 dark:text-blue-100">Configuration actuelle</p>
<p className="text-sm text-blue-700 dark:text-blue-300">
{requiredCount} champ{requiredCount > 1 ? 's' : ''} obligatoire{requiredCount > 1 ? 's' : ''} sur {totalCount}
</p>
</div>
</div>
<Badge variant="secondary" className="text-sm">
Score 100%
</Badge>
</div>
<p className="text-sm text-muted-foreground flex items-start gap-2">
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>
Les champs marqués comme obligatoires doivent être détectés pour atteindre un score de 100%.
Les champs optionnels n'affectent pas le score.
</span>
</p>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead>Champ</TableHead>
<TableHead className="text-center">Obligatoire</TableHead>
<TableRow className="bg-muted/50">
<TableHead className="font-semibold">Champ</TableHead>
<TableHead className="text-center font-semibold">Statut</TableHead>
<TableHead className="text-center font-semibold">Obligatoire</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{fields?.map((field) => (
<TableRow key={field.id}>
<TableRow key={field.id} className="hover:bg-muted/30 transition-colors">
<TableCell className="font-medium">{field.displayName}</TableCell>
<TableCell className="text-center">
{field.isRequired === 1 ? (
<Badge variant="default" className="bg-green-500 hover:bg-green-600">
Obligatoire
</Badge>
) : (
<Badge variant="secondary">
Optionnel
</Badge>
)}
</TableCell>
<TableCell className="text-center">
<div className="flex justify-center">
<Checkbox
checked={field.isRequired === 1}
onCheckedChange={() => handleToggle(field.fieldName, field.isRequired)}
disabled={updateFieldMutation.isPending}
className="data-[state=checked]:bg-green-500 data-[state=checked]:border-green-500"
/>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
);
}
@@ -114,7 +166,7 @@ export default function Settings() {
const saveMutation = trpc.settings.upsert.useMutation({
onSuccess: () => {
toast.success("Paramètres enregistrés");
toast.success("Paramètres enregistrés avec succès");
utils.settings.get.invalidate();
},
onError: (error) => {
@@ -125,9 +177,9 @@ export default function Settings() {
const testSftpMutation = trpc.sftp.testConnection.useMutation({
onSuccess: (data) => {
if (data.success) {
toast.success("Connexion SFTP réussie");
toast.success(" Connexion SFTP réussie");
} else {
toast.error("Échec de la connexion SFTP");
toast.error(" Échec de la connexion SFTP");
}
},
onError: (error) => {
@@ -161,41 +213,103 @@ export default function Settings() {
if (isLoading) {
return (
<DashboardLayout>
<div className="flex items-center justify-center h-64">
<Loader2 className="w-8 h-8 animate-spin text-gray-400" />
<div className="flex flex-col items-center justify-center h-64 gap-4">
<Loader2 className="w-12 h-12 animate-spin text-primary" />
<p className="text-muted-foreground">Chargement des paramètres...</p>
</div>
</DashboardLayout>
);
}
const isSftpConfigured = sftpHost && sftpUsername;
return (
<DashboardLayout>
<div className="max-w-4xl space-y-6">
<div className="max-w-5xl space-y-8">
{/* Header */}
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="p-3 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl shadow-lg">
<Sparkles className="w-7 h-7 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold">Paramètres</h1>
<p className="text-gray-500 mt-1">Configurez l'extraction et l'export de vos factures</p>
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent">
Paramètres
</h1>
<p className="text-muted-foreground mt-1">
Configurez l'extraction et l'export de vos factures
</p>
</div>
</div>
</div>
{/* LLM Configuration */}
<Card>
<CardHeader>
<CardTitle>Configuration LLM</CardTitle>
<CardDescription>Paramètres du modèle d'extraction Mistral AI</CardDescription>
{/* Tabs Navigation */}
<Tabs defaultValue="llm" className="space-y-6">
<TabsList className="grid w-full grid-cols-3 h-auto p-1 bg-muted/50">
<TabsTrigger
value="llm"
className="flex items-center gap-2 py-3 data-[state=active]:bg-background data-[state=active]:shadow-sm transition-all"
>
<Brain className="w-5 h-5" />
<span className="font-medium">Intelligence AI</span>
</TabsTrigger>
<TabsTrigger
value="keywords"
className="flex items-center gap-2 py-3 data-[state=active]:bg-background data-[state=active]:shadow-sm transition-all"
>
<Key className="w-5 h-5" />
<span className="font-medium">Mots-clés</span>
</TabsTrigger>
<TabsTrigger
value="sftp"
className="flex items-center gap-2 py-3 data-[state=active]:bg-background data-[state=active]:shadow-sm transition-all"
>
<Server className="w-5 h-5" />
<span className="font-medium">Export SFTP</span>
{isSftpConfigured && (
<Badge variant="secondary" className="ml-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400">
Configuré
</Badge>
)}
</TabsTrigger>
</TabsList>
{/* LLM Tab */}
<TabsContent value="llm" className="space-y-6 animate-in fade-in-50 duration-300">
<Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-950/20 dark:to-pink-950/20 border-b">
<div className="flex items-center gap-3">
<div className="p-2 bg-purple-500 rounded-lg">
<Brain className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Configuration du modèle AI</CardTitle>
<CardDescription className="mt-1">
Paramètres du modèle d'extraction Mistral AI
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="llmModel">Modèle Mistral</Label>
<CardContent className="space-y-6 pt-6">
<div className="space-y-3">
<Label htmlFor="llmModel" className="text-base font-semibold">Modèle Mistral</Label>
<Input
id="llmModel"
value={llmModel}
onChange={(e) => setLlmModel(e.target.value)}
placeholder="mistral-large-latest"
className="h-11"
/>
<p className="text-xs text-gray-500">Nom du modèle Mistral à utiliser pour l'extraction</p>
<p className="text-sm text-muted-foreground flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Nom du modèle Mistral à utiliser pour l'extraction
</p>
</div>
<div className="space-y-2">
<Label htmlFor="llmLogsRetentionMonths">Rétention des logs LLM (mois)</Label>
<div className="space-y-3">
<Label htmlFor="llmLogsRetentionMonths" className="text-base font-semibold">
Rétention des logs (mois)
</Label>
<Input
id="llmLogsRetentionMonths"
type="number"
@@ -203,174 +317,233 @@ export default function Settings() {
onChange={(e) => setLlmLogsRetentionMonths(parseInt(e.target.value) || 3)}
min={1}
max={12}
className="h-11"
/>
<p className="text-xs text-gray-500">Durée de conservation des logs LLM (1-12 mois)</p>
<p className="text-sm text-muted-foreground flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Durée de conservation des logs LLM (1-12 mois)
</p>
</div>
</CardContent>
</Card>
{/* LLM Fields Configuration */}
<Card>
<CardHeader>
<CardTitle>Champs de détection</CardTitle>
<CardDescription>
Configurez quels champs sont obligatoires pour atteindre un score de reconnaissance de 100%
<Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-950/20 dark:to-cyan-950/20 border-b">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500 rounded-lg">
<FileCheck className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Champs de détection</CardTitle>
<CardDescription className="mt-1">
Configurez quels champs sont obligatoires pour un score de 100%
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent>
<CardContent className="pt-6">
<LlmFieldsConfigSection />
</CardContent>
</Card>
</TabsContent>
{/* Keywords Configuration */}
<Card>
<CardHeader>
<CardTitle>Mots-clés personnalisés</CardTitle>
<CardDescription>
Mots-clés pour améliorer la détection des champs (séparés par des virgules)
{/* Keywords Tab */}
<TabsContent value="keywords" className="space-y-6 animate-in fade-in-50 duration-300">
<Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-amber-50 to-orange-50 dark:from-amber-950/20 dark:to-orange-950/20 border-b">
<div className="flex items-center gap-3">
<div className="p-2 bg-amber-500 rounded-lg">
<Key className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Mots-clés personnalisés</CardTitle>
<CardDescription className="mt-1">
Améliorez la détection en ajoutant vos propres mots-clés (séparés par des virgules)
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="invoiceNumberKeywords">Numéro de facture</Label>
<CardContent className="space-y-6 pt-6">
<div className="grid gap-6">
<div className="space-y-3">
<Label htmlFor="invoiceNumberKeywords" className="text-base font-semibold">
Numéro de facture
</Label>
<Textarea
id="invoiceNumberKeywords"
value={invoiceNumberKeywords}
onChange={(e) => setInvoiceNumberKeywords(e.target.value)}
placeholder="Référence, Ref facture, Invoice ref"
rows={2}
className="resize-none"
/>
</div>
<div className="space-y-2">
<Label htmlFor="deliveryNoteKeywords">Bon de livraison</Label>
<div className="space-y-3">
<Label htmlFor="deliveryNoteKeywords" className="text-base font-semibold">
Bon de livraison
</Label>
<Textarea
id="deliveryNoteKeywords"
value={deliveryNoteKeywords}
onChange={(e) => setDeliveryNoteKeywords(e.target.value)}
placeholder="Livraison, Delivery, Expédition"
rows={2}
className="resize-none"
/>
</div>
<div className="space-y-2">
<Label htmlFor="orderNumberKeywords">Numéro de commande</Label>
<div className="space-y-3">
<Label htmlFor="orderNumberKeywords" className="text-base font-semibold">
Numéro de commande
</Label>
<Textarea
id="orderNumberKeywords"
value={orderNumberKeywords}
onChange={(e) => setOrderNumberKeywords(e.target.value)}
placeholder="Cde client, Référence commande, PO Number"
rows={2}
className="resize-none"
/>
</div>
<div className="space-y-2">
<Label htmlFor="supplierKeywords">Fournisseur</Label>
<div className="space-y-3">
<Label htmlFor="supplierKeywords" className="text-base font-semibold">
Fournisseur
</Label>
<Textarea
id="supplierKeywords"
value={supplierKeywords}
onChange={(e) => setSupplierKeywords(e.target.value)}
placeholder="Vendeur, Société, Émetteur"
rows={2}
className="resize-none"
/>
</div>
<div className="space-y-2">
<Label htmlFor="totalAmountKeywords">Montant total</Label>
<div className="space-y-3">
<Label htmlFor="totalAmountKeywords" className="text-base font-semibold">
Montant total
</Label>
<Textarea
id="totalAmountKeywords"
value={totalAmountKeywords}
onChange={(e) => setTotalAmountKeywords(e.target.value)}
placeholder="Net à payer, Total à régler, Amount due"
rows={2}
className="resize-none"
/>
</div>
<div className="space-y-2">
<Label htmlFor="subscriptionKeywords">Abonnement</Label>
<div className="space-y-3">
<Label htmlFor="subscriptionKeywords" className="text-base font-semibold">
Abonnement
</Label>
<Textarea
id="subscriptionKeywords"
value={subscriptionKeywords}
onChange={(e) => setSubscriptionKeywords(e.target.value)}
placeholder="Abonnement, Subscription, Mensuel, Annuel, Recurring"
rows={2}
className="resize-none"
/>
<p className="text-sm text-muted-foreground">
<p className="text-sm text-muted-foreground flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Les factures contenant ces mots-clés seront automatiquement marquées comme abonnement
</p>
</div>
</div>
</CardContent>
</Card>
</TabsContent>
{/* SFTP Configuration */}
<Card>
<CardHeader>
<CardTitle>Configuration SFTP</CardTitle>
<CardDescription>Paramètres d'export vers un serveur SFTP</CardDescription>
{/* SFTP Tab */}
<TabsContent value="sftp" className="space-y-6 animate-in fade-in-50 duration-300">
<Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-b">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-500 rounded-lg">
<Server className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Configuration SFTP</CardTitle>
<CardDescription className="mt-1">
Paramètres d'export automatique vers un serveur SFTP
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="sftpHost">Hôte SFTP</Label>
<CardContent className="space-y-6 pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<Label htmlFor="sftpHost" className="text-base font-semibold">Hôte SFTP</Label>
<Input
id="sftpHost"
value={sftpHost}
onChange={(e) => setSftpHost(e.target.value)}
placeholder="sftp.example.com"
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="sftpPort">Port</Label>
<div className="space-y-3">
<Label htmlFor="sftpPort" className="text-base font-semibold">Port</Label>
<Input
id="sftpPort"
type="number"
value={sftpPort}
onChange={(e) => setSftpPort(parseInt(e.target.value) || 22)}
className="h-11"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="sftpUsername">Nom d'utilisateur</Label>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-3">
<Label htmlFor="sftpUsername" className="text-base font-semibold">Nom d'utilisateur</Label>
<Input
id="sftpUsername"
value={sftpUsername}
onChange={(e) => setSftpUsername(e.target.value)}
placeholder="username"
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="sftpPassword">Mot de passe</Label>
<div className="space-y-3">
<Label htmlFor="sftpPassword" className="text-base font-semibold">Mot de passe</Label>
<Input
id="sftpPassword"
type="password"
value={sftpPassword}
onChange={(e) => setSftpPassword(e.target.value)}
placeholder=""
className="h-11"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="sftpRemotePath">Chemin distant</Label>
<div className="space-y-3">
<Label htmlFor="sftpRemotePath" className="text-base font-semibold">Chemin distant</Label>
<Input
id="sftpRemotePath"
value={sftpRemotePath}
onChange={(e) => setSftpRemotePath(e.target.value)}
placeholder="/invoices"
className="h-11"
/>
<p className="text-xs text-gray-500">
<p className="text-sm text-muted-foreground flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
Les fichiers seront organisés par date: /chemin/YYYY/MM/DD/
</p>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label htmlFor="sftpAutoExport">Export automatique</Label>
<p className="text-xs text-gray-500">
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg border">
<div className="space-y-1">
<Label htmlFor="sftpAutoExport" className="text-base font-semibold cursor-pointer">
Export automatique
</Label>
<p className="text-sm text-muted-foreground">
Exporter automatiquement les factures après extraction
</p>
</div>
@@ -378,33 +551,55 @@ export default function Settings() {
id="sftpAutoExport"
checked={sftpAutoExport}
onCheckedChange={setSftpAutoExport}
className="data-[state=checked]:bg-green-500"
/>
</div>
<div className="pt-4 border-t">
<Button
variant="outline"
onClick={handleTestSftp}
disabled={testSftpMutation.isPending || !sftpHost}
className="w-full h-11 text-base"
size="lg"
>
{testSftpMutation.isPending ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Test en cours...
</>
) : (
<CheckCircle className="w-4 h-4 mr-2" />
<>
<CheckCircle className="w-5 h-5 mr-2" />
Tester la connexion SFTP
</>
)}
Tester la connexion
</Button>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
{/* Save button */}
<div className="flex justify-end">
<Button onClick={handleSave} disabled={saveMutation.isPending} size="lg">
{/* Save Button */}
<div className="flex justify-end pt-4 border-t">
<Button
onClick={handleSave}
disabled={saveMutation.isPending}
size="lg"
className="h-12 px-8 text-base bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 shadow-lg hover:shadow-xl transition-all"
>
{saveMutation.isPending ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Enregistrement...
</>
) : (
<Save className="w-4 h-4 mr-2" />
)}
<>
<Save className="w-5 h-5 mr-2" />
Enregistrer les paramètres
</>
)}
</Button>
</div>
</div>

14
todo.md
View File

@@ -382,5 +382,17 @@
- [x] Modifier le code d'extraction (invoiceExtractor.ts) pour utiliser la configuration
- [x] Adapter le calcul du qualityScore selon les champs obligatoires configurés
- [x] Initialiser les valeurs par défaut (supplierName, invoiceNumber, invoiceDate, totalAmount obligatoires)
- [ ] Tester la configuration et vérifier le calcul du score
- [x] Tester la configuration et vérifier le calcul du score
- [x] Déployer sur le VPS
## Modernisation de la page Paramètres
- [x] Analyser la structure actuelle et identifier les améliorations UX
- [x] Remplacer le layout par un système de Tabs pour organiser les sections
- [x] Utiliser des Cards modernes avec ombres et bordures pour chaque section
- [x] Améliorer la typographie (titres, descriptions, espacements)
- [x] Ajouter des icônes plus grandes et colorées pour chaque section
- [x] Implémenter des animations de transition fluides
- [x] Améliorer le responsive design pour mobile
- [x] Ajouter des badges de statut (Actif/Inactif) avec couleurs
- [x] Tester localement l'interface modernisée
- [ ] Déployer sur le VPS