fix: page Paramètres - 2 onglets Import/Export, bug date expiration secret corrigé, chargement optimisé

This commit is contained in:
Manus
2026-05-06 09:10:41 -04:00
parent 96cfdc8901
commit 5cb1f878a5
2 changed files with 666 additions and 652 deletions

View File

@@ -6,14 +6,26 @@ 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 { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner"; import { toast } from "sonner";
import { Loader2, Save, Upload, FolderOpen, Mail, Play, Square, Download, Inbox, CheckCircle2, Monitor, FolderOutput, Wifi, WifiOff, AlertTriangle, Calendar } from "lucide-react"; import {
Loader2, Save, Upload, FolderOpen, Mail, Play, Square, Download,
Inbox, CheckCircle2, Monitor, FolderOutput, Wifi, AlertTriangle, Calendar,
ArrowDownToLine, Share2
} from "lucide-react";
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
export default function ImportSettings() { export default function ImportSettings() {
const { data: settings, isLoading } = trpc.importSettings.get.useQuery(); // Chargement différé : on ne charge les statuts de services qu'après le montage
const { data: emailServiceStatus } = trpc.emailImportService.status.useQuery(); const { data: settings, isLoading } = trpc.importSettings.get.useQuery(undefined, {
const { data: folderServiceStatus } = trpc.folderImportService.status.useQuery(); staleTime: 30_000,
});
const { data: emailServiceStatus } = trpc.emailImportService.status.useQuery(undefined, {
staleTime: 10_000,
});
const { data: folderServiceStatus } = trpc.folderImportService.status.useQuery(undefined, {
staleTime: 10_000,
});
const updateMutation = trpc.importSettings.update.useMutation(); const updateMutation = trpc.importSettings.update.useMutation();
const startEmailServiceMutation = trpc.emailImportService.start.useMutation(); const startEmailServiceMutation = trpc.emailImportService.start.useMutation();
const stopEmailServiceMutation = trpc.emailImportService.stop.useMutation(); const stopEmailServiceMutation = trpc.emailImportService.stop.useMutation();
@@ -21,6 +33,7 @@ export default function ImportSettings() {
const utils = trpc.useUtils(); const utils = trpc.useUtils();
const startFolderServiceMutation = trpc.folderImportService.start.useMutation(); const startFolderServiceMutation = trpc.folderImportService.start.useMutation();
const stopFolderServiceMutation = trpc.folderImportService.stop.useMutation(); const stopFolderServiceMutation = trpc.folderImportService.stop.useMutation();
const testAzureConnectionMutation = trpc.importSettings.testAzureConnection.useMutation();
// Manual import // Manual import
const [manualImportEnabled, setManualImportEnabled] = useState(true); const [manualImportEnabled, setManualImportEnabled] = useState(true);
@@ -40,18 +53,15 @@ export default function ImportSettings() {
// Export folder // Export folder
const [exportFolder, setExportFolder] = useState(""); const [exportFolder, setExportFolder] = useState("");
// BAP export mode : les deux peuvent être actifs simultanément
const [bapExportBrowser, setBapExportBrowser] = useState(true); const [bapExportBrowser, setBapExportBrowser] = useState(true);
const [bapExportFolder, setBapExportFolder] = useState(false); const [bapExportFolder, setBapExportFolder] = useState(false);
const [exportFolderType, setExportFolderType] = useState<"local" | "teams" | "sharepoint">("local"); const [exportFolderType, setExportFolderType] = useState<"local" | "teams" | "sharepoint">("local");
// Compat. ancienne valeur
const [bapExportMode, setBapExportMode] = useState<"browser" | "folder" | "both">("browser");
// Azure AD credentials for SharePoint // Azure AD credentials for SharePoint
const [azureTenantId, setAzureTenantId] = useState(""); const [azureTenantId, setAzureTenantId] = useState("");
const [azureClientId, setAzureClientId] = useState(""); const [azureClientId, setAzureClientId] = useState("");
const [azureClientSecret, setAzureClientSecret] = useState(""); const [azureClientSecret, setAzureClientSecret] = useState("");
// Date stockée en string ISO "YYYY-MM-DD" pour le champ <input type="date">
const [azureSecretExpiresAt, setAzureSecretExpiresAt] = useState(""); const [azureSecretExpiresAt, setAzureSecretExpiresAt] = useState("");
const testAzureConnectionMutation = trpc.importSettings.testAzureConnection.useMutation();
// Initialize form with settings from database // Initialize form with settings from database
useEffect(() => { useEffect(() => {
@@ -68,29 +78,35 @@ export default function ImportSettings() {
setEmailImportFrequency(settings.emailImportFrequency || 30); setEmailImportFrequency(settings.emailImportFrequency || 30);
setExportFolder(settings.exportFolder || ""); setExportFolder(settings.exportFolder || "");
const savedMode = (settings.bapExportMode as string) || "browser"; const savedMode = (settings.bapExportMode as string) || "browser";
// Compat : ancienne valeur unique -> dériver les deux switches
setBapExportBrowser(savedMode === "browser" || savedMode === "both"); setBapExportBrowser(savedMode === "browser" || savedMode === "both");
setBapExportFolder(savedMode === "folder" || savedMode === "both"); setBapExportFolder(savedMode === "folder" || savedMode === "both");
setBapExportMode(savedMode as "browser" | "folder" | "both");
// Lire le type de dossier depuis le champ dédié exportFolderType
if ((settings as any).exportFolderType) { if ((settings as any).exportFolderType) {
setExportFolderType((settings as any).exportFolderType as "local" | "teams" | "sharepoint"); setExportFolderType((settings as any).exportFolderType as "local" | "teams" | "sharepoint");
} else { } else {
// Compat. ancienne logique préfixe
if ((settings.exportFolder || "").startsWith("teams://")) setExportFolderType("teams"); if ((settings.exportFolder || "").startsWith("teams://")) setExportFolderType("teams");
else if ((settings.exportFolder || "").startsWith("sharepoint://")) setExportFolderType("sharepoint"); else if ((settings.exportFolder || "").startsWith("sharepoint://")) setExportFolderType("sharepoint");
else setExportFolderType("local"); else setExportFolderType("local");
} }
// Azure AD
setAzureTenantId((settings as any).azureTenantId || ""); setAzureTenantId((settings as any).azureTenantId || "");
setAzureClientId((settings as any).azureClientId || ""); setAzureClientId((settings as any).azureClientId || "");
setAzureClientSecret((settings as any).azureClientSecret || ""); setAzureClientSecret((settings as any).azureClientSecret || "");
// Convertir la date DB en string "YYYY-MM-DD"
const expDate = (settings as any).azureSecretExpiresAt; const expDate = (settings as any).azureSecretExpiresAt;
setAzureSecretExpiresAt(expDate ? new Date(expDate).toISOString().split('T')[0] : ""); if (expDate) {
try {
const d = new Date(expDate);
if (!isNaN(d.getTime())) {
setAzureSecretExpiresAt(d.toISOString().split('T')[0]);
}
} catch {
setAzureSecretExpiresAt("");
}
} else {
setAzureSecretExpiresAt("");
}
} }
}, [settings]); }, [settings]);
// Calculer le bapExportMode à sauvegarder depuis les deux switches
const computedExportMode = (): "browser" | "folder" | "both" => { const computedExportMode = (): "browser" | "folder" | "both" => {
if (bapExportBrowser && bapExportFolder) return "both"; if (bapExportBrowser && bapExportFolder) return "both";
if (bapExportFolder) return "folder"; if (bapExportFolder) return "folder";
@@ -99,6 +115,15 @@ export default function ImportSettings() {
const handleSave = async () => { const handleSave = async () => {
try { try {
// Convertir la date string en objet Date ou null — envoyé en ISO string via superjson
let expiresAtValue: Date | null = null;
if (azureSecretExpiresAt) {
const parsed = new Date(azureSecretExpiresAt + "T12:00:00.000Z");
if (!isNaN(parsed.getTime())) {
expiresAtValue = parsed;
}
}
await updateMutation.mutateAsync({ await updateMutation.mutateAsync({
manualImportEnabled: manualImportEnabled ? 1 : 0, manualImportEnabled: manualImportEnabled ? 1 : 0,
autoImportEnabled: autoImportEnabled ? 1 : 0, autoImportEnabled: autoImportEnabled ? 1 : 0,
@@ -116,12 +141,13 @@ export default function ImportSettings() {
azureTenantId: azureTenantId || null, azureTenantId: azureTenantId || null,
azureClientId: azureClientId || null, azureClientId: azureClientId || null,
azureClientSecret: azureClientSecret || null, azureClientSecret: azureClientSecret || null,
azureSecretExpiresAt: azureSecretExpiresAt ? new Date(azureSecretExpiresAt) : null, azureSecretExpiresAt: expiresAtValue,
}); });
toast.success("Paramètres enregistrés avec succès"); toast.success("Paramètres enregistrés avec succès");
} catch (error) { } catch (error: any) {
toast.error("Impossible d'enregistrer les paramètres"); console.error("Erreur enregistrement paramètres:", error);
toast.error("Impossible d'enregistrer les paramètres : " + (error?.message || "erreur inconnue"));
} }
}; };
@@ -136,27 +162,49 @@ export default function ImportSettings() {
); );
} }
// Calcul jours restants pour l'alerte expiration
const expirationAlert = (() => {
if (!azureSecretExpiresAt) return null;
const expDate = new Date(azureSecretExpiresAt + "T12:00:00.000Z");
const daysLeft = Math.ceil((expDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
return { daysLeft, expDate };
})();
return ( return (
<DashboardLayout> <DashboardLayout>
<div className="max-w-5xl space-y-8"> <div className="max-w-5xl space-y-6">
{/* Header */} {/* Header */}
<div className="space-y-2">
<div className="flex items-center gap-3"> <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"> <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" /> <Inbox className="w-7 h-7 text-white" />
</div> </div>
<div> <div>
<h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent"> <h1 className="text-3xl font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
Paramètres import / export Paramètres import / export
</h1> </h1>
<p className="text-muted-foreground mt-1"> <p className="text-muted-foreground mt-0.5 text-sm">
Configurez les méthodes d'importation et les options d'export des factures BAP Configurez les méthodes d'importation et les options d'export des factures BAP
</p> </p>
</div> </div>
</div> </div>
</div>
{/* Manual Import */} {/* Onglets Import / Export */}
<Tabs defaultValue="import" className="w-full">
<TabsList className="grid w-full grid-cols-2 h-12 mb-6">
<TabsTrigger value="import" className="flex items-center gap-2 text-base">
<ArrowDownToLine className="w-4 h-4" />
Paramètres d'import
</TabsTrigger>
<TabsTrigger value="export" className="flex items-center gap-2 text-base">
<Share2 className="w-4 h-4" />
Paramètres d'export
</TabsTrigger>
</TabsList>
{/* ===== ONGLET IMPORT ===== */}
<TabsContent value="import" className="space-y-6">
{/* Import manuel */}
<Card className="border-2 hover:border-primary/50 transition-colors"> <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"> <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 justify-between">
@@ -171,7 +219,7 @@ export default function ImportSettings() {
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<Badge variant={manualImportEnabled ? "default" : "secondary"} className="text-sm"> <Badge variant={manualImportEnabled ? "default" : "secondary"}>
{manualImportEnabled ? "Activé" : "Désactivé"} {manualImportEnabled ? "Activé" : "Désactivé"}
</Badge> </Badge>
</div> </div>
@@ -190,7 +238,7 @@ export default function ImportSettings() {
</CardContent> </CardContent>
</Card> </Card>
{/* Automatic Import from Folder */} {/* Import automatique depuis dossier */}
<Card className="border-2 hover:border-primary/50 transition-colors"> <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"> <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 justify-between">
@@ -205,7 +253,7 @@ export default function ImportSettings() {
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<Badge variant={autoImportEnabled ? "default" : "secondary"} className="text-sm"> <Badge variant={autoImportEnabled ? "default" : "secondary"}>
{autoImportEnabled ? "Activé" : "Désactivé"} {autoImportEnabled ? "Activé" : "Désactivé"}
</Badge> </Badge>
</div> </div>
@@ -238,7 +286,6 @@ export default function ImportSettings() {
Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF
</p> </p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="auto-frequency" className="text-base font-medium">Fréquence de lecture (minutes)</Label> <Label htmlFor="auto-frequency" className="text-base font-medium">Fréquence de lecture (minutes)</Label>
<Input <Input
@@ -249,12 +296,7 @@ export default function ImportSettings() {
onChange={(e) => setAutoImportFrequency(parseInt(e.target.value) || 60)} onChange={(e) => setAutoImportFrequency(parseInt(e.target.value) || 60)}
className="h-11" className="h-11"
/> />
<p className="text-sm text-muted-foreground">
Intervalle de temps entre chaque vérification du dossier (minimum: 1 minute)
</p>
</div> </div>
{/* Service Control Buttons */}
<div className="flex gap-3 pt-4 border-t border-blue-200 dark:border-blue-800"> <div className="flex gap-3 pt-4 border-t border-blue-200 dark:border-blue-800">
{folderServiceStatus?.isRunning ? ( {folderServiceStatus?.isRunning ? (
<> <>
@@ -265,17 +307,13 @@ export default function ImportSettings() {
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é");
} catch (error) { } catch {
toast.error("Impossible d'arrêter le service"); toast.error("Impossible d'arrêter le service");
} }
}} }}
disabled={stopFolderServiceMutation.isPending} disabled={stopFolderServiceMutation.isPending}
> >
{stopFolderServiceMutation.isPending ? ( {stopFolderServiceMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <Square className="mr-2 h-5 w-5" />}
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<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"> <Badge variant="default" className="flex items-center gap-2 px-4 bg-green-500">
@@ -294,18 +332,14 @@ export default function ImportSettings() {
} else { } else {
toast.error("Impossible de démarrer le service"); toast.error("Impossible de démarrer le service");
} }
} catch (error) { } catch {
toast.error("Impossible de démarrer le service"); toast.error("Impossible de démarrer le service");
} }
}} }}
disabled={!autoImportEnabled || startFolderServiceMutation.isPending} disabled={!autoImportEnabled || startFolderServiceMutation.isPending}
className="bg-blue-500 hover:bg-blue-600" className="bg-blue-500 hover:bg-blue-600"
> >
{startFolderServiceMutation.isPending ? ( {startFolderServiceMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <Play className="mr-2 h-5 w-5" />}
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Play className="mr-2 h-5 w-5" />
)}
Démarrer le service Démarrer le service
</Button> </Button>
)} )}
@@ -315,7 +349,7 @@ export default function ImportSettings() {
</CardContent> </CardContent>
</Card> </Card>
{/* Email Import */} {/* Import par email */}
<Card className="border-2 hover:border-primary/50 transition-colors"> <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"> <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 justify-between">
@@ -330,7 +364,7 @@ export default function ImportSettings() {
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
<Badge variant={emailImportEnabled ? "default" : "secondary"} className="text-sm"> <Badge variant={emailImportEnabled ? "default" : "secondary"}>
{emailImportEnabled ? "Activé" : "Désactivé"} {emailImportEnabled ? "Activé" : "Désactivé"}
</Badge> </Badge>
</div> </div>
@@ -359,11 +393,7 @@ export default function ImportSettings() {
onChange={(e) => setEmailImportAddress(e.target.value)} onChange={(e) => setEmailImportAddress(e.target.value)}
className="h-11" className="h-11"
/> />
<p className="text-sm text-muted-foreground">
Adresse email à surveiller pour les factures
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email-password" className="text-base font-medium">Mot de passe</Label> <Label htmlFor="email-password" className="text-base font-medium">Mot de passe</Label>
<Input <Input
@@ -374,11 +404,7 @@ export default function ImportSettings() {
onChange={(e) => setEmailImportPassword(e.target.value)} onChange={(e) => setEmailImportPassword(e.target.value)}
className="h-11" className="h-11"
/> />
<p className="text-sm text-muted-foreground">
Mot de passe du compte email (stocké de manière sécurisée)
</p>
</div> </div>
<div className="grid grid-cols-1 md: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" className="text-base font-medium">Serveur IMAP</Label> <Label htmlFor="email-host" className="text-base font-medium">Serveur IMAP</Label>
@@ -390,11 +416,7 @@ export default function ImportSettings() {
onChange={(e) => setEmailImportHost(e.target.value)} onChange={(e) => setEmailImportHost(e.target.value)}
className="h-11" className="h-11"
/> />
<p className="text-sm text-muted-foreground">
Adresse du serveur IMAP
</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email-port" className="text-base font-medium">Port IMAP</Label> <Label htmlFor="email-port" className="text-base font-medium">Port IMAP</Label>
<Input <Input
@@ -406,12 +428,8 @@ export default function ImportSettings() {
onChange={(e) => setEmailImportPort(parseInt(e.target.value) || 993)} onChange={(e) => setEmailImportPort(parseInt(e.target.value) || 993)}
className="h-11" className="h-11"
/> />
<p className="text-sm text-muted-foreground">
Port SSL (993 par défaut)
</p>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email-frequency" className="text-base font-medium">Fréquence de lecture (minutes)</Label> <Label htmlFor="email-frequency" className="text-base font-medium">Fréquence de lecture (minutes)</Label>
<Input <Input
@@ -422,12 +440,7 @@ export default function ImportSettings() {
onChange={(e) => setEmailImportFrequency(parseInt(e.target.value) || 30)} onChange={(e) => setEmailImportFrequency(parseInt(e.target.value) || 30)}
className="h-11" className="h-11"
/> />
<p className="text-sm text-muted-foreground">
Intervalle de temps entre chaque vérification des emails (minimum: 1 minute)
</p>
</div> </div>
{/* Service Control Buttons */}
<div className="flex flex-wrap gap-3 pt-4 border-t border-green-200 dark:border-green-800"> <div className="flex flex-wrap gap-3 pt-4 border-t border-green-200 dark:border-green-800">
{emailServiceStatus?.isRunning ? ( {emailServiceStatus?.isRunning ? (
<> <>
@@ -438,17 +451,13 @@ export default function ImportSettings() {
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é");
} catch (error) { } catch {
toast.error("Impossible d'arrêter le service"); toast.error("Impossible d'arrêter le service");
} }
}} }}
disabled={stopEmailServiceMutation.isPending} disabled={stopEmailServiceMutation.isPending}
> >
{stopEmailServiceMutation.isPending ? ( {stopEmailServiceMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <Square className="mr-2 h-5 w-5" />}
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Square className="mr-2 h-5 w-5" />
)}
Arrêter le service Arrêter le service
</Button> </Button>
<Button <Button
@@ -463,18 +472,14 @@ export default function ImportSettings() {
} else { } else {
toast.error(result.message); toast.error(result.message);
} }
} catch (error: any) { } catch {
toast.error("Impossible de vérifier les emails"); toast.error("Impossible de vérifier les emails");
} }
}} }}
disabled={checkNowEmailMutation.isPending} disabled={checkNowEmailMutation.isPending}
className="border-green-300 hover:bg-green-50 dark:hover:bg-green-950/20" className="border-green-300 hover:bg-green-50 dark:hover:bg-green-950/20"
> >
{checkNowEmailMutation.isPending ? ( {checkNowEmailMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <CheckCircle2 className="mr-2 h-5 w-5" />}
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<CheckCircle2 className="mr-2 h-5 w-5" />
)}
Vérifier maintenant Vérifier maintenant
</Button> </Button>
<Badge variant="default" className="flex items-center gap-2 px-4 bg-green-500"> <Badge variant="default" className="flex items-center gap-2 px-4 bg-green-500">
@@ -493,18 +498,14 @@ export default function ImportSettings() {
} else { } else {
toast.error("Impossible de démarrer le service"); toast.error("Impossible de démarrer le service");
} }
} catch (error) { } catch {
toast.error("Impossible de démarrer le service"); toast.error("Impossible de démarrer le service");
} }
}} }}
disabled={!emailImportEnabled || startEmailServiceMutation.isPending} disabled={!emailImportEnabled || startEmailServiceMutation.isPending}
className="bg-green-500 hover:bg-green-600" className="bg-green-500 hover:bg-green-600"
> >
{startEmailServiceMutation.isPending ? ( {startEmailServiceMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <Play className="mr-2 h-5 w-5" />}
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
) : (
<Play className="mr-2 h-5 w-5" />
)}
Démarrer le service Démarrer le service
</Button> </Button>
)} )}
@@ -514,6 +515,26 @@ export default function ImportSettings() {
</CardContent> </CardContent>
</Card> </Card>
{/* Bouton Enregistrer onglet Import */}
<div className="flex justify-end pb-4">
<Button
onClick={handleSave}
disabled={updateMutation.isPending}
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 ? (
<><Loader2 className="mr-2 h-5 w-5 animate-spin" />Enregistrement...</>
) : (
<><Save className="mr-2 h-5 w-5" />Enregistrer les paramètres</>
)}
</Button>
</div>
</TabsContent>
{/* ===== ONGLET EXPORT ===== */}
<TabsContent value="export" className="space-y-6">
{/* Export des factures BAP */} {/* Export des factures BAP */}
<Card className="border-2 hover:border-primary/50 transition-colors"> <Card className="border-2 hover:border-primary/50 transition-colors">
<CardHeader className="bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-950/20 dark:to-amber-950/20 border-b"> <CardHeader className="bg-gradient-to-r from-orange-50 to-amber-50 dark:from-orange-950/20 dark:to-amber-950/20 border-b">
@@ -567,7 +588,7 @@ export default function ImportSettings() {
/> />
</div> </div>
{/* Configuration du dossier (visible si bapExportFolder actif) */} {/* Configuration du dossier */}
{bapExportFolder && ( {bapExportFolder && (
<div className="space-y-4 p-4 bg-orange-50/30 dark:bg-orange-950/10 rounded-lg border-2 border-orange-200 dark:border-orange-800"> <div className="space-y-4 p-4 bg-orange-50/30 dark:bg-orange-950/10 rounded-lg border-2 border-orange-200 dark:border-orange-800">
@@ -622,7 +643,6 @@ export default function ImportSettings() {
{exportFolderType === "sharepoint" && "URL SharePoint / chemin"} {exportFolderType === "sharepoint" && "URL SharePoint / chemin"}
<span className="text-red-500 ml-1">*</span> <span className="text-red-500 ml-1">*</span>
</Label> </Label>
<div className="flex gap-2">
<Input <Input
id="exportFolder" id="exportFolder"
type="text" type="text"
@@ -633,9 +653,8 @@ export default function ImportSettings() {
} }
value={exportFolder} value={exportFolder}
onChange={(e) => setExportFolder(e.target.value)} onChange={(e) => setExportFolder(e.target.value)}
className="h-11 flex-1" className="h-11"
/> />
</div>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{exportFolderType === "local" && "Chemin absolu du dossier local où les PDF BAP seront enregistrés (exécuté côté serveur)"} {exportFolderType === "local" && "Chemin absolu du dossier local où les PDF BAP seront enregistrés (exécuté côté serveur)"}
{exportFolderType === "teams" && "Format : teams://NomEquipe/Chemin/Dossier — le PDF sera copié dans le canal Teams correspondant"} {exportFolderType === "teams" && "Format : teams://NomEquipe/Chemin/Dossier — le PDF sera copié dans le canal Teams correspondant"}
@@ -703,28 +722,24 @@ export default function ImportSettings() {
</div> </div>
{/* Alerte expiration */} {/* Alerte expiration */}
{azureSecretExpiresAt && (() => { {expirationAlert && (
const expDate = new Date(azureSecretExpiresAt); expirationAlert.daysLeft <= 0 ? (
const daysLeft = Math.ceil((expDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24));
if (daysLeft <= 0) return (
<div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm"> <div className="flex items-center gap-2 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
<AlertTriangle className="h-4 w-4 shrink-0" /> <AlertTriangle className="h-4 w-4 shrink-0" />
<span><strong>Secret expiré</strong> renouvelez-le immédiatement dans le portail Azure AD</span> <span><strong>Secret expiré</strong> renouvelez-le immédiatement dans le portail Azure AD</span>
</div> </div>
); ) : expirationAlert.daysLeft <= 30 ? (
if (daysLeft <= 30) return (
<div className="flex items-center gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg text-amber-700 text-sm"> <div className="flex items-center gap-2 p-3 bg-amber-50 border border-amber-200 rounded-lg text-amber-700 text-sm">
<AlertTriangle className="h-4 w-4 shrink-0" /> <AlertTriangle className="h-4 w-4 shrink-0" />
<span><strong>Expiration dans {daysLeft} jours</strong> pensez à renouveler le secret Azure AD</span> <span>Secret expire dans <strong>{expirationAlert.daysLeft} jours</strong> (le {expirationAlert.expDate.toLocaleDateString('fr-FR')}) pensez à le renouveler</span>
</div> </div>
); ) : (
return (
<div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm"> <div className="flex items-center gap-2 p-3 bg-green-50 border border-green-200 rounded-lg text-green-700 text-sm">
<CheckCircle2 className="h-4 w-4 shrink-0" /> <CheckCircle2 className="h-4 w-4 shrink-0" />
<span>Secret valide encore <strong>{daysLeft} jours</strong> (expire le {expDate.toLocaleDateString('fr-FR')})</span> <span>Secret valide encore <strong>{expirationAlert.daysLeft} jours</strong> (expire le {expirationAlert.expDate.toLocaleDateString('fr-FR')})</span>
</div> </div>
); )
})()} )}
{/* Bouton test connexion */} {/* Bouton test connexion */}
<div className="pt-2"> <div className="pt-2">
@@ -733,12 +748,16 @@ export default function ImportSettings() {
variant="outline" variant="outline"
size="sm" size="sm"
onClick={async () => { onClick={async () => {
try {
const result = await testAzureConnectionMutation.mutateAsync(); const result = await testAzureConnectionMutation.mutateAsync();
if (result.success) { if (result.success) {
toast.success(result.message || 'Connexion Azure AD réussie'); toast.success(result.message || 'Connexion Azure AD réussie');
} else { } else {
toast.error(result.error || 'Erreur de connexion Azure AD'); toast.error(result.error || 'Erreur de connexion Azure AD');
} }
} catch (e: any) {
toast.error("Erreur : " + (e?.message || "connexion impossible"));
}
}} }}
disabled={testAzureConnectionMutation.isPending || !azureTenantId || !azureClientId || !azureClientSecret} disabled={testAzureConnectionMutation.isPending || !azureTenantId || !azureClientId || !azureClientSecret}
className="gap-2" className="gap-2"
@@ -768,31 +787,26 @@ export default function ImportSettings() {
].filter(Boolean).join(" + ") ].filter(Boolean).join(" + ")
} }
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
{/* Save Button */} {/* Bouton Enregistrer onglet Export */}
<div className="flex justify-end pb-8"> <div className="flex justify-end pb-4">
<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" className="bg-gradient-to-r from-orange-500 to-amber-600 hover:from-orange-600 hover:to-amber-700 text-white shadow-lg"
> >
{updateMutation.isPending ? ( {updateMutation.isPending ? (
<> <><Loader2 className="mr-2 h-5 w-5 animate-spin" />Enregistrement...</>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
Enregistrement...
</>
) : ( ) : (
<> <><Save className="mr-2 h-5 w-5" />Enregistrer les paramètres</>
<Save className="mr-2 h-5 w-5" />
Enregistrer les paramètres
</>
)} )}
</Button> </Button>
</div> </div>
</TabsContent>
</Tabs>
</div> </div>
</DashboardLayout> </DashboardLayout>
); );

View File

@@ -1505,7 +1505,7 @@ export const appRouter = router({
azureTenantId: z.string().nullable().optional(), azureTenantId: z.string().nullable().optional(),
azureClientId: z.string().nullable().optional(), azureClientId: z.string().nullable().optional(),
azureClientSecret: z.string().nullable().optional(), azureClientSecret: z.string().nullable().optional(),
azureSecretExpiresAt: z.date().nullable().optional(), azureSecretExpiresAt: z.union([z.date(), z.string().datetime({ offset: true }).transform(s => new Date(s)), z.string().regex(/^\d{4}-\d{2}-\d{2}$/).transform(s => new Date(s + 'T12:00:00.000Z'))]).nullable().optional(),
})) }))
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
const settings = await upsertImportSettings({ const settings = await upsertImportSettings({