Files
demat-facturation/client/src/pages/ImportSettings.tsx

963 lines
51 KiB
TypeScript

import { useState, useEffect } from "react";
import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Badge } from "@/components/ui/badge";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "sonner";
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";
export default function ImportSettings() {
// Chargement différé : on ne charge les statuts de services qu'après le montage
const { data: settings, isLoading } = trpc.importSettings.get.useQuery(undefined, {
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 startEmailServiceMutation = trpc.emailImportService.start.useMutation();
const stopEmailServiceMutation = trpc.emailImportService.stop.useMutation();
const checkNowEmailMutation = trpc.emailImportService.checkNow.useMutation();
const utils = trpc.useUtils();
const startFolderServiceMutation = trpc.folderImportService.start.useMutation();
const stopFolderServiceMutation = trpc.folderImportService.stop.useMutation();
const testAzureConnectionMutation = trpc.importSettings.testAzureConnection.useMutation();
const testSharePointUploadMutation = trpc.importSettings.testSharePointUpload.useMutation();
const testEmailConnectionMutation = trpc.importSettings.testEmailConnection.useMutation();
// Manual import
const [manualImportEnabled, setManualImportEnabled] = useState(true);
// Automatic import from folder
const [autoImportEnabled, setAutoImportEnabled] = useState(false);
const [autoImportSourcePath, setAutoImportSourcePath] = useState("");
const [autoImportFrequency, setAutoImportFrequency] = useState(60);
// Email import
const [emailImportEnabled, setEmailImportEnabled] = useState(false);
const [emailImportAddress, setEmailImportAddress] = useState("");
const [emailImportPassword, setEmailImportPassword] = useState("");
const [emailImportAuthMode, setEmailImportAuthMode] = useState<"basic" | "oauth2">("basic");
const [emailImportHost, setEmailImportHost] = useState("");
const [emailImportPort, setEmailImportPort] = useState(993);
const [emailImportFrequency, setEmailImportFrequency] = useState(30);
// Date de début de lecture des emails (string ISO "YYYY-MM-DD" pour le champ <input type="date">)
const [emailImportSinceDate, setEmailImportSinceDate] = useState("");
// Export folder
const [exportFolder, setExportFolder] = useState("");
const [bapExportBrowser, setBapExportBrowser] = useState(true);
const [bapExportFolder, setBapExportFolder] = useState(false);
const [exportFolderType, setExportFolderType] = useState<"local" | "teams" | "sharepoint">("local");
// Azure AD credentials for SharePoint
const [azureTenantId, setAzureTenantId] = useState("");
const [azureClientId, setAzureClientId] = useState("");
const [azureClientSecret, setAzureClientSecret] = useState("");
// Date stockée en string ISO "YYYY-MM-DD" pour le champ <input type="date">
const [azureSecretExpiresAt, setAzureSecretExpiresAt] = useState("");
// Initialize form with settings from database
useEffect(() => {
if (settings) {
setManualImportEnabled(settings.manualImportEnabled === 1);
setAutoImportEnabled(settings.autoImportEnabled === 1);
setAutoImportSourcePath(settings.autoImportSourcePath || "");
setAutoImportFrequency(settings.autoImportFrequency || 60);
setEmailImportEnabled(settings.emailImportEnabled === 1);
setEmailImportAddress(settings.emailImportAddress || "");
setEmailImportPassword(settings.emailImportPassword || "");
setEmailImportAuthMode(((settings as any).emailImportAuthMode as "basic" | "oauth2") || "basic");
setEmailImportHost(settings.emailImportHost || "");
setEmailImportPort(settings.emailImportPort || 993);
setEmailImportFrequency(settings.emailImportFrequency || 30);
// Convertir le timestamp Unix (s) en string ISO pour l'input date
if ((settings as any).emailImportSinceDate) {
const d = new Date((settings as any).emailImportSinceDate * 1000);
setEmailImportSinceDate(d.toISOString().split('T')[0]);
} else {
setEmailImportSinceDate("");
}
setExportFolder(settings.exportFolder || "");
const savedMode = (settings.bapExportMode as string) || "browser";
setBapExportBrowser(savedMode === "browser" || savedMode === "both");
setBapExportFolder(savedMode === "folder" || savedMode === "both");
if ((settings as any).exportFolderType) {
setExportFolderType((settings as any).exportFolderType as "local" | "teams" | "sharepoint");
} else {
if ((settings.exportFolder || "").startsWith("teams://")) setExportFolderType("teams");
else if ((settings.exportFolder || "").startsWith("sharepoint://")) setExportFolderType("sharepoint");
else setExportFolderType("local");
}
setAzureTenantId((settings as any).azureTenantId || "");
setAzureClientId((settings as any).azureClientId || "");
setAzureClientSecret((settings as any).azureClientSecret || "");
// Convertir la date DB en string "YYYY-MM-DD"
const expDate = (settings as any).azureSecretExpiresAt;
if (expDate) {
try {
const d = new Date(expDate);
if (!isNaN(d.getTime())) {
setAzureSecretExpiresAt(d.toISOString().split('T')[0]);
}
} catch {
setAzureSecretExpiresAt("");
}
} else {
setAzureSecretExpiresAt("");
}
}
}, [settings]);
const computedExportMode = (): "browser" | "folder" | "both" => {
if (bapExportBrowser && bapExportFolder) return "both";
if (bapExportFolder) return "folder";
return "browser";
};
const handleSave = async () => {
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({
manualImportEnabled: manualImportEnabled ? 1 : 0,
autoImportEnabled: autoImportEnabled ? 1 : 0,
autoImportSourcePath: autoImportSourcePath || null,
autoImportFrequency: autoImportFrequency,
emailImportEnabled: emailImportEnabled ? 1 : 0,
emailImportAddress: emailImportAddress || null,
emailImportPassword: emailImportPassword || null,
emailImportHost: emailImportHost || null,
emailImportPort: emailImportPort,
emailImportFrequency: emailImportFrequency,
emailImportSinceDate: emailImportSinceDate
? Math.floor(new Date(emailImportSinceDate + 'T00:00:00Z').getTime() / 1000)
: null,
emailImportAuthMode: emailImportAuthMode,
exportFolder: exportFolder || null,
exportFolderType: exportFolderType,
bapExportMode: computedExportMode() as "browser" | "folder" | "both",
azureTenantId: azureTenantId || null,
azureClientId: azureClientId || null,
azureClientSecret: azureClientSecret || null,
azureSecretExpiresAt: expiresAtValue,
});
toast.success("Paramètres enregistrés avec succès");
} catch (error: any) {
console.error("Erreur enregistrement paramètres:", error);
toast.error("Impossible d'enregistrer les paramètres : " + (error?.message || "erreur inconnue"));
}
};
if (isLoading) {
return (
<DashboardLayout>
<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>
);
}
// 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 (
<DashboardLayout>
<div className="max-w-5xl space-y-6">
{/* Header */}
<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-3xl font-bold bg-gradient-to-r from-blue-600 to-cyan-600 bg-clip-text text-transparent">
Paramètres import / export
</h1>
<p className="text-muted-foreground mt-0.5 text-sm">
Configurez les méthodes d'importation et les options d'export des factures BAP
</p>
</div>
</div>
{/* 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">
<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="p-2 bg-purple-500 rounded-lg">
<Upload className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Import manuel</CardTitle>
<CardDescription className="mt-1">
Autoriser l'upload manuel de fichiers PDF via l'interface
</CardDescription>
</div>
</div>
<Badge variant={manualImportEnabled ? "default" : "secondary"}>
{manualImportEnabled ? "Activé" : "Désactivé"}
</Badge>
</div>
</CardHeader>
<CardContent className="pt-6">
<div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<Label htmlFor="manual-import" className="text-base font-medium">
Activer l'import manuel
</Label>
<Switch
id="manual-import"
checked={manualImportEnabled}
onCheckedChange={setManualImportEnabled}
/>
</div>
</CardContent>
</Card>
{/* Import automatique depuis dossier */}
<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 justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-blue-500 rounded-lg">
<FolderOpen className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Import automatique depuis dossier</CardTitle>
<CardDescription className="mt-1">
Surveiller un dossier et importer automatiquement les nouveaux fichiers PDF
</CardDescription>
</div>
</div>
<Badge variant={autoImportEnabled ? "default" : "secondary"}>
{autoImportEnabled ? "Activé" : "Désactivé"}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-6 pt-6">
<div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<Label htmlFor="auto-import" className="text-base font-medium">
Activer l'import automatique
</Label>
<Switch
id="auto-import"
checked={autoImportEnabled}
onCheckedChange={setAutoImportEnabled}
/>
</div>
{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">
<Label htmlFor="source-path" className="text-base font-medium">Chemin du dossier source</Label>
<Input
id="source-path"
type="text"
placeholder="/chemin/vers/dossier/factures"
value={autoImportSourcePath}
onChange={(e) => setAutoImportSourcePath(e.target.value)}
className="h-11"
/>
<p className="text-sm text-muted-foreground">
Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF
</p>
</div>
<div className="space-y-2">
<Label htmlFor="auto-frequency" className="text-base font-medium">Fréquence de lecture (minutes)</Label>
<Input
id="auto-frequency"
type="number"
min="1"
value={autoImportFrequency}
onChange={(e) => setAutoImportFrequency(parseInt(e.target.value) || 60)}
className="h-11"
/>
</div>
<div className="flex gap-3 pt-4 border-t border-blue-200 dark:border-blue-800">
{folderServiceStatus?.isRunning ? (
<>
<Button
variant="destructive"
onClick={async () => {
try {
await stopFolderServiceMutation.mutateAsync();
await utils.folderImportService.status.invalidate();
toast.success("Service arrêté");
} catch {
toast.error("Impossible d'arrêter le service");
}
}}
disabled={stopFolderServiceMutation.isPending}
>
{stopFolderServiceMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <Square className="mr-2 h-5 w-5" />}
Arrêter le service
</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
onClick={async () => {
try {
const result = await startFolderServiceMutation.mutateAsync();
if (result.success) {
await utils.folderImportService.status.invalidate();
toast.success("Service démarré");
} else {
toast.error("Impossible de démarrer le service");
}
} catch {
toast.error("Impossible de démarrer le service");
}
}}
disabled={!autoImportEnabled || startFolderServiceMutation.isPending}
className="bg-blue-500 hover:bg-blue-600"
>
{startFolderServiceMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <Play className="mr-2 h-5 w-5" />}
Démarrer le service
</Button>
)}
</div>
</div>
)}
</CardContent>
</Card>
{/* Import par email */}
<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 justify-between">
<div className="flex items-center gap-3">
<div className="p-2 bg-green-500 rounded-lg">
<Mail className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Import par email</CardTitle>
<CardDescription className="mt-1">
Récupérer automatiquement les factures reçues par email
</CardDescription>
</div>
</div>
<Badge variant={emailImportEnabled ? "default" : "secondary"}>
{emailImportEnabled ? "Activé" : "Désactivé"}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-6 pt-6">
<div className="flex items-center justify-between p-4 bg-muted/30 rounded-lg">
<Label htmlFor="email-import" className="text-base font-medium">
Activer l'import par email
</Label>
<Switch
id="email-import"
checked={emailImportEnabled}
onCheckedChange={setEmailImportEnabled}
/>
</div>
{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">
{/* Mode d'authentification */}
<div className="space-y-2">
<Label className="text-base font-medium">Mode d'authentification IMAP</Label>
<div className="flex gap-3">
<button
type="button"
onClick={() => setEmailImportAuthMode("basic")}
className={`flex-1 py-2 px-4 rounded-lg border-2 text-sm font-medium transition-colors ${
emailImportAuthMode === "basic"
? "border-blue-500 bg-blue-50 text-blue-700 dark:bg-blue-950/30 dark:text-blue-300"
: "border-muted bg-muted/30 text-muted-foreground hover:border-blue-300"
}`}
>
Basique (login/mot de passe)
</button>
<button
type="button"
onClick={() => setEmailImportAuthMode("oauth2")}
className={`flex-1 py-2 px-4 rounded-lg border-2 text-sm font-medium transition-colors ${
emailImportAuthMode === "oauth2"
? "border-purple-500 bg-purple-50 text-purple-700 dark:bg-purple-950/30 dark:text-purple-300"
: "border-muted bg-muted/30 text-muted-foreground hover:border-purple-300"
}`}
>
OAuth2 (Office 365 / Azure AD)
</button>
</div>
{emailImportAuthMode === "oauth2" && (
<div className="p-3 bg-purple-50 dark:bg-purple-950/20 rounded-lg border border-purple-200 dark:border-purple-800 text-sm text-purple-700 dark:text-purple-300">
<strong>Office 365 :</strong> Microsoft a désactivé l'auth basique pour Exchange Online. Utilisez ce mode avec les credentials Azure AD déjà configurés dans l'onglet Export.
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="email-address" className="text-base font-medium">Adresse email</Label>
<Input
id="email-address"
type="email"
placeholder="factures@votreentreprise.com"
value={emailImportAddress}
onChange={(e) => setEmailImportAddress(e.target.value)}
className="h-11"
/>
</div>
{emailImportAuthMode === "basic" && (
<div className="space-y-2">
<Label htmlFor="email-password" className="text-base font-medium">Mot de passe</Label>
<Input
id="email-password"
type="password"
placeholder="••••••••"
value={emailImportPassword}
onChange={(e) => setEmailImportPassword(e.target.value)}
className="h-11"
/>
</div>
)}
{emailImportAuthMode === "oauth2" && (
<div className="p-3 bg-muted/30 rounded-lg border text-sm text-muted-foreground">
<Wifi className="inline h-4 w-4 mr-1" />
En mode OAuth2, les credentials Azure AD (Tenant ID, Client ID, Client Secret) configurés dans l'onglet <strong>Export &gt; SharePoint</strong> seront utilisés automatiquement.
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email-host" className="text-base font-medium">Serveur IMAP</Label>
<Input
id="email-host"
type="text"
placeholder="imap.gmail.com"
value={emailImportHost}
onChange={(e) => setEmailImportHost(e.target.value)}
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email-port" className="text-base font-medium">Port IMAP</Label>
<Input
id="email-port"
type="number"
min="1"
max="65535"
value={emailImportPort}
onChange={(e) => setEmailImportPort(parseInt(e.target.value) || 993)}
className="h-11"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="email-frequency" className="text-base font-medium">Fréquence de lecture (minutes)</Label>
<Input
id="email-frequency"
type="number"
min="1"
value={emailImportFrequency}
onChange={(e) => setEmailImportFrequency(parseInt(e.target.value) || 30)}
className="h-11"
/>
</div>
<div className="space-y-2">
<Label htmlFor="email-since-date" className="text-base font-medium">Ne pas lire les emails antérieurs au</Label>
<p className="text-sm text-muted-foreground">Les emails reçus avant cette date ne seront pas importés, même s'ils sont non lus. Laissez vide pour lire tous les emails non lus.</p>
<Input
id="email-since-date"
type="date"
value={emailImportSinceDate}
onChange={(e) => setEmailImportSinceDate(e.target.value)}
className="h-11"
/>
{emailImportSinceDate && (
<Button
variant="ghost"
size="sm"
className="text-muted-foreground"
onClick={() => setEmailImportSinceDate("")}
>
Effacer la date limite
</Button>
)}
</div>
{/* Bouton test connexion IMAP */}
<div className="pt-2">
<Button
variant="outline"
onClick={async () => {
try {
toast.info("Test de connexion IMAP en cours...");
const result = await testEmailConnectionMutation.mutateAsync();
if (result.success) {
toast.success(result.message);
} else {
toast.error(result.message, { duration: 8000 });
}
} catch (e: any) {
toast.error("Erreur : " + (e?.message || "inconnue"));
}
}}
disabled={testEmailConnectionMutation.isPending}
className="border-blue-300 hover:bg-blue-50 dark:hover:bg-blue-950/20"
>
{testEmailConnectionMutation.isPending
? <><Loader2 className="mr-2 h-4 w-4 animate-spin" />Test en cours...</>
: <><Wifi className="mr-2 h-4 w-4" />Tester la connexion IMAP</>
}
</Button>
<p className="text-xs text-muted-foreground mt-1">Vérifie que la connexion au serveur IMAP est fonctionnelle (sans importer d'emails)</p>
</div>
<div className="flex flex-wrap gap-3 pt-4 border-t border-green-200 dark:border-green-800">
{emailServiceStatus?.isRunning ? (
<>
<Button
variant="destructive"
onClick={async () => {
try {
await stopEmailServiceMutation.mutateAsync();
await utils.emailImportService.status.invalidate();
toast.success("Service arrêté");
} catch {
toast.error("Impossible d'arrêter le service");
}
}}
disabled={stopEmailServiceMutation.isPending}
>
{stopEmailServiceMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <Square className="mr-2 h-5 w-5" />}
Arrêter le service
</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 {
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
onClick={async () => {
try {
const result = await startEmailServiceMutation.mutateAsync();
if (result.success) {
await utils.emailImportService.status.invalidate();
toast.success("Service démarré");
} else {
toast.error("Impossible de démarrer le service");
}
} catch {
toast.error("Impossible de démarrer le service");
}
}}
disabled={!emailImportEnabled || startEmailServiceMutation.isPending}
className="bg-green-500 hover:bg-green-600"
>
{startEmailServiceMutation.isPending ? <Loader2 className="mr-2 h-5 w-5 animate-spin" /> : <Play className="mr-2 h-5 w-5" />}
Démarrer le service
</Button>
)}
</div>
</div>
)}
</CardContent>
</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 */}
<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">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-500 rounded-lg">
<FolderOutput className="w-6 h-6 text-white" />
</div>
<div>
<CardTitle className="text-xl">Export des factures BAP</CardTitle>
<CardDescription className="mt-1">
Configurez les modes d'export pour les factures validées BAP (les deux modes peuvent être actifs simultanément)
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6 pt-6">
{/* Switch : Ouvrir dans le navigateur */}
<div className="flex items-center justify-between p-4 bg-blue-50/50 dark:bg-blue-950/10 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">
<Monitor className="w-5 h-5 text-white" />
</div>
<div>
<Label htmlFor="bap-browser" className="text-base font-medium cursor-pointer">Ouvrir dans le navigateur</Label>
<p className="text-xs text-muted-foreground mt-0.5">Le PDF s'ouvre directement dans un nouvel onglet</p>
</div>
</div>
<Switch
id="bap-browser"
checked={bapExportBrowser}
onCheckedChange={setBapExportBrowser}
/>
</div>
{/* Switch : Enregistrer dans un dossier */}
<div className="flex items-center justify-between p-4 bg-orange-50/50 dark:bg-orange-950/10 rounded-lg border border-orange-200 dark:border-orange-800">
<div className="flex items-center gap-3">
<div className="p-2 bg-orange-500 rounded-lg">
<FolderOutput className="w-5 h-5 text-white" />
</div>
<div>
<Label htmlFor="bap-folder" className="text-base font-medium cursor-pointer">Enregistrer dans un dossier</Label>
<p className="text-xs text-muted-foreground mt-0.5">Le PDF est copié dans le dossier configuré ci-dessous</p>
</div>
</div>
<Switch
id="bap-folder"
checked={bapExportFolder}
onCheckedChange={setBapExportFolder}
/>
</div>
{/* Configuration du dossier */}
{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">
{/* Type de dossier */}
<div className="space-y-2">
<Label className="text-base font-medium">Type de destination</Label>
<div className="grid grid-cols-3 gap-3">
<button
type="button"
onClick={() => setExportFolderType("local")}
className={`flex flex-col items-center gap-2 p-3 rounded-xl border-2 transition-all ${
exportFolderType === "local"
? "border-orange-500 bg-orange-50 dark:bg-orange-950/30 text-orange-700 dark:text-orange-300"
: "border-muted hover:border-muted-foreground/40 text-muted-foreground"
}`}
>
<FolderOpen className="w-6 h-6" />
<span className="text-xs font-semibold">Dossier local</span>
</button>
<button
type="button"
onClick={() => setExportFolderType("teams")}
className={`flex flex-col items-center gap-2 p-3 rounded-xl border-2 transition-all ${
exportFolderType === "teams"
? "border-purple-500 bg-purple-50 dark:bg-purple-950/30 text-purple-700 dark:text-purple-300"
: "border-muted hover:border-muted-foreground/40 text-muted-foreground"
}`}
>
<Download className="w-6 h-6" />
<span className="text-xs font-semibold">Teams</span>
</button>
<button
type="button"
onClick={() => setExportFolderType("sharepoint")}
className={`flex flex-col items-center gap-2 p-3 rounded-xl border-2 transition-all ${
exportFolderType === "sharepoint"
? "border-cyan-500 bg-cyan-50 dark:bg-cyan-950/30 text-cyan-700 dark:text-cyan-300"
: "border-muted hover:border-muted-foreground/40 text-muted-foreground"
}`}
>
<Download className="w-6 h-6" />
<span className="text-xs font-semibold">SharePoint</span>
</button>
</div>
</div>
{/* Chemin du dossier */}
<div className="space-y-2">
<Label htmlFor="exportFolder" className="text-base font-medium">
{exportFolderType === "local" && "Chemin du dossier local"}
{exportFolderType === "teams" && "Chemin du dossier Teams"}
{exportFolderType === "sharepoint" && "URL SharePoint / chemin"}
<span className="text-red-500 ml-1">*</span>
</Label>
<Input
id="exportFolder"
type="text"
placeholder={
exportFolderType === "local" ? "C:\\Users\\...\\Factures BAP" :
exportFolderType === "teams" ? "teams://NomEquipe/Documents/Factures BAP" :
"sharepoint://https://entreprise.sharepoint.com/sites/..."
}
value={exportFolder}
onChange={(e) => setExportFolder(e.target.value)}
className="h-11"
/>
<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 === "teams" && "Format : teams://NomEquipe/Chemin/Dossier — le PDF sera copié dans le canal Teams correspondant"}
{exportFolderType === "sharepoint" && "URL complète du dossier SharePoint de destination"}
</p>
</div>
{/* Champs Azure AD — visibles uniquement pour SharePoint */}
{exportFolderType === "sharepoint" && (
<div className="mt-4 p-4 bg-cyan-50 dark:bg-cyan-950/20 rounded-xl border border-cyan-200 dark:border-cyan-800 space-y-4">
<div className="flex items-center gap-2 mb-2">
<div className="w-2 h-2 rounded-full bg-cyan-500" />
<span className="text-sm font-semibold text-cyan-700 dark:text-cyan-300">Configuration Microsoft Azure AD</span>
</div>
<div className="grid grid-cols-1 gap-3">
<div className="space-y-1">
<Label htmlFor="azureTenantId" className="text-sm font-medium">ID de l'annuaire (Tenant ID) <span className="text-red-500">*</span></Label>
<Input
id="azureTenantId"
type="text"
placeholder="487d0a81-de35-44ce-8847-03bb74ec553e"
value={azureTenantId}
onChange={(e) => setAzureTenantId(e.target.value)}
className="h-10 font-mono text-sm"
/>
</div>
<div className="space-y-1">
<Label htmlFor="azureClientId" className="text-sm font-medium">ID d'application (Client ID) <span className="text-red-500">*</span></Label>
<Input
id="azureClientId"
type="text"
placeholder="e6c2f351-16a6-4659-9b04-de3ea90194f0"
value={azureClientId}
onChange={(e) => setAzureClientId(e.target.value)}
className="h-10 font-mono text-sm"
/>
</div>
<div className="space-y-1">
<Label htmlFor="azureClientSecret" className="text-sm font-medium">Secret client <span className="text-red-500">*</span></Label>
<Input
id="azureClientSecret"
type="password"
placeholder="Valeur du secret Azure AD"
value={azureClientSecret}
onChange={(e) => setAzureClientSecret(e.target.value)}
className="h-10 font-mono text-sm"
/>
<p className="text-xs text-muted-foreground">Stocké de façon sécurisée. Permissions requises : <code>Files.ReadWrite.All</code> et <code>Sites.ReadWrite.All</code></p>
</div>
{/* Date d'expiration du secret */}
<div className="space-y-1">
<Label htmlFor="azureSecretExpiresAt" className="text-sm font-medium flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
Date d'expiration du secret
</Label>
<Input
id="azureSecretExpiresAt"
type="date"
value={azureSecretExpiresAt}
onChange={(e) => setAzureSecretExpiresAt(e.target.value)}
className="h-10"
/>
<p className="text-xs text-muted-foreground">Renseignez la date d'expiration pour recevoir une alerte avant renouvellement</p>
</div>
{/* Alerte expiration */}
{expirationAlert && (
expirationAlert.daysLeft <= 0 ? (
<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" />
<span><strong>Secret expiré</strong> renouvelez-le immédiatement dans le portail Azure AD</span>
</div>
) : expirationAlert.daysLeft <= 30 ? (
<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" />
<span>Secret expire dans <strong>{expirationAlert.daysLeft} jours</strong> (le {expirationAlert.expDate.toLocaleDateString('fr-FR')}) pensez à le renouveler</span>
</div>
) : (
<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" />
<span>Secret valide encore <strong>{expirationAlert.daysLeft} jours</strong> (expire le {expirationAlert.expDate.toLocaleDateString('fr-FR')})</span>
</div>
)
)}
{/* Bouton test connexion */}
<div className="pt-2">
<Button
type="button"
variant="outline"
size="sm"
onClick={async () => {
try {
const result = await testAzureConnectionMutation.mutateAsync();
if (result.success) {
toast.success(result.message || 'Connexion Azure AD réussie');
} else {
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}
className="gap-2"
>
{testAzureConnectionMutation.isPending ? (
<><Loader2 className="h-4 w-4 animate-spin" />Test en cours...</>
) : (
<><Wifi className="h-4 w-4" />Tester la connexion Azure AD</>
)}
</Button>
<p className="text-xs text-muted-foreground mt-1">Vérifie le token OAuth2 et l'accès au site SharePoint</p>
</div>
{/* Bouton test upload réel */}
<div className="pt-1">
<Button
type="button"
variant="outline"
size="sm"
onClick={async () => {
try {
const result = await testSharePointUploadMutation.mutateAsync();
if (result.success) {
toast.success(
result.webUrl
? `Fichier test déposé avec succès ! Cliquez pour l'ouvrir`
: `Fichier test déposé avec succès dans SharePoint`,
{
duration: 8000,
action: result.webUrl ? { label: 'Ouvrir', onClick: () => window.open(result.webUrl!, '_blank') } : undefined,
}
);
} else {
toast.error(`Échec upload test : ${result.error}`, { duration: 10000 });
if ((result as any).debugInfo) {
console.error('[SharePoint debug]', (result as any).debugInfo);
}
}
} catch (e: any) {
toast.error("Erreur : " + (e?.message || "impossible de tester l'upload"));
}
}}
disabled={testSharePointUploadMutation.isPending || !azureTenantId || !azureClientId || !azureClientSecret || !exportFolder}
className="gap-2 border-blue-300 text-blue-700 hover:bg-blue-50"
>
{testSharePointUploadMutation.isPending ? (
<><Loader2 className="h-4 w-4 animate-spin" />Upload test en cours...</>
) : (
<><Upload className="h-4 w-4" />Tester l'upload SharePoint</>
)}
</Button>
<p className="text-xs text-muted-foreground mt-1">Dépose un fichier texte de 1 Ko dans le dossier SharePoint pour valider les permissions d'upload</p>
</div>
</div>
</div>
)}
</div>
)}
{/* Résumé de la configuration */}
<div className="p-3 bg-muted/30 rounded-lg border text-sm text-muted-foreground">
<span className="font-medium text-foreground">Configuration active : </span>
{!bapExportBrowser && !bapExportFolder
? <span className="text-amber-600">Aucun mode sélectionné — le téléchargement direct restera disponible</span>
: [
bapExportBrowser && "Ouverture navigateur",
bapExportFolder && `Enregistrement ${exportFolderType === "local" ? "dossier local" : exportFolderType === "teams" ? "Teams" : "SharePoint"}`,
].filter(Boolean).join(" + ")
}
</div>
</CardContent>
</Card>
{/* Bouton Enregistrer onglet Export */}
<div className="flex justify-end pb-4">
<Button
onClick={handleSave}
disabled={updateMutation.isPending}
size="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 ? (
<><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>
</Tabs>
</div>
</DashboardLayout>
);
}