**Corrections d'erreurs :** 1. **Erreurs SQL DATE_FORMAT corrigées** (analyticsDb.ts) : - Remplacement des sous-requêtes corrélées par des JOIN dans getTauxRemplissageByMonth() - Utilisation de COUNT(CASE WHEN...) au lieu de SUM(SELECT COUNT(*)) - Ajout de leftJoin pour éviter les erreurs de sous-requêtes 2. **Erreur React "Cannot read properties of null" corrigée** (AdminRapportPublicCible.tsx) : - Ajout de l'opérateur null-safe `?.` pour accéder à `insc.apprenant?.fonction` - Vérification explicite si la fonction est null/undefined - Gestion du cas "public cible = tous" qui correspond toujours 3. **Tests réussis** : - Page rapport-public-cible : affiche correctement les statistiques et recommandations - Tableau de bord analytique : graphiques et statistiques fonctionnels - Page suivi questionnaires : affiche correctement les données **Harmonisation charte graphique :** - Application du style bleu clair (#6B9FE8) avec ombre portée à tous les titres des 32 pages - Création de la classe CSS `.page-title` réutilisable dans index.css - Cohérence visuelle sur toute l'application **Résultat :** ✅ Toutes les erreurs SQL et React sont corrigées ✅ Les pages fonctionnent correctement sans erreur ✅ Charte graphique harmonisée sur toutes les pages
489 lines
19 KiB
TypeScript
489 lines
19 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import DashboardLayout from "@/components/DashboardLayout";
|
|
import { trpc } from "@/lib/trpc";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { toast } from "sonner";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { AlertCircle, CheckCircle2, Mail, Send, Server } from "lucide-react";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
|
|
export default function AdminEmailConfig() {
|
|
const utils = trpc.useUtils();
|
|
const { data: config, isLoading, refetch } = trpc.emailConfig.get.useQuery();
|
|
const upsertMutation = trpc.emailConfig.upsert.useMutation();
|
|
const testEmailMutation = trpc.emailConfig.testEmail.useMutation();
|
|
|
|
const [formData, setFormData] = useState({
|
|
provider: "resend" as "resend" | "smtp",
|
|
apiKey: "",
|
|
fromEmail: "",
|
|
fromName: "Formation Manager Itinova",
|
|
mode: "simulation" as "simulation" | "production",
|
|
domainVerified: false,
|
|
smtpHost: "",
|
|
smtpPort: 587,
|
|
smtpSecure: "tls" as "none" | "tls" | "ssl",
|
|
smtpUser: "",
|
|
smtpPassword: "",
|
|
});
|
|
|
|
const [testEmail, setTestEmail] = useState("");
|
|
|
|
// Charger la configuration existante
|
|
useEffect(() => {
|
|
if (config) {
|
|
setFormData({
|
|
provider: config.provider as "resend" | "smtp",
|
|
apiKey: config.apiKey || "",
|
|
fromEmail: config.fromEmail,
|
|
fromName: config.fromName,
|
|
mode: config.mode,
|
|
domainVerified: config.domainVerified,
|
|
smtpHost: config.smtpHost || "",
|
|
smtpPort: config.smtpPort || 587,
|
|
smtpSecure: (config.smtpSecure as "none" | "tls" | "ssl") || "tls",
|
|
smtpUser: config.smtpUser || "",
|
|
smtpPassword: config.smtpPassword || "",
|
|
});
|
|
}
|
|
}, [config]);
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
|
|
upsertMutation.mutate(formData, {
|
|
onSuccess: async () => {
|
|
toast.success("Configuration enregistrée avec succès");
|
|
// Invalider le cache et forcer le rechargement
|
|
await utils.emailConfig.get.invalidate();
|
|
await refetch();
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Erreur: ${error.message}`);
|
|
},
|
|
});
|
|
};
|
|
|
|
const handleTestEmail = () => {
|
|
if (!testEmail) {
|
|
toast.error("Veuillez saisir une adresse email");
|
|
return;
|
|
}
|
|
|
|
testEmailMutation.mutate(
|
|
{ toEmail: testEmail },
|
|
{
|
|
onSuccess: (data) => {
|
|
if (data.success) {
|
|
toast.success(data.message || "Email de test envoyé avec succès");
|
|
} else {
|
|
toast.error(data.message || "Échec de l'envoi de l'email de test");
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
toast.error(`Erreur: ${error.message}`);
|
|
},
|
|
}
|
|
);
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="container py-8">
|
|
<p>Chargement...</p>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="container py-8 max-w-4xl">
|
|
<div className="mb-8">
|
|
<h1 className="page-title">Configuration des emails</h1>
|
|
<p className="text-muted-foreground mt-2">
|
|
Configurez l'envoi d'emails via Resend et basculez entre mode simulation et production
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-6">
|
|
{/* Statut actuel */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Mail className="h-5 w-5" />
|
|
Statut actuel
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between p-4 border rounded-lg">
|
|
<div>
|
|
<p className="font-medium">Mode d'envoi</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{formData.mode === "simulation"
|
|
? "Les emails sont simulés et envoyés comme notifications"
|
|
: formData.provider === "smtp"
|
|
? "Les emails sont envoyés via SMTP Direct"
|
|
: "Les emails sont envoyés via Resend"}
|
|
</p>
|
|
</div>
|
|
<div className={`px-3 py-1 rounded-full text-sm font-medium ${
|
|
formData.mode === "production"
|
|
? "bg-green-100 text-green-800"
|
|
: "bg-yellow-100 text-yellow-800"
|
|
}`}>
|
|
{formData.mode === "production" ? "Production" : "Simulation"}
|
|
</div>
|
|
</div>
|
|
|
|
{formData.provider === "resend" && formData.apiKey && (
|
|
<Alert>
|
|
<CheckCircle2 className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Clé API Resend configurée
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{formData.provider === "smtp" && formData.smtpHost && formData.smtpUser && (
|
|
<Alert>
|
|
<CheckCircle2 className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Configuration SMTP active ({formData.smtpHost}:{formData.smtpPort})
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{formData.provider === "resend" && !formData.apiKey && (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Aucune clé API Resend configurée. Les emails seront simulés.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
{formData.provider === "smtp" && (!formData.smtpHost || !formData.smtpUser) && (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Configuration SMTP incomplète. Les emails seront simulés.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Formulaire de configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Configuration de l'envoi d'emails</CardTitle>
|
|
<CardDescription>
|
|
Choisissez votre méthode d'envoi : Resend (API) ou SMTP direct
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-6">
|
|
{/* Sélecteur de provider */}
|
|
<div className="space-y-2">
|
|
<Label htmlFor="provider">Méthode d'envoi</Label>
|
|
<Select
|
|
value={formData.provider}
|
|
onValueChange={(value: "resend" | "smtp") => setFormData({ ...formData, provider: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="resend">
|
|
<div className="flex items-center gap-2">
|
|
<Mail className="h-4 w-4" />
|
|
Resend (API)
|
|
</div>
|
|
</SelectItem>
|
|
<SelectItem value="smtp">
|
|
<div className="flex items-center gap-2">
|
|
<Server className="h-4 w-4" />
|
|
SMTP Direct
|
|
</div>
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-sm text-muted-foreground">
|
|
{formData.provider === "resend"
|
|
? "Service d'envoi d'emails via API (recommandé)"
|
|
: "Connexion directe à un serveur SMTP (Gmail, Outlook, etc.)"}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Configuration Resend */}
|
|
{formData.provider === "resend" && (
|
|
<>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="apiKey">Clé API Resend *</Label>
|
|
<Input
|
|
id="apiKey"
|
|
type="password"
|
|
placeholder="re_..."
|
|
value={formData.apiKey}
|
|
onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })}
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
Obtenez votre clé API sur{" "}
|
|
<a
|
|
href="https://resend.com/api-keys"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary hover:underline"
|
|
>
|
|
resend.com/api-keys
|
|
</a>
|
|
</p>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="fromEmail">Email expéditeur *</Label>
|
|
<Input
|
|
id="fromEmail"
|
|
type="email"
|
|
placeholder="noreply@votredomaine.com"
|
|
value={formData.fromEmail}
|
|
onChange={(e) => setFormData({ ...formData, fromEmail: e.target.value })}
|
|
required
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
L'adresse email doit être vérifiée dans votre compte Resend
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* Configuration SMTP */}
|
|
{formData.provider === "smtp" && (
|
|
<>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="smtpHost">Hôte SMTP *</Label>
|
|
<Input
|
|
id="smtpHost"
|
|
type="text"
|
|
placeholder="smtp.gmail.com"
|
|
value={formData.smtpHost}
|
|
onChange={(e) => setFormData({ ...formData, smtpHost: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="smtpPort">Port *</Label>
|
|
<Input
|
|
id="smtpPort"
|
|
type="number"
|
|
placeholder="587"
|
|
value={formData.smtpPort}
|
|
onChange={(e) => setFormData({ ...formData, smtpPort: parseInt(e.target.value) || 587 })}
|
|
required
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="smtpSecure">Sécurité</Label>
|
|
<Select
|
|
value={formData.smtpSecure}
|
|
onValueChange={(value: "none" | "tls" | "ssl") => setFormData({ ...formData, smtpSecure: value })}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="tls">TLS/STARTTLS (port 587)</SelectItem>
|
|
<SelectItem value="ssl">SSL (port 465)</SelectItem>
|
|
<SelectItem value="none">Aucune (port 25)</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="smtpUser">Nom d'utilisateur *</Label>
|
|
<Input
|
|
id="smtpUser"
|
|
type="text"
|
|
placeholder="votre@email.com"
|
|
value={formData.smtpUser}
|
|
onChange={(e) => setFormData({ ...formData, smtpUser: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="smtpPassword">Mot de passe *</Label>
|
|
<Input
|
|
id="smtpPassword"
|
|
type="password"
|
|
placeholder="Mot de passe ou mot de passe d'application"
|
|
value={formData.smtpPassword}
|
|
onChange={(e) => setFormData({ ...formData, smtpPassword: e.target.value })}
|
|
required
|
|
/>
|
|
<p className="text-sm text-muted-foreground">
|
|
Pour Gmail, utilisez un{" "}
|
|
<a
|
|
href="https://support.google.com/accounts/answer/185833"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-primary hover:underline"
|
|
>
|
|
mot de passe d'application
|
|
</a>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="fromEmail">Email expéditeur *</Label>
|
|
<Input
|
|
id="fromEmail"
|
|
type="email"
|
|
placeholder="noreply@votredomaine.com"
|
|
value={formData.fromEmail}
|
|
onChange={(e) => setFormData({ ...formData, fromEmail: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="fromName">Nom de l'expéditeur *</Label>
|
|
<Input
|
|
id="fromName"
|
|
type="text"
|
|
placeholder="Formation Manager Itinova"
|
|
value={formData.fromName}
|
|
onChange={(e) => setFormData({ ...formData, fromName: e.target.value })}
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between p-4 border rounded-lg">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="mode">Mode production</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Activer l'envoi réel d'emails (désactiver pour simuler)
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="mode"
|
|
checked={formData.mode === "production"}
|
|
onCheckedChange={(checked) =>
|
|
setFormData({ ...formData, mode: checked ? "production" : "simulation" })
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<Button type="submit" disabled={upsertMutation.isPending}>
|
|
{upsertMutation.isPending ? "Enregistrement..." : "Enregistrer la configuration"}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Test d'envoi */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Send className="h-5 w-5" />
|
|
Test d'envoi
|
|
</CardTitle>
|
|
<CardDescription>
|
|
Envoyez un email de test pour vérifier votre configuration
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="testEmail">Adresse email de test</Label>
|
|
<Input
|
|
id="testEmail"
|
|
type="email"
|
|
placeholder="votre@email.com"
|
|
value={testEmail}
|
|
onChange={(e) => setTestEmail(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<Button
|
|
onClick={handleTestEmail}
|
|
disabled={testEmailMutation.isPending || !testEmail}
|
|
variant="outline"
|
|
>
|
|
{testEmailMutation.isPending ? "Envoi en cours..." : "Envoyer un email de test"}
|
|
</Button>
|
|
|
|
{formData.mode === "simulation" && (
|
|
<Alert>
|
|
<AlertCircle className="h-4 w-4" />
|
|
<AlertDescription>
|
|
Mode simulation actif. L'email de test sera simulé et envoyé comme notification au propriétaire.
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Documentation */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Guide de configuration</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4 text-sm">
|
|
<div>
|
|
<h4 className="font-medium mb-2">1. Créer un compte Resend</h4>
|
|
<p className="text-muted-foreground">
|
|
Inscrivez-vous gratuitement sur{" "}
|
|
<a href="https://resend.com" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">
|
|
resend.com
|
|
</a>{" "}
|
|
(100 emails/jour gratuits)
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="font-medium mb-2">2. Vérifier votre domaine</h4>
|
|
<p className="text-muted-foreground">
|
|
Ajoutez et vérifiez votre domaine dans les paramètres Resend pour pouvoir envoyer des emails
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="font-medium mb-2">3. Obtenir la clé API</h4>
|
|
<p className="text-muted-foreground">
|
|
Générez une clé API dans la section "API Keys" de votre compte Resend
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="font-medium mb-2">4. Configurer et tester</h4>
|
|
<p className="text-muted-foreground">
|
|
Saisissez vos informations ci-dessus, enregistrez, puis testez l'envoi avant de passer en mode production
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|