Merge remote-tracking branch 'user_github/main'
# Conflicts: # server/webImportSourcesMigration.test.ts # todo.md
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
import AzureAdExportSettingsCard from "@/components/AzureAdExportSettingsCard";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -8,6 +8,7 @@ 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 { useAuth } from "@/_core/hooks/useAuth";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Loader2, Save, Upload, FolderOpen, Mail, Play, Square, Download,
|
||||
@@ -153,6 +154,8 @@ function BackupSection() {
|
||||
|
||||
|
||||
export default function ImportSettings() {
|
||||
const { user } = useAuth();
|
||||
const isAdmin = user?.role === "admin";
|
||||
// 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,
|
||||
@@ -173,6 +176,18 @@ export default function ImportSettings() {
|
||||
const testAzureConnectionMutation = trpc.importSettings.testAzureConnection.useMutation();
|
||||
const testSharePointUploadMutation = trpc.importSettings.testSharePointUpload.useMutation();
|
||||
const testEmailConnectionMutation = trpc.importSettings.testEmailConnection.useMutation();
|
||||
const { data: emailAccounts, isLoading: emailAccountsLoading } = trpc.emailImportService.getConfiguredAccounts.useQuery(undefined, {
|
||||
enabled: isAdmin,
|
||||
staleTime: 30_000,
|
||||
});
|
||||
const [selectedEmailAccountId, setSelectedEmailAccountId] = useState("");
|
||||
const selectedEmailAccountUserId = Number(selectedEmailAccountId);
|
||||
const { data: selectedEmailAccountStatus } = trpc.emailImportService.accountStatus.useQuery(
|
||||
{ userId: selectedEmailAccountUserId },
|
||||
{ enabled: isAdmin && Number.isInteger(selectedEmailAccountUserId) && selectedEmailAccountUserId > 0 },
|
||||
);
|
||||
const startEmailAccountMutation = trpc.emailImportService.startAccount.useMutation();
|
||||
const stopEmailAccountMutation = trpc.emailImportService.stopAccount.useMutation();
|
||||
|
||||
// Manual import
|
||||
const [manualImportEnabled, setManualImportEnabled] = useState(true);
|
||||
@@ -257,6 +272,22 @@ export default function ImportSettings() {
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
const configuredEmailAccounts = useMemo(
|
||||
() => (emailAccounts ?? []).filter((account) => account.isConfigured === 1),
|
||||
[emailAccounts],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAdmin || configuredEmailAccounts.length === 0) {
|
||||
setSelectedEmailAccountId("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configuredEmailAccounts.some((account) => String(account.userId) === selectedEmailAccountId)) {
|
||||
setSelectedEmailAccountId(String(configuredEmailAccounts[0].userId));
|
||||
}
|
||||
}, [isAdmin, configuredEmailAccounts, selectedEmailAccountId]);
|
||||
|
||||
const computedExportMode = (): "browser" | "folder" | "both" => {
|
||||
if (bapExportBrowser && bapExportFolder) return "both";
|
||||
if (bapExportFolder) return "folder";
|
||||
@@ -537,8 +568,9 @@ export default function ImportSettings() {
|
||||
/>
|
||||
</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">
|
||||
<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">
|
||||
{emailImportEnabled && (
|
||||
<>
|
||||
{/* Mode d'authentification */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-base font-medium">Mode d'authentification IMAP</Label>
|
||||
@@ -568,7 +600,7 @@ export default function ImportSettings() {
|
||||
</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.
|
||||
<strong>Office 365 :</strong> Microsoft a désactivé l'auth basique pour Exchange Online. Utilisez ce mode avec les paramètres Azure AD configurés dans la carte Microsoft Azure AD au début de cette page.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -600,7 +632,7 @@ export default function ImportSettings() {
|
||||
{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 > SharePoint</strong> seront utilisés automatiquement.
|
||||
En mode OAuth2, les paramètres Azure AD (Tenant ID, Client ID, Client Secret) configurés dans la <strong>carte Microsoft Azure AD</strong> seront utilisés automatiquement.
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
@@ -741,12 +773,12 @@ export default function ImportSettings() {
|
||||
const result = await startEmailServiceMutation.mutateAsync();
|
||||
if (result.success) {
|
||||
await utils.emailImportService.status.invalidate();
|
||||
toast.success("Service démarré");
|
||||
toast.success(result.message);
|
||||
} else {
|
||||
toast.error("Impossible de démarrer le service");
|
||||
toast.error(result.message || "Impossible de démarrer le service", { duration: 8000 });
|
||||
}
|
||||
} catch {
|
||||
toast.error("Impossible de démarrer le service");
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "Impossible de démarrer le service", { duration: 8000 });
|
||||
}
|
||||
}}
|
||||
disabled={!emailImportEnabled || startEmailServiceMutation.isPending}
|
||||
@@ -757,8 +789,82 @@ export default function ImportSettings() {
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enregistrez les paramètres avant de démarrer. Le service ne démarre que si le test IMAP est concluant.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isAdmin && (
|
||||
<div className="space-y-3 border-t border-green-200 pt-4 dark:border-green-800">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Compte source administré</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Démarrez la boîte déjà configurée, même si ses paramètres appartiennent à un autre compte applicatif. Aucun identifiant n’est affiché ni recopié.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||
<select
|
||||
aria-label="Compte source de réception e-mail"
|
||||
className="h-10 min-w-0 flex-1 rounded-md border border-input bg-background px-3 text-sm"
|
||||
value={selectedEmailAccountId}
|
||||
onChange={(event) => setSelectedEmailAccountId(event.target.value)}
|
||||
disabled={emailAccountsLoading || configuredEmailAccounts.length === 0}
|
||||
>
|
||||
{configuredEmailAccounts.length === 0 ? (
|
||||
<option value="">Aucune boîte e-mail configurée</option>
|
||||
) : (
|
||||
configuredEmailAccounts.map((account) => (
|
||||
<option key={account.userId} value={String(account.userId)}>
|
||||
{account.userName || account.userEmail || `Compte #${account.userId}`}
|
||||
{account.automaticEnabled === 1 ? " — réception activée" : " — réception désactivée"}
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{selectedEmailAccountStatus?.isRunning ? (
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await stopEmailAccountMutation.mutateAsync({ userId: selectedEmailAccountUserId });
|
||||
await utils.emailImportService.accountStatus.invalidate();
|
||||
toast.success(result.message);
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "Impossible d’arrêter le service.");
|
||||
}
|
||||
}}
|
||||
disabled={stopEmailAccountMutation.isPending}
|
||||
>
|
||||
{stopEmailAccountMutation.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Square className="mr-2 h-4 w-4" />}
|
||||
Arrêter
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
try {
|
||||
const result = await startEmailAccountMutation.mutateAsync({ userId: selectedEmailAccountUserId });
|
||||
if (result.success) {
|
||||
await utils.emailImportService.accountStatus.invalidate();
|
||||
toast.success(result.message);
|
||||
} else {
|
||||
toast.error(result.message, { duration: 8000 });
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(error?.message || "Impossible de démarrer le service.", { duration: 8000 });
|
||||
}
|
||||
}}
|
||||
disabled={!selectedEmailAccountUserId || startEmailAccountMutation.isPending}
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
{startEmailAccountMutation.isPending ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Play className="mr-2 h-4 w-4" />}
|
||||
Démarrer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
CREATE TABLE `webImportSources` (
|
||||
CREATE TABLE IF NOT EXISTS `webImportSources` (
|
||||
`id` int AUTO_INCREMENT NOT NULL,
|
||||
`userId` int NOT NULL,
|
||||
`name` varchar(100) NOT NULL,
|
||||
|
||||
6
todo.md
6
todo.md
@@ -956,3 +956,9 @@
|
||||
- [x] Résoudre une destination par facture d’abonnement sélectionnée selon les règles actives
|
||||
- [x] Préserver les paramètres globaux comme repli lorsqu’aucune règle ne correspond
|
||||
- [x] Ajouter les tests de priorité et de regroupement des exports d’abonnements
|
||||
|
||||
## Déploiement des règles d’export d’abonnements
|
||||
- [x] Synchroniser la version validée et reconstruire le service applicatif de recette
|
||||
- [x] Vérifier en recette les routes, le conteneur, le volume PDF et le flux de résolution par facture
|
||||
- [x] Synchroniser la même version et reconstruire le service applicatif de production
|
||||
- [x] Vérifier en production les routes, le conteneur, le volume PDF et le dashboard associé
|
||||
|
||||
Reference in New Issue
Block a user