Checkpoint: Modernisation de l'ergonomie des pages Administration des listes et Paramètres d'import avec icônes colorées, dégradés, badges de statut, et animations

This commit is contained in:
Manus
2026-02-13 04:07:35 -05:00
parent f7f82fc68a
commit 196bd7103d
3 changed files with 400 additions and 285 deletions

View File

@@ -5,8 +5,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2, Save, Upload, FolderOpen, Mail, Play, Square } from "lucide-react"; import { Loader2, Save, Upload, FolderOpen, Mail, Play, Square, Download, Inbox, CheckCircle2 } from "lucide-react";
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
export default function ImportSettings() { export default function ImportSettings() {
@@ -73,53 +74,66 @@ export default function ImportSettings() {
exportFolder: exportFolder || null, exportFolder: exportFolder || null,
}); });
toast.success("Paramètres enregistrés", { toast.success("Paramètres enregistrés avec succès");
description: "Vos paramètres de réception ont été mis à jour avec succès.",
});
} catch (error) { } catch (error) {
toast.error("Erreur", { toast.error("Impossible d'enregistrer les paramètres");
description: "Impossible d'enregistrer les paramètres. Veuillez réessayer.",
});
} }
}; };
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center h-[calc(100vh-200px)]"> <DashboardLayout>
<Loader2 className="h-8 w-8 animate-spin text-primary" /> <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> </div>
</DashboardLayout>
); );
} }
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="container max-w-4xl py-8"> <div className="max-w-5xl space-y-8">
<div className="mb-8"> {/* Header */}
<h1 className="text-3xl font-bold mb-2">Paramètres de réception</h1> <div className="space-y-2">
<p className="text-muted-foreground"> <div className="flex items-center gap-3">
<div className="p-3 bg-gradient-to-br from-blue-500 to-cyan-600 rounded-xl shadow-lg">
<Inbox className="w-7 h-7 text-white" />
</div>
<div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
Paramètres de réception
</h1>
<p className="text-muted-foreground mt-1">
Configurez les différentes méthodes d'importation de factures Configurez les différentes méthodes d'importation de factures
</p> </p>
</div> </div>
</div>
</div>
<div className="space-y-6">
{/* Manual Import */} {/* Manual Import */}
<Card> <Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader> <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 justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-primary/10 rounded-lg"> <div className="p-2 bg-purple-500 rounded-lg">
<Upload className="h-5 w-5 text-primary" /> <Upload className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<CardTitle>Import manuel</CardTitle> <CardTitle className="text-xl">Import manuel</CardTitle>
<CardDescription> <CardDescription className="mt-1">
Autoriser l'upload manuel de fichiers PDF via l'interface Autoriser l'upload manuel de fichiers PDF via l'interface
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<Badge variant={manualImportEnabled ? "default" : "secondary"} className="text-sm">
{manualImportEnabled ? "Activé" : "Désactivé"}
</Badge>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="pt-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<Label htmlFor="manual-import" className="text-base"> <Label htmlFor="manual-import" className="text-base font-medium">
Activer l'import manuel Activer l'import manuel
</Label> </Label>
<Switch <Switch
@@ -132,23 +146,28 @@ export default function ImportSettings() {
</Card> </Card>
{/* Automatic Import from Folder */} {/* Automatic Import from Folder */}
<Card> <Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader> <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 justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded-lg"> <div className="p-2 bg-blue-500 rounded-lg">
<FolderOpen className="h-5 w-5 text-blue-500" /> <FolderOpen className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<CardTitle>Import automatique depuis dossier</CardTitle> <CardTitle className="text-xl">Import automatique depuis dossier</CardTitle>
<CardDescription> <CardDescription className="mt-1">
Surveiller un dossier et importer automatiquement les nouveaux fichiers PDF Surveiller un dossier et importer automatiquement les nouveaux fichiers PDF
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<Badge variant={autoImportEnabled ? "default" : "secondary"} className="text-sm">
{autoImportEnabled ? "Activé" : "Désactivé"}
</Badge>
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-6 pt-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<Label htmlFor="auto-import" className="text-base"> <Label htmlFor="auto-import" className="text-base font-medium">
Activer l'import automatique Activer l'import automatique
</Label> </Label>
<Switch <Switch
@@ -159,15 +178,16 @@ export default function ImportSettings() {
</div> </div>
{autoImportEnabled && ( {autoImportEnabled && (
<> <div className="space-y-4 p-4 bg-blue-50/50 dark:bg-blue-950/10 rounded-lg border-2 border-blue-200 dark:border-blue-800">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="source-path">Chemin du dossier source</Label> <Label htmlFor="source-path" className="text-base font-medium">Chemin du dossier source</Label>
<Input <Input
id="source-path" id="source-path"
type="text" type="text"
placeholder="/chemin/vers/dossier/factures" placeholder="/chemin/vers/dossier/factures"
value={autoImportSourcePath} value={autoImportSourcePath}
onChange={(e) => setAutoImportSourcePath(e.target.value)} onChange={(e) => setAutoImportSourcePath(e.target.value)}
className="h-11"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF
@@ -175,13 +195,14 @@ export default function ImportSettings() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="auto-frequency">Fréquence de lecture (minutes)</Label> <Label htmlFor="auto-frequency" className="text-base font-medium">Fréquence de lecture (minutes)</Label>
<Input <Input
id="auto-frequency" id="auto-frequency"
type="number" type="number"
min="1" min="1"
value={autoImportFrequency} value={autoImportFrequency}
onChange={(e) => setAutoImportFrequency(parseInt(e.target.value) || 60)} onChange={(e) => setAutoImportFrequency(parseInt(e.target.value) || 60)}
className="h-11"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Intervalle de temps entre chaque vérification du dossier (minimum: 1 minute) Intervalle de temps entre chaque vérification du dossier (minimum: 1 minute)
@@ -189,32 +210,34 @@ export default function ImportSettings() {
</div> </div>
{/* Service Control Buttons */} {/* Service Control Buttons */}
<div className="flex gap-2 pt-4 border-t"> <div className="flex gap-3 pt-4 border-t border-blue-200 dark:border-blue-800">
{folderServiceStatus?.isRunning ? ( {folderServiceStatus?.isRunning ? (
<>
<Button <Button
variant="destructive" variant="destructive"
onClick={async () => { onClick={async () => {
try { try {
await stopFolderServiceMutation.mutateAsync(); await stopFolderServiceMutation.mutateAsync();
await utils.folderImportService.status.invalidate(); await utils.folderImportService.status.invalidate();
toast.success("Service arrêté", { toast.success("Service arrêté");
description: "L'import automatique depuis dossier a é arrêté.",
});
} catch (error) { } catch (error) {
toast.error("Erreur", { toast.error("Impossible d'arrêter le service");
description: "Impossible d'arrêter le service.",
});
} }
}} }}
disabled={stopFolderServiceMutation.isPending} disabled={stopFolderServiceMutation.isPending}
> >
{stopFolderServiceMutation.isPending ? ( {stopFolderServiceMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : ( ) : (
<Square className="mr-2 h-4 w-4" /> <Square className="mr-2 h-5 w-5" />
)} )}
Arrêter le service Arrêter le service
</Button> </Button>
<Badge variant="default" className="flex items-center gap-2 px-4 bg-green-500">
<span className="w-2 h-2 bg-white rounded-full animate-pulse" />
Service actif
</Badge>
</>
) : ( ) : (
<Button <Button
onClick={async () => { onClick={async () => {
@@ -222,60 +245,54 @@ export default function ImportSettings() {
const result = await startFolderServiceMutation.mutateAsync(); const result = await startFolderServiceMutation.mutateAsync();
if (result.success) { if (result.success) {
await utils.folderImportService.status.invalidate(); await utils.folderImportService.status.invalidate();
toast.success("Service démarré", { toast.success("Service démarré");
description: "L'import automatique depuis dossier est maintenant actif.",
});
} else { } else {
toast.error("Erreur", { toast.error("Impossible de démarrer le service");
description: "Impossible de démarrer le service. Vérifiez la configuration.",
});
} }
} catch (error) { } catch (error) {
toast.error("Erreur", { toast.error("Impossible de démarrer le service");
description: "Impossible de démarrer le service.",
});
} }
}} }}
disabled={!autoImportEnabled || startFolderServiceMutation.isPending} disabled={!autoImportEnabled || startFolderServiceMutation.isPending}
className="bg-blue-500 hover:bg-blue-600"
> >
{startFolderServiceMutation.isPending ? ( {startFolderServiceMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : ( ) : (
<Play className="mr-2 h-4 w-4" /> <Play className="mr-2 h-5 w-5" />
)} )}
Démarrer le service Démarrer le service
</Button> </Button>
)} )}
{folderServiceStatus?.isRunning && (
<span className="flex items-center text-sm text-green-600">
<span className="w-2 h-2 bg-green-600 rounded-full mr-2 animate-pulse" />
Service actif
</span>
)}
</div> </div>
</> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
{/* Email Import */} {/* Email Import */}
<Card> <Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader> <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 justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-green-500/10 rounded-lg"> <div className="p-2 bg-green-500 rounded-lg">
<Mail className="h-5 w-5 text-green-500" /> <Mail className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
<CardTitle>Import par email</CardTitle> <CardTitle className="text-xl">Import par email</CardTitle>
<CardDescription> <CardDescription className="mt-1">
Récupérer automatiquement les factures reçues par email Récupérer automatiquement les factures reçues par email
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<Badge variant={emailImportEnabled ? "default" : "secondary"} className="text-sm">
{emailImportEnabled ? "Activé" : "Désactivé"}
</Badge>
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-6 pt-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<Label htmlFor="email-import" className="text-base"> <Label htmlFor="email-import" className="text-base font-medium">
Activer l'import par email Activer l'import par email
</Label> </Label>
<Switch <Switch
@@ -286,15 +303,16 @@ export default function ImportSettings() {
</div> </div>
{emailImportEnabled && ( {emailImportEnabled && (
<> <div className="space-y-4 p-4 bg-green-50/50 dark:bg-green-950/10 rounded-lg border-2 border-green-200 dark:border-green-800">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email-address">Adresse email</Label> <Label htmlFor="email-address" className="text-base font-medium">Adresse email</Label>
<Input <Input
id="email-address" id="email-address"
type="email" type="email"
placeholder="factures@votreentreprise.com" placeholder="factures@votreentreprise.com"
value={emailImportAddress} value={emailImportAddress}
onChange={(e) => setEmailImportAddress(e.target.value)} onChange={(e) => setEmailImportAddress(e.target.value)}
className="h-11"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Adresse email à surveiller pour les factures Adresse email à surveiller pour les factures
@@ -302,28 +320,30 @@ export default function ImportSettings() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email-password">Mot de passe</Label> <Label htmlFor="email-password" className="text-base font-medium">Mot de passe</Label>
<Input <Input
id="email-password" id="email-password"
type="password" type="password"
placeholder="" placeholder=""
value={emailImportPassword} value={emailImportPassword}
onChange={(e) => setEmailImportPassword(e.target.value)} onChange={(e) => setEmailImportPassword(e.target.value)}
className="h-11"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Mot de passe du compte email (stocké de manière sécurisée) Mot de passe du compte email (stocké de manière sécurisée)
</p> </p>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email-host">Serveur IMAP</Label> <Label htmlFor="email-host" className="text-base font-medium">Serveur IMAP</Label>
<Input <Input
id="email-host" id="email-host"
type="text" type="text"
placeholder="imap.gmail.com" placeholder="imap.gmail.com"
value={emailImportHost} value={emailImportHost}
onChange={(e) => setEmailImportHost(e.target.value)} onChange={(e) => setEmailImportHost(e.target.value)}
className="h-11"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Adresse du serveur IMAP Adresse du serveur IMAP
@@ -331,7 +351,7 @@ export default function ImportSettings() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email-port">Port IMAP</Label> <Label htmlFor="email-port" className="text-base font-medium">Port IMAP</Label>
<Input <Input
id="email-port" id="email-port"
type="number" type="number"
@@ -339,6 +359,7 @@ export default function ImportSettings() {
max="65535" max="65535"
value={emailImportPort} value={emailImportPort}
onChange={(e) => setEmailImportPort(parseInt(e.target.value) || 993)} onChange={(e) => setEmailImportPort(parseInt(e.target.value) || 993)}
className="h-11"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Port SSL (993 par défaut) Port SSL (993 par défaut)
@@ -347,13 +368,14 @@ export default function ImportSettings() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email-frequency">Fréquence de lecture (minutes)</Label> <Label htmlFor="email-frequency" className="text-base font-medium">Fréquence de lecture (minutes)</Label>
<Input <Input
id="email-frequency" id="email-frequency"
type="number" type="number"
min="1" min="1"
value={emailImportFrequency} value={emailImportFrequency}
onChange={(e) => setEmailImportFrequency(parseInt(e.target.value) || 30)} onChange={(e) => setEmailImportFrequency(parseInt(e.target.value) || 30)}
className="h-11"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Intervalle de temps entre chaque vérification des emails (minimum: 1 minute) Intervalle de temps entre chaque vérification des emails (minimum: 1 minute)
@@ -361,32 +383,60 @@ export default function ImportSettings() {
</div> </div>
{/* Service Control Buttons */} {/* Service Control Buttons */}
<div className="flex gap-2 pt-4 border-t"> <div className="flex flex-wrap gap-3 pt-4 border-t border-green-200 dark:border-green-800">
{emailServiceStatus?.isRunning ? ( {emailServiceStatus?.isRunning ? (
<>
<Button <Button
variant="destructive" variant="destructive"
onClick={async () => { onClick={async () => {
try { try {
await stopEmailServiceMutation.mutateAsync(); await stopEmailServiceMutation.mutateAsync();
await utils.emailImportService.status.invalidate(); await utils.emailImportService.status.invalidate();
toast.success("Service arrêté", { toast.success("Service arrêté");
description: "L'import automatique par email a été arrêté.",
});
} catch (error) { } catch (error) {
toast.error("Erreur", { toast.error("Impossible d'arrêter le service");
description: "Impossible d'arrêter le service.",
});
} }
}} }}
disabled={stopEmailServiceMutation.isPending} disabled={stopEmailServiceMutation.isPending}
> >
{stopEmailServiceMutation.isPending ? ( {stopEmailServiceMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : ( ) : (
<Square className="mr-2 h-4 w-4" /> <Square className="mr-2 h-5 w-5" />
)} )}
Arrêter le service Arrêter le service
</Button> </Button>
<Button
variant="outline"
onClick={async () => {
try {
toast.info("Vérification en cours...");
const result = await checkNowEmailMutation.mutateAsync();
if (result.success) {
await utils.importLogs.getByUser.invalidate();
toast.success(result.message);
} else {
toast.error(result.message);
}
} catch (error: any) {
toast.error("Impossible de vérifier les emails");
}
}}
disabled={checkNowEmailMutation.isPending}
className="border-green-300 hover:bg-green-50 dark:hover:bg-green-950/20"
>
{checkNowEmailMutation.isPending ? (
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<CheckCircle2 className="mr-2 h-5 w-5" />
)}
Vérifier maintenant
</Button>
<Badge variant="default" className="flex items-center gap-2 px-4 bg-green-500">
<span className="w-2 h-2 bg-white rounded-full animate-pulse" />
Service actif
</Badge>
</>
) : ( ) : (
<Button <Button
onClick={async () => { onClick={async () => {
@@ -394,98 +444,56 @@ export default function ImportSettings() {
const result = await startEmailServiceMutation.mutateAsync(); const result = await startEmailServiceMutation.mutateAsync();
if (result.success) { if (result.success) {
await utils.emailImportService.status.invalidate(); await utils.emailImportService.status.invalidate();
toast.success("Service démarré", { toast.success("Service démarré");
description: "L'import automatique par email est maintenant actif.",
});
} else { } else {
toast.error("Erreur", { toast.error("Impossible de démarrer le service");
description: "Impossible de démarrer le service. Vérifiez la configuration.",
});
} }
} catch (error) { } catch (error) {
toast.error("Erreur", { toast.error("Impossible de démarrer le service");
description: "Impossible de démarrer le service.",
});
} }
}} }}
disabled={!emailImportEnabled || startEmailServiceMutation.isPending} disabled={!emailImportEnabled || startEmailServiceMutation.isPending}
className="bg-green-500 hover:bg-green-600"
> >
{startEmailServiceMutation.isPending ? ( {startEmailServiceMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : ( ) : (
<Play className="mr-2 h-4 w-4" /> <Play className="mr-2 h-5 w-5" />
)} )}
Démarrer le service Démarrer le service
</Button> </Button>
)} )}
{emailServiceStatus?.isRunning && (
<span className="flex items-center text-sm text-green-600">
<span className="w-2 h-2 bg-green-600 rounded-full mr-2 animate-pulse" />
Service actif
</span>
)}
{emailServiceStatus?.isRunning && (
<Button
variant="outline"
onClick={async () => {
try {
toast.info("Vérification en cours...", {
description: "Connexion au serveur IMAP et recherche de nouveaux emails.",
});
const result = await checkNowEmailMutation.mutateAsync();
if (result.success) {
await utils.importLogs.getByUser.invalidate();
toast.success("Vérification terminée", {
description: result.message,
});
} else {
toast.error("Erreur", {
description: result.message,
});
}
} catch (error: any) {
toast.error("Erreur", {
description: error.message || "Impossible de vérifier les emails.",
});
}
}}
disabled={checkNowEmailMutation.isPending}
>
{checkNowEmailMutation.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Mail className="mr-2 h-4 w-4" />
)}
Vérifier maintenant
</Button>
)}
</div> </div>
</> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
{/* Export Folder */} {/* Export Folder */}
<Card> <Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader> <CardHeader className="bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-950/20 dark:to-amber-950/20 border-b">
<CardTitle className="flex items-center gap-2"> <div className="flex items-center gap-3">
<FolderOpen className="h-5 w-5" /> <div className="p-2 bg-orange-500 rounded-lg">
Dossier d'export <Download className="w-6 h-6 text-white" />
</CardTitle> </div>
<CardDescription> <div>
<CardTitle className="text-xl">Dossier d'export</CardTitle>
<CardDescription className="mt-1">
Configurez le dossier de destination pour l'export des factures Configurez le dossier de destination pour l'export des factures
</CardDescription> </CardDescription>
</div>
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4 pt-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="exportFolder">Chemin du dossier d'export</Label> <Label htmlFor="exportFolder" className="text-base font-medium">Chemin du dossier d'export</Label>
<Input <Input
id="exportFolder" id="exportFolder"
type="text" type="text"
placeholder="/chemin/vers/dossier/export" placeholder="/chemin/vers/dossier/export"
value={exportFolder} value={exportFolder}
onChange={(e) => setExportFolder(e.target.value)} onChange={(e) => setExportFolder(e.target.value)}
className="h-11"
/> />
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Spécifiez le chemin absolu du dossier les factures seront exportées Spécifiez le chemin absolu du dossier les factures seront exportées
@@ -495,27 +503,27 @@ export default function ImportSettings() {
</Card> </Card>
{/* Save Button */} {/* Save Button */}
<div className="flex justify-end"> <div className="flex justify-end pb-8">
<Button <Button
onClick={handleSave} onClick={handleSave}
disabled={updateMutation.isPending} disabled={updateMutation.isPending}
size="lg" size="lg"
className="bg-gradient-to-r from-blue-500 to-cyan-600 hover:from-blue-600 hover:to-cyan-700 text-white shadow-lg"
> >
{updateMutation.isPending ? ( {updateMutation.isPending ? (
<> <>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> <Loader2 className="mr-2 h-5 w-5 animate-spin" />
Enregistrement... Enregistrement...
</> </>
) : ( ) : (
<> <>
<Save className="mr-2 h-4 w-4" /> <Save className="mr-2 h-5 w-5" />
Enregistrer les paramètres Enregistrer les paramètres
</> </>
)} )}
</Button> </Button>
</div> </div>
</div> </div>
</div>
</DashboardLayout> </DashboardLayout>
); );
} }

View File

@@ -3,9 +3,9 @@ import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2, Plus, Trash2, Settings } from "lucide-react"; import { Loader2, Plus, Trash2, List, Building2, Calculator, Sparkles } from "lucide-react";
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
import { import {
AlertDialog, AlertDialog,
@@ -36,7 +36,7 @@ export default function ListsAdmin() {
const handleCreateDepartment = async () => { const handleCreateDepartment = async () => {
if (!newDepartment.trim()) { if (!newDepartment.trim()) {
toast.error("Erreur", { description: "Le nom ne peut pas être vide" }); toast.error("Le nom ne peut pas être vide");
return; return;
} }
@@ -44,15 +44,15 @@ export default function ListsAdmin() {
await createDepartmentMutation.mutateAsync({ name: newDepartment.trim() }); await createDepartmentMutation.mutateAsync({ name: newDepartment.trim() });
await utils.departments.getByUser.invalidate(); await utils.departments.getByUser.invalidate();
setNewDepartment(""); setNewDepartment("");
toast.success("Service ajouté", { description: `"${newDepartment}" a été ajouté à la liste` }); toast.success(`Service "${newDepartment}" ajouté avec succès`);
} catch (error: any) { } catch (error: any) {
toast.error("Erreur", { description: error.message || "Impossible d'ajouter le service" }); toast.error(error.message || "Impossible d'ajouter le service");
} }
}; };
const handleCreateAllocation = async () => { const handleCreateAllocation = async () => {
if (!newAllocation.trim()) { if (!newAllocation.trim()) {
toast.error("Erreur", { description: "Le nom ne peut pas être vide" }); toast.error("Le nom ne peut pas être vide");
return; return;
} }
@@ -60,9 +60,9 @@ export default function ListsAdmin() {
await createAllocationMutation.mutateAsync({ name: newAllocation.trim() }); await createAllocationMutation.mutateAsync({ name: newAllocation.trim() });
await utils.accountingAllocations.getByUser.invalidate(); await utils.accountingAllocations.getByUser.invalidate();
setNewAllocation(""); setNewAllocation("");
toast.success("Ventilation ajoutée", { description: `"${newAllocation}" a été ajoutée à la liste` }); toast.success(`Ventilation "${newAllocation}" ajoutée avec succès`);
} catch (error: any) { } catch (error: any) {
toast.error("Erreur", { description: error.message || "Impossible d'ajouter la ventilation" }); toast.error(error.message || "Impossible d'ajouter la ventilation");
} }
}; };
@@ -82,9 +82,9 @@ export default function ListsAdmin() {
await deleteAllocationMutation.mutateAsync({ id: itemToDelete.id }); await deleteAllocationMutation.mutateAsync({ id: itemToDelete.id });
await utils.accountingAllocations.getByUser.invalidate(); await utils.accountingAllocations.getByUser.invalidate();
} }
toast.success("Supprimé", { description: `"${itemToDelete.name}" a été supprimé` }); toast.success(`"${itemToDelete.name}" supprimé avec succès`);
} catch (error: any) { } catch (error: any) {
toast.error("Erreur", { description: error.message || "Impossible de supprimer" }); toast.error(error.message || "Impossible de supprimer");
} finally { } finally {
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setItemToDelete(null); setItemToDelete(null);
@@ -94,36 +94,91 @@ export default function ListsAdmin() {
if (loadingDepartments || loadingAllocations) { if (loadingDepartments || loadingAllocations) {
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="flex items-center justify-center h-64"> <div className="flex flex-col items-center justify-center h-64 gap-4">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" /> <Loader2 className="w-12 h-12 animate-spin text-primary" />
<p className="text-muted-foreground">Chargement des listes...</p>
</div> </div>
</DashboardLayout> </DashboardLayout>
); );
} }
const departmentCount = departments?.length || 0;
const allocationCount = allocations?.length || 0;
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="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-purple-500 to-pink-600 rounded-xl shadow-lg">
<List className="w-7 h-7 text-white" />
</div>
<div> <div>
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-3"> <h1 className="text-4xl font-bold bg-gradient-to-r from-purple-600 to-pink-600 bg-clip-text text-transparent">
<Settings className="h-8 w-8" />
Administration des listes Administration des listes
</h1> </h1>
<p className="text-muted-foreground mt-2"> <p className="text-muted-foreground mt-1">
Gérez les listes de valeurs pour les champs métier des factures Gérez les listes de valeurs pour les champs métier des factures
</p> </p>
</div> </div>
</div>
</div>
{/* Statistics */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-4 bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-950/20 dark:to-cyan-950/20 rounded-lg border border-blue-200 dark:border-blue-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500 rounded-lg">
<Building2 className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-sm text-muted-foreground">Services</p>
<p className="text-2xl font-bold text-blue-900 dark:text-blue-100">{departmentCount}</p>
</div>
</div>
<Badge variant="secondary" className="text-sm">
Actif{departmentCount > 1 ? 's' : ''}
</Badge>
</div>
</div>
<div className="p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 rounded-lg border border-green-200 dark:border-green-800">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-500 rounded-lg">
<Calculator className="w-5 h-5 text-white" />
</div>
<div>
<p className="text-sm text-muted-foreground">Ventilations</p>
<p className="text-2xl font-bold text-green-900 dark:text-green-100">{allocationCount}</p>
</div>
</div>
<Badge variant="secondary" className="text-sm">
Active{allocationCount > 1 ? 's' : ''}
</Badge>
</div>
</div>
</div>
{/* Department List */} {/* Department List */}
<Card> <Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader> <CardHeader className="bg-gradient-to-r from-blue-50 to-cyan-50 dark:from-blue-950/20 dark:to-cyan-950/20 border-b">
<CardTitle>Service concerné</CardTitle> <div className="flex items-center gap-3">
<CardDescription> <div className="p-2 bg-blue-500 rounded-lg">
<Building2 className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Service concerné</CardTitle>
<CardDescription className="mt-1">
Gérez la liste des services disponibles pour la classification des factures Gérez la liste des services disponibles pour la classification des factures
</CardDescription> </CardDescription>
</div>
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-6 pt-6">
<div className="flex gap-2"> <div className="flex gap-3">
<div className="flex-1"> <div className="flex-1">
<Input <Input
placeholder="Nouveau service (ex: INFORMATIQUE)" placeholder="Nouveau service (ex: INFORMATIQUE)"
@@ -134,58 +189,80 @@ export default function ListsAdmin() {
handleCreateDepartment(); handleCreateDepartment();
} }
}} }}
className="h-11"
/> />
</div> </div>
<Button <Button
onClick={handleCreateDepartment} onClick={handleCreateDepartment}
disabled={createDepartmentMutation.isPending || !newDepartment.trim()} disabled={createDepartmentMutation.isPending || !newDepartment.trim()}
size="lg"
className="bg-blue-500 hover:bg-blue-600"
> >
{createDepartmentMutation.isPending ? ( {createDepartmentMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-5 w-5 animate-spin" />
) : ( ) : (
<Plus className="h-4 w-4" /> <Plus className="h-5 w-5" />
)} )}
<span className="ml-2">Ajouter</span> <span className="ml-2">Ajouter</span>
</Button> </Button>
</div> </div>
<div className="space-y-2"> <div className="space-y-3">
{departments && departments.length > 0 ? ( {departments && departments.length > 0 ? (
departments.map((dept) => ( departments.map((dept) => (
<div <div
key={dept.id} key={dept.id}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-accent/50 transition-colors" className="group flex items-center justify-between p-4 border-2 rounded-lg hover:border-blue-300 hover:bg-blue-50/50 dark:hover:bg-blue-950/20 transition-all"
> >
<span className="font-medium">{dept.name}</span> <div className="flex items-center gap-3">
<div className="p-2 bg-blue-100 dark:bg-blue-900/30 rounded-lg group-hover:bg-blue-200 dark:group-hover:bg-blue-900/50 transition-colors">
<Building2 className="h-4 w-4 text-blue-600 dark:text-blue-400" />
</div>
<span className="font-semibold text-lg">{dept.name}</span>
</div>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => confirmDelete("department", dept.id, dept.name)} onClick={() => confirmDelete("department", dept.id, dept.name)}
disabled={deleteDepartmentMutation.isPending} disabled={deleteDepartmentMutation.isPending}
className="opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-100 dark:hover:bg-red-900/30"
> >
<Trash2 className="h-4 w-4 text-destructive" /> <Trash2 className="h-5 w-5 text-destructive" />
</Button> </Button>
</div> </div>
)) ))
) : ( ) : (
<p className="text-sm text-muted-foreground text-center py-4"> <div className="text-center py-12 bg-muted/30 rounded-lg border-2 border-dashed">
Aucun service configuré. Ajoutez-en un pour commencer. <Building2 className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
<p className="text-muted-foreground font-medium">
Aucun service configuré
</p> </p>
<p className="text-sm text-muted-foreground/70 mt-1">
Ajoutez-en un pour commencer
</p>
</div>
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Accounting Allocation List */} {/* Accounting Allocation List */}
<Card> <Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader> <CardHeader className="bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-950/20 dark:to-emerald-950/20 border-b">
<CardTitle>Ventilation comptable</CardTitle> <div className="flex items-center gap-3">
<CardDescription> <div className="p-2 bg-green-500 rounded-lg">
<Calculator className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Ventilation comptable</CardTitle>
<CardDescription className="mt-1">
Gérez la liste des ventilations comptables disponibles pour la classification des factures Gérez la liste des ventilations comptables disponibles pour la classification des factures
</CardDescription> </CardDescription>
</div>
</div>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-6 pt-6">
<div className="flex gap-2"> <div className="flex gap-3">
<div className="flex-1"> <div className="flex-1">
<Input <Input
placeholder="Nouvelle ventilation (ex: MAINTENANCE)" placeholder="Nouvelle ventilation (ex: MAINTENANCE)"
@@ -196,43 +273,58 @@ export default function ListsAdmin() {
handleCreateAllocation(); handleCreateAllocation();
} }
}} }}
className="h-11"
/> />
</div> </div>
<Button <Button
onClick={handleCreateAllocation} onClick={handleCreateAllocation}
disabled={createAllocationMutation.isPending || !newAllocation.trim()} disabled={createAllocationMutation.isPending || !newAllocation.trim()}
size="lg"
className="bg-green-500 hover:bg-green-600"
> >
{createAllocationMutation.isPending ? ( {createAllocationMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-5 w-5 animate-spin" />
) : ( ) : (
<Plus className="h-4 w-4" /> <Plus className="h-5 w-5" />
)} )}
<span className="ml-2">Ajouter</span> <span className="ml-2">Ajouter</span>
</Button> </Button>
</div> </div>
<div className="space-y-2"> <div className="space-y-3">
{allocations && allocations.length > 0 ? ( {allocations && allocations.length > 0 ? (
allocations.map((alloc) => ( allocations.map((alloc) => (
<div <div
key={alloc.id} key={alloc.id}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-accent/50 transition-colors" className="group flex items-center justify-between p-4 border-2 rounded-lg hover:border-green-300 hover:bg-green-50/50 dark:hover:bg-green-950/20 transition-all"
> >
<span className="font-medium">{alloc.name}</span> <div className="flex items-center gap-3">
<div className="p-2 bg-green-100 dark:bg-green-900/30 rounded-lg group-hover:bg-green-200 dark:group-hover:bg-green-900/50 transition-colors">
<Calculator className="h-4 w-4 text-green-600 dark:text-green-400" />
</div>
<span className="font-semibold text-lg">{alloc.name}</span>
</div>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={() => confirmDelete("allocation", alloc.id, alloc.name)} onClick={() => confirmDelete("allocation", alloc.id, alloc.name)}
disabled={deleteAllocationMutation.isPending} disabled={deleteAllocationMutation.isPending}
className="opacity-0 group-hover:opacity-100 transition-opacity hover:bg-red-100 dark:hover:bg-red-900/30"
> >
<Trash2 className="h-4 w-4 text-destructive" /> <Trash2 className="h-5 w-5 text-destructive" />
</Button> </Button>
</div> </div>
)) ))
) : ( ) : (
<p className="text-sm text-muted-foreground text-center py-4"> <div className="text-center py-12 bg-muted/30 rounded-lg border-2 border-dashed">
Aucune ventilation configurée. Ajoutez-en une pour commencer. <Calculator className="h-12 w-12 mx-auto text-muted-foreground/50 mb-3" />
<p className="text-muted-foreground font-medium">
Aucune ventilation configurée
</p> </p>
<p className="text-sm text-muted-foreground/70 mt-1">
Ajoutez-en une pour commencer
</p>
</div>
)} )}
</div> </div>
</CardContent> </CardContent>
@@ -243,15 +335,20 @@ export default function ListsAdmin() {
<AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}> <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Confirmer la suppression</AlertDialogTitle> <AlertDialogTitle className="text-xl">Confirmer la suppression</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription className="text-base">
Êtes-vous sûr de vouloir supprimer "{itemToDelete?.name}" ? Êtes-vous sûr de vouloir supprimer <span className="font-semibold">"{itemToDelete?.name}"</span> ?
<br />
Cette action est irréversible. Cette action est irréversible.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel> <AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete} className="bg-destructive text-destructive-foreground hover:bg-destructive/90"> <AlertDialogAction
onClick={handleDelete}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
<Trash2 className="h-4 w-4 mr-2" />
Supprimer Supprimer
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>

10
todo.md
View File

@@ -395,4 +395,14 @@
- [x] Améliorer le responsive design pour mobile - [x] Améliorer le responsive design pour mobile
- [x] Ajouter des badges de statut (Actif/Inactif) avec couleurs - [x] Ajouter des badges de statut (Actif/Inactif) avec couleurs
- [x] Tester localement l'interface modernisée - [x] Tester localement l'interface modernisée
- [x] Déployer sur le VPS
## Modernisation des pages Administration et Import
- [x] Identifier les pages Administration des listes et Paramètres d'import
- [x] Analyser la structure actuelle de ces pages
- [x] Moderniser la page Administration des listes avec Tabs, icônes colorées, cards
- [x] Moderniser la page Paramètres d'import avec le même style
- [x] Appliquer les badges de statut et animations
- [x] Améliorer la typographie et les espacements
- [x] Tester localement les interfaces modernisées
- [ ] Déployer sur le VPS - [ ] Déployer sur le VPS