-
-
Configuration Microsoft Azure AD
+ {/* Import manuel */}
+
+
+
+
+
+
-
-
- ID de l'annuaire (Tenant ID) *
- setAzureTenantId(e.target.value)}
- className="h-10 font-mono text-sm"
- />
-
-
- ID d'application (Client ID) *
- setAzureClientId(e.target.value)}
- className="h-10 font-mono text-sm"
- />
-
-
-
Secret client *
-
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é"}
+
+
+
+
+
+
+ Activer l'import manuel
+
+
+
+
+
- {/* Date d'expiration du secret */}
-
-
-
- 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é"}
+
+
+
+
+
+
+ Activer l'import automatique
+
+
+
- {/* 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 && (
+
+
+
Chemin du dossier source
+
setAutoImportSourcePath(e.target.value)}
+ className="h-11"
+ />
+
+ Chemin absolu du dossier à surveiller pour les nouveaux fichiers PDF
+
+
+
+ Fréquence de lecture (minutes)
+ setAutoImportFrequency(parseInt(e.target.value) || 60)}
+ className="h-11"
+ />
+
+
+ {folderServiceStatus?.isRunning ? (
+ <>
+
{
+ 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 ? : }
+ Arrêter le service
+
+
+
+ Service actif
+
+ >
+ ) : (
{
- 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');
+ 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={testAzureConnectionMutation.isPending || !azureTenantId || !azureClientId || !azureClientSecret}
- className="gap-2"
+ disabled={!autoImportEnabled || startFolderServiceMutation.isPending}
+ className="bg-blue-500 hover:bg-blue-600"
>
- {testAzureConnectionMutation.isPending ? (
- <> Test en cours...>
- ) : (
- <> Tester la connexion Azure AD>
- )}
+ {startFolderServiceMutation.isPending ? : }
+ Démarrer le service
-
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é"}
+
+
+
+
+
+
+ Activer l'import par email
+
+
+
+
+ {emailImportEnabled && (
+
+
+ Adresse email
+ setEmailImportAddress(e.target.value)}
+ className="h-11"
+ />
+
+
+ Mot de passe
+ setEmailImportPassword(e.target.value)}
+ className="h-11"
+ />
+
+
+
+ Fréquence de lecture (minutes)
+ setEmailImportFrequency(parseInt(e.target.value) || 30)}
+ className="h-11"
+ />
+
+
+ {emailServiceStatus?.isRunning ? (
+ <>
+
{
+ 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 ? : }
+ Arrêter le service
+
+
{
+ 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 ? : }
+ Vérifier maintenant
+
+
+
+ Service actif
+
+ >
+ ) : (
+
{
+ 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 ? : }
+ Démarrer le service
+
+ )}
+
+
+ )}
+
+
+
+ {/* Bouton Enregistrer onglet Import */}
+
+
+ {updateMutation.isPending ? (
+ <> Enregistrement...>
+ ) : (
+ <> Enregistrer les paramètres>
+ )}
+
+
-
-
+ {/* ===== ONGLET EXPORT ===== */}
+
- {/* Save Button */}
-
-
- {updateMutation.isPending ? (
- <>
-
- Enregistrement...
- >
- ) : (
- <>
-
- Enregistrer les paramètres
- >
- )}
-
-
+ {/* 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 */}
+
+
+
+
+
+
+
Ouvrir dans le navigateur
+
Le PDF s'ouvre directement dans un nouvel onglet
+
+
+
+
+
+ {/* Switch : Enregistrer dans un dossier */}
+
+
+
+
+
+
+
Enregistrer dans un dossier
+
Le PDF est copié dans le dossier configuré ci-dessous
+
+
+
+
+
+ {/* Configuration du dossier */}
+ {bapExportFolder && (
+
+
+ {/* Type de dossier */}
+
+
Type de destination
+
+ 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"
+ }`}
+ >
+
+ Dossier local
+
+ 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"
+ }`}
+ >
+
+ Teams
+
+ 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"
+ }`}
+ >
+
+ SharePoint
+
+
+
+
+ {/* Chemin du dossier */}
+
+
+ {exportFolderType === "local" && "Chemin du dossier local"}
+ {exportFolderType === "teams" && "Chemin du dossier Teams"}
+ {exportFolderType === "sharepoint" && "URL SharePoint / chemin"}
+ *
+
+
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
+
+
+
+ ID de l'annuaire (Tenant ID) *
+ setAzureTenantId(e.target.value)}
+ className="h-10 font-mono text-sm"
+ />
+
+
+ ID d'application (Client ID) *
+ setAzureClientId(e.target.value)}
+ className="h-10 font-mono text-sm"
+ />
+
+
+
Secret client *
+
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 */}
+
+
+
+ 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 */}
+
+
{
+ 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 ? (
+ <> Test en cours...>
+ ) : (
+ <> Tester la connexion Azure AD>
+ )}
+
+
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 */}
+
+
+ {updateMutation.isPending ? (
+ <> Enregistrement...>
+ ) : (
+ <> Enregistrer les paramètres>
+ )}
+
+
+
+
);
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({