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:
@@ -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,41 +47,81 @@ 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">
|
||||
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.
|
||||
<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>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Champ</TableHead>
|
||||
<TableHead className="text-center">Obligatoire</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{fields?.map((field) => (
|
||||
<TableRow key={field.id}>
|
||||
<TableCell className="font-medium">{field.displayName}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Checkbox
|
||||
checked={field.isRequired === 1}
|
||||
onCheckedChange={() => handleToggle(field.fieldName, field.isRequired)}
|
||||
disabled={updateFieldMutation.isPending}
|
||||
/>
|
||||
</TableCell>
|
||||
<div className="border rounded-lg overflow-hidden">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{fields?.map((field) => (
|
||||
<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,250 +213,393 @@ 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>
|
||||
<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>
|
||||
<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-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>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="llmModel">Modèle Mistral</Label>
|
||||
<Input
|
||||
id="llmModel"
|
||||
value={llmModel}
|
||||
onChange={(e) => setLlmModel(e.target.value)}
|
||||
placeholder="mistral-large-latest"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">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>
|
||||
<Input
|
||||
id="llmLogsRetentionMonths"
|
||||
type="number"
|
||||
value={llmLogsRetentionMonths}
|
||||
onChange={(e) => setLlmLogsRetentionMonths(parseInt(e.target.value) || 3)}
|
||||
min={1}
|
||||
max={12}
|
||||
/>
|
||||
<p className="text-xs text-gray-500">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%
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<LlmFieldsConfigSection />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 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)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="invoiceNumberKeywords">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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="deliveryNoteKeywords">Bon de livraison</Label>
|
||||
<Textarea
|
||||
id="deliveryNoteKeywords"
|
||||
value={deliveryNoteKeywords}
|
||||
onChange={(e) => setDeliveryNoteKeywords(e.target.value)}
|
||||
placeholder="Livraison, Delivery, Expédition"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="orderNumberKeywords">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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="supplierKeywords">Fournisseur</Label>
|
||||
<Textarea
|
||||
id="supplierKeywords"
|
||||
value={supplierKeywords}
|
||||
onChange={(e) => setSupplierKeywords(e.target.value)}
|
||||
placeholder="Vendeur, Société, Émetteur"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="totalAmountKeywords">Montant total</Label>
|
||||
<Textarea
|
||||
id="totalAmountKeywords"
|
||||
value={totalAmountKeywords}
|
||||
onChange={(e) => setTotalAmountKeywords(e.target.value)}
|
||||
placeholder="Net à payer, Total à régler, Amount due"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subscriptionKeywords">Abonnement</Label>
|
||||
<Textarea
|
||||
id="subscriptionKeywords"
|
||||
value={subscriptionKeywords}
|
||||
onChange={(e) => setSubscriptionKeywords(e.target.value)}
|
||||
placeholder="Abonnement, Subscription, Mensuel, Annuel, Recurring"
|
||||
rows={2}
|
||||
/>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Les factures contenant ces mots-clés seront automatiquement marquées comme abonnement
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SFTP Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Configuration SFTP</CardTitle>
|
||||
<CardDescription>Paramètres d'export vers un serveur SFTP</CardDescription>
|
||||
</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>
|
||||
<Input
|
||||
id="sftpHost"
|
||||
value={sftpHost}
|
||||
onChange={(e) => setSftpHost(e.target.value)}
|
||||
placeholder="sftp.example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sftpPort">Port</Label>
|
||||
<Input
|
||||
id="sftpPort"
|
||||
type="number"
|
||||
value={sftpPort}
|
||||
onChange={(e) => setSftpPort(parseInt(e.target.value) || 22)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sftpUsername">Nom d'utilisateur</Label>
|
||||
<Input
|
||||
id="sftpUsername"
|
||||
value={sftpUsername}
|
||||
onChange={(e) => setSftpUsername(e.target.value)}
|
||||
placeholder="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sftpPassword">Mot de passe</Label>
|
||||
<Input
|
||||
id="sftpPassword"
|
||||
type="password"
|
||||
value={sftpPassword}
|
||||
onChange={(e) => setSftpPassword(e.target.value)}
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sftpRemotePath">Chemin distant</Label>
|
||||
<Input
|
||||
id="sftpRemotePath"
|
||||
value={sftpRemotePath}
|
||||
onChange={(e) => setSftpRemotePath(e.target.value)}
|
||||
placeholder="/invoices"
|
||||
/>
|
||||
<p className="text-xs text-gray-500">
|
||||
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">
|
||||
Exporter automatiquement les factures après extraction
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="sftpAutoExport"
|
||||
checked={sftpAutoExport}
|
||||
onCheckedChange={setSftpAutoExport}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleTestSftp}
|
||||
disabled={testSftpMutation.isPending || !sftpHost}
|
||||
{/* 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"
|
||||
>
|
||||
{testSftpMutation.isPending ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
<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>
|
||||
)}
|
||||
Tester la connexion
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Save button */}
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave} disabled={saveMutation.isPending} size="lg">
|
||||
{/* 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-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-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-3">
|
||||
<Label htmlFor="llmLogsRetentionMonths" className="text-base font-semibold">
|
||||
Rétention des logs (mois)
|
||||
</Label>
|
||||
<Input
|
||||
id="llmLogsRetentionMonths"
|
||||
type="number"
|
||||
value={llmLogsRetentionMonths}
|
||||
onChange={(e) => setLlmLogsRetentionMonths(parseInt(e.target.value) || 3)}
|
||||
min={1}
|
||||
max={12}
|
||||
className="h-11"
|
||||
/>
|
||||
<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>
|
||||
|
||||
<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 className="pt-6">
|
||||
<LlmFieldsConfigSection />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 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-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-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-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-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-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-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 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 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-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-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-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-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-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-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 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>
|
||||
<Switch
|
||||
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-5 h-5 mr-2 animate-spin" />
|
||||
Test en cours...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CheckCircle className="w-5 h-5 mr-2" />
|
||||
Tester la connexion SFTP
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 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
|
||||
</>
|
||||
)}
|
||||
Enregistrer les paramètres
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
14
todo.md
14
todo.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user