diff --git a/client/src/pages/ImportSettings.tsx b/client/src/pages/ImportSettings.tsx index 2298bac..2c06c1e 100644 --- a/client/src/pages/ImportSettings.tsx +++ b/client/src/pages/ImportSettings.tsx @@ -6,14 +6,26 @@ 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, 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"; export default function ImportSettings() { - const { data: settings, isLoading } = trpc.importSettings.get.useQuery(); - const { data: emailServiceStatus } = trpc.emailImportService.status.useQuery(); - const { data: folderServiceStatus } = trpc.folderImportService.status.useQuery(); + // 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(); @@ -21,6 +33,7 @@ export default function ImportSettings() { const utils = trpc.useUtils(); const startFolderServiceMutation = trpc.folderImportService.start.useMutation(); const stopFolderServiceMutation = trpc.folderImportService.stop.useMutation(); + const testAzureConnectionMutation = trpc.importSettings.testAzureConnection.useMutation(); // Manual import const [manualImportEnabled, setManualImportEnabled] = useState(true); @@ -40,18 +53,15 @@ export default function ImportSettings() { // Export folder const [exportFolder, setExportFolder] = useState(""); - // BAP export mode : les deux peuvent être actifs simultanément const [bapExportBrowser, setBapExportBrowser] = useState(true); const [bapExportFolder, setBapExportFolder] = useState(false); const [exportFolderType, setExportFolderType] = useState<"local" | "teams" | "sharepoint">("local"); - // Compat. ancienne valeur - const [bapExportMode, setBapExportMode] = useState<"browser" | "folder" | "both">("browser"); // 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 const [azureSecretExpiresAt, setAzureSecretExpiresAt] = useState(""); - const testAzureConnectionMutation = trpc.importSettings.testAzureConnection.useMutation(); // Initialize form with settings from database useEffect(() => { @@ -68,29 +78,35 @@ export default function ImportSettings() { setEmailImportFrequency(settings.emailImportFrequency || 30); setExportFolder(settings.exportFolder || ""); const savedMode = (settings.bapExportMode as string) || "browser"; - // Compat : ancienne valeur unique -> dériver les deux switches setBapExportBrowser(savedMode === "browser" || 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) { setExportFolderType((settings as any).exportFolderType as "local" | "teams" | "sharepoint"); } else { - // Compat. ancienne logique préfixe if ((settings.exportFolder || "").startsWith("teams://")) setExportFolderType("teams"); else if ((settings.exportFolder || "").startsWith("sharepoint://")) setExportFolderType("sharepoint"); else setExportFolderType("local"); } - // Azure AD 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; - 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]); - // Calculer le bapExportMode à sauvegarder depuis les deux switches const computedExportMode = (): "browser" | "folder" | "both" => { if (bapExportBrowser && bapExportFolder) return "both"; if (bapExportFolder) return "folder"; @@ -99,6 +115,15 @@ export default function ImportSettings() { 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, @@ -116,12 +141,13 @@ export default function ImportSettings() { azureTenantId: azureTenantId || null, azureClientId: azureClientId || null, azureClientSecret: azureClientSecret || null, - azureSecretExpiresAt: azureSecretExpiresAt ? new Date(azureSecretExpiresAt) : null, + azureSecretExpiresAt: expiresAtValue, }); toast.success("Paramètres enregistrés avec succès"); - } catch (error) { - toast.error("Impossible d'enregistrer les paramètres"); + } catch (error: any) { + console.error("Erreur enregistrement paramètres:", error); + toast.error("Impossible d'enregistrer les paramètres : " + (error?.message || "erreur inconnue")); } }; @@ -136,663 +162,651 @@ 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 ( -
+
{/* Header */} -
-
-
- -
-
-

- Paramètres import / export -

-

- Configurez les méthodes d'importation et les options d'export des factures BAP -

-
+
+
+ +
+
+

+ Paramètres import / export +

+

+ Configurez les méthodes d'importation et les options d'export des factures BAP +

- {/* Manual Import */} - - -
-
-
- -
-
- Import manuel - - Autoriser l'upload manuel de fichiers PDF via l'interface - -
-
- - {manualImportEnabled ? "Activé" : "Désactivé"} - -
-
- -
- - -
-
-
+ {/* Onglets Import / Export */} + + + + + Paramètres d'import + + + + Paramètres d'export + + - {/* Automatic Import from Folder */} - - -
-
-
- -
-
- Import automatique depuis dossier - - Surveiller un dossier et importer automatiquement les nouveaux fichiers PDF - -
-
- - {autoImportEnabled ? "Activé" : "Désactivé"} - -
-
- -
- - -
+ {/* ===== ONGLET IMPORT ===== */} + - {autoImportEnabled && ( -
-
- - setAutoImportSourcePath(e.target.value)} - className="h-11" - /> -

- Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF -

-
- -
- - setAutoImportFrequency(parseInt(e.target.value) || 60)} - className="h-11" - /> -

- Intervalle de temps entre chaque vérification du dossier (minimum: 1 minute) -

-
- - {/* Service Control Buttons */} -
- {folderServiceStatus?.isRunning ? ( - <> - - - - Service actif - - - ) : ( - - )} -
-
- )} -
-
- - {/* Email Import */} - - -
-
-
- -
-
- Import par email - - Récupérer automatiquement les factures reçues par email - -
-
- - {emailImportEnabled ? "Activé" : "Désactivé"} - -
-
- -
- - -
- - {emailImportEnabled && ( -
-
- - setEmailImportAddress(e.target.value)} - className="h-11" - /> -

- Adresse email à surveiller pour les factures -

-
- -
- - setEmailImportPassword(e.target.value)} - className="h-11" - /> -

- Mot de passe du compte email (stocké de manière sécurisée) -

-
- -
-
- - setEmailImportHost(e.target.value)} - className="h-11" - /> -

- Adresse du serveur IMAP -

-
- -
- - setEmailImportPort(parseInt(e.target.value) || 993)} - className="h-11" - /> -

- Port SSL (993 par défaut) -

-
-
- -
- - setEmailImportFrequency(parseInt(e.target.value) || 30)} - className="h-11" - /> -

- Intervalle de temps entre chaque vérification des emails (minimum: 1 minute) -

-
- - {/* Service Control Buttons */} -
- {emailServiceStatus?.isRunning ? ( - <> - - - - - Service actif - - - ) : ( - - )} -
-
- )} -
-
- - {/* Export des factures BAP */} - - -
-
- -
-
- Export des factures BAP - - Configurez les modes d'export pour les factures validées BAP (les deux modes peuvent être actifs simultanément) - -
-
-
- - - {/* Switch : Ouvrir dans le navigateur */} -
-
-
- -
-
- -

Le PDF s'ouvre directement dans un nouvel onglet

-
-
- -
- - {/* Switch : Enregistrer dans un dossier */} -
-
-
- -
-
- -

Le PDF est copié dans le dossier configuré ci-dessous

-
-
- -
- - {/* Configuration du dossier (visible si bapExportFolder actif) */} - {bapExportFolder && ( -
- - {/* Type de dossier */} -
- -
- - - -
-
- - {/* Chemin du dossier */} -
- -
- setExportFolder(e.target.value)} - className="h-11 flex-1" - /> -
-

- {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"} -

-
- - {/* Champs Azure AD — visibles uniquement pour SharePoint */} - {exportFolderType === "sharepoint" && ( -
-
-
- Configuration Microsoft Azure AD + {/* Import manuel */} + + +
+
+
+
-
-
- - setAzureTenantId(e.target.value)} - className="h-10 font-mono text-sm" - /> -
-
- - setAzureClientId(e.target.value)} - className="h-10 font-mono text-sm" - /> -
-
- - setAzureClientSecret(e.target.value)} - className="h-10 font-mono text-sm" - /> -

Stocké de façon sécurisée. Permissions requises : Files.ReadWrite.All et Sites.ReadWrite.All

-
+
+ Import manuel + + Autoriser l'upload manuel de fichiers PDF via l'interface + +
+
+ + {manualImportEnabled ? "Activé" : "Désactivé"} + +
+ + +
+ + +
+
+ - {/* Date d'expiration du secret */} -
- - setAzureSecretExpiresAt(e.target.value)} - className="h-10" - /> -

Renseignez la date d'expiration pour recevoir une alerte avant renouvellement

-
+ {/* Import automatique depuis dossier */} + + +
+
+
+ +
+
+ Import automatique depuis dossier + + Surveiller un dossier et importer automatiquement les nouveaux fichiers PDF + +
+
+ + {autoImportEnabled ? "Activé" : "Désactivé"} + +
+
+ +
+ + +
- {/* Alerte expiration */} - {azureSecretExpiresAt && (() => { - const expDate = new Date(azureSecretExpiresAt); - const daysLeft = Math.ceil((expDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)); - if (daysLeft <= 0) return ( -
- - Secret expiré — renouvelez-le immédiatement dans le portail Azure AD -
- ); - if (daysLeft <= 30) return ( -
- - Expiration dans {daysLeft} jours — pensez à renouveler le secret Azure AD -
- ); - return ( -
- - Secret valide encore {daysLeft} jours (expire le {expDate.toLocaleDateString('fr-FR')}) -
- ); - })()} - - {/* Bouton test connexion */} -
+ {autoImportEnabled && ( +
+
+ + setAutoImportSourcePath(e.target.value)} + className="h-11" + /> +

+ Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF +

+
+
+ + setAutoImportFrequency(parseInt(e.target.value) || 60)} + className="h-11" + /> +
+
+ {folderServiceStatus?.isRunning ? ( + <> + + + + Service actif + + + ) : ( -

Vérifie le token OAuth2 et l'accès au site SharePoint

-
+ )}
)} -
- )} + +
- {/* Résumé de la configuration */} -
- Configuration active : - {!bapExportBrowser && !bapExportFolder - ? Aucun mode sélectionné — le téléchargement direct restera disponible - : [ - bapExportBrowser && "Ouverture navigateur", - bapExportFolder && `Enregistrement ${exportFolderType === "local" ? "dossier local" : exportFolderType === "teams" ? "Teams" : "SharePoint"}`, - ].filter(Boolean).join(" + ") - } + {/* Import par email */} + + +
+
+
+ +
+
+ Import par email + + Récupérer automatiquement les factures reçues par email + +
+
+ + {emailImportEnabled ? "Activé" : "Désactivé"} + +
+
+ +
+ + +
+ + {emailImportEnabled && ( +
+
+ + setEmailImportAddress(e.target.value)} + className="h-11" + /> +
+
+ + setEmailImportPassword(e.target.value)} + className="h-11" + /> +
+
+
+ + setEmailImportHost(e.target.value)} + className="h-11" + /> +
+
+ + setEmailImportPort(parseInt(e.target.value) || 993)} + className="h-11" + /> +
+
+
+ + setEmailImportFrequency(parseInt(e.target.value) || 30)} + className="h-11" + /> +
+
+ {emailServiceStatus?.isRunning ? ( + <> + + + + + Service actif + + + ) : ( + + )} +
+
+ )} +
+
+ + {/* Bouton Enregistrer onglet Import */} +
+
+ - - + {/* ===== ONGLET EXPORT ===== */} + - {/* Save Button */} -
- -
+ {/* Export des factures BAP */} + + +
+
+ +
+
+ Export des factures BAP + + Configurez les modes d'export pour les factures validées BAP (les deux modes peuvent être actifs simultanément) + +
+
+
+ + + {/* Switch : Ouvrir dans le navigateur */} +
+
+
+ +
+
+ +

Le PDF s'ouvre directement dans un nouvel onglet

+
+
+ +
+ + {/* Switch : Enregistrer dans un dossier */} +
+
+
+ +
+
+ +

Le PDF est copié dans le dossier configuré ci-dessous

+
+
+ +
+ + {/* Configuration du dossier */} + {bapExportFolder && ( +
+ + {/* Type de dossier */} +
+ +
+ + + +
+
+ + {/* Chemin du dossier */} +
+ + setExportFolder(e.target.value)} + className="h-11" + /> +

+ {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"} +

+
+ + {/* Champs Azure AD — visibles uniquement pour SharePoint */} + {exportFolderType === "sharepoint" && ( +
+
+
+ Configuration Microsoft Azure AD +
+
+
+ + setAzureTenantId(e.target.value)} + className="h-10 font-mono text-sm" + /> +
+
+ + setAzureClientId(e.target.value)} + className="h-10 font-mono text-sm" + /> +
+
+ + setAzureClientSecret(e.target.value)} + className="h-10 font-mono text-sm" + /> +

Stocké de façon sécurisée. Permissions requises : Files.ReadWrite.All et Sites.ReadWrite.All

+
+ + {/* Date d'expiration du secret */} +
+ + setAzureSecretExpiresAt(e.target.value)} + className="h-10" + /> +

Renseignez la date d'expiration pour recevoir une alerte avant renouvellement

+
+ + {/* Alerte expiration */} + {expirationAlert && ( + expirationAlert.daysLeft <= 0 ? ( +
+ + Secret expiré — renouvelez-le immédiatement dans le portail Azure AD +
+ ) : expirationAlert.daysLeft <= 30 ? ( +
+ + Secret expire dans {expirationAlert.daysLeft} jours (le {expirationAlert.expDate.toLocaleDateString('fr-FR')}) — pensez à le renouveler +
+ ) : ( +
+ + Secret valide encore {expirationAlert.daysLeft} jours (expire le {expirationAlert.expDate.toLocaleDateString('fr-FR')}) +
+ ) + )} + + {/* Bouton test connexion */} +
+ +

Vérifie le token OAuth2 et l'accès au site SharePoint

+
+
+
+ )} +
+ )} + + {/* Résumé de la configuration */} +
+ Configuration active : + {!bapExportBrowser && !bapExportFolder + ? Aucun mode sélectionné — le téléchargement direct restera disponible + : [ + bapExportBrowser && "Ouverture navigateur", + bapExportFolder && `Enregistrement ${exportFolderType === "local" ? "dossier local" : exportFolderType === "teams" ? "Teams" : "SharePoint"}`, + ].filter(Boolean).join(" + ") + } +
+ + + + {/* Bouton Enregistrer onglet Export */} +
+ +
+ +
); diff --git a/server/routers.ts b/server/routers.ts index 3ec35bd..a528223 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -1505,7 +1505,7 @@ export const appRouter = router({ azureTenantId: z.string().nullable().optional(), azureClientId: 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 }) => { const settings = await upsertImportSettings({