diff --git a/client/src/pages/Admin.tsx b/client/src/pages/Admin.tsx index 1d490e7..5b45a36 100644 --- a/client/src/pages/Admin.tsx +++ b/client/src/pages/Admin.tsx @@ -1,14 +1,31 @@ import DashboardLayout from "@/components/DashboardLayout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { trpc } from "@/lib/trpc"; -import { GraduationCap, Calendar, Users, CheckCircle } from "lucide-react"; +import { GraduationCap, Calendar, Users, CheckCircle, PieChart as PieChartIcon } from "lucide-react"; import { Skeleton } from "@/components/ui/skeleton"; +import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from "recharts"; +import { useMemo } from "react"; export default function Admin() { const { data: formations, isLoading: loadingFormations } = trpc.formations.list.useQuery(); const { data: sessions, isLoading: loadingSessions } = trpc.sessions.list.useQuery(); const { data: apprenants, isLoading: loadingApprenants } = trpc.apprenants.list.useQuery(); + // Statistiques par fonction + const statsFonction = useMemo(() => { + if (!apprenants) return []; + + const directeurs = apprenants.filter(a => a.fonction === "directeur").length; + const chefsService = apprenants.filter(a => a.fonction === "chef_service").length; + const autres = apprenants.filter(a => a.fonction === "autre").length; + + return [ + { name: "Directeurs", value: directeurs, color: "#3b82f6" }, + { name: "Chefs de service", value: chefsService, color: "#10b981" }, + { name: "Autres", value: autres, color: "#f59e0b" }, + ].filter(item => item.value > 0); + }, [apprenants]); + const stats = [ { title: "Formations actives", @@ -66,6 +83,51 @@ export default function Admin() { ))} + {/* Graphique de répartition par fonction */} + {statsFonction.length > 0 && ( + + + + + Répartition des apprenants par fonction + + + Distribution des {apprenants?.length || 0} apprenants selon leur fonction + + + + {loadingApprenants ? ( +
+ +
+ ) : ( +
+ + + `${name}: ${(percent * 100).toFixed(0)}%`} + outerRadius={100} + fill="#8884d8" + dataKey="value" + > + {statsFonction.map((entry, index) => ( + + ))} + + + + + +
+ )} +
+
+ )} + Bienvenue dans l'espace d'administration diff --git a/client/src/pages/AdminApprenants.tsx b/client/src/pages/AdminApprenants.tsx index d0b6422..7ac7ca8 100644 --- a/client/src/pages/AdminApprenants.tsx +++ b/client/src/pages/AdminApprenants.tsx @@ -16,6 +16,7 @@ export default function AdminApprenants() { const [open, setOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [searchTerm, setSearchTerm] = useState(""); + const [filterFonction, setFilterFonction] = useState("all"); const [formData, setFormData] = useState({ nom: "", prenom: "", @@ -104,9 +105,11 @@ export default function AdminApprenants() { apprenant.email.toLowerCase().includes(searchTerm.toLowerCase()) || apprenant.codeEtablissement.toLowerCase().includes(searchTerm.toLowerCase()); - return matchesSearch; + const matchesFonction = filterFonction === "all" || apprenant.fonction === filterFonction; + + return matchesSearch && matchesFonction; }); - }, [apprenants, searchTerm]); + }, [apprenants, searchTerm, filterFonction]); return ( @@ -218,17 +221,34 @@ export default function AdminApprenants() { -
- -
- - setSearchTerm(e.target.value)} - className="pl-8" - /> +
+
+ +
+ + setSearchTerm(e.target.value)} + className="pl-8" + /> +
+
+ +
+ +
diff --git a/client/src/pages/AdminSessionInscrits.tsx b/client/src/pages/AdminSessionInscrits.tsx index 790300e..fcdc043 100644 --- a/client/src/pages/AdminSessionInscrits.tsx +++ b/client/src/pages/AdminSessionInscrits.tsx @@ -21,6 +21,7 @@ export default function AdminSessionInscrits() { const [searchTerm, setSearchTerm] = useState(""); const [filterStatut, setFilterStatut] = useState("all"); const [filterEtablissement, setFilterEtablissement] = useState("all"); + const [filterFonction, setFilterFonction] = useState("all"); const [sortBy, setSortBy] = useState<"date" | "nom">("date"); const utils = trpc.useUtils(); @@ -151,7 +152,11 @@ export default function AdminSessionInscrits() { const matchesEtablissement = filterEtablissement === "all" || inscription.apprenant?.codeEtablissement === filterEtablissement; - return matchesSearch && matchesStatut && matchesEtablissement; + // Filtre par fonction + const matchesFonction = filterFonction === "all" || + inscription.apprenant?.fonction === filterFonction; + + return matchesSearch && matchesStatut && matchesEtablissement && matchesFonction; }); // Tri @@ -165,7 +170,7 @@ export default function AdminSessionInscrits() { }); return filtered; - }, [inscriptions, searchTerm, filterStatut, filterEtablissement, sortBy]); + }, [inscriptions, searchTerm, filterStatut, filterEtablissement, filterFonction, sortBy]); const getStatutBadge = (statut: string) => { const styles = { @@ -213,7 +218,7 @@ export default function AdminSessionInscrits() { -
+
{/* Recherche */}
@@ -261,6 +266,22 @@ export default function AdminSessionInscrits() {
+ {/* Filtre par fonction */} +
+ + +
+ {/* Tri */}
diff --git a/package.json b/package.json index 533ea88..e82644a 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "react-dom": "^19.1.1", "react-hook-form": "^7.64.0", "react-resizable-panels": "^3.0.6", - "recharts": "^2.15.2", + "recharts": "^2.15.4", "sonner": "^2.0.7", "streamdown": "^1.4.0", "superjson": "^1.13.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b29340c..0308415 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -188,7 +188,7 @@ importers: specifier: ^3.0.6 version: 3.0.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0) recharts: - specifier: ^2.15.2 + specifier: ^2.15.4 version: 2.15.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) sonner: specifier: ^2.0.7 diff --git a/server/emailService.ts b/server/emailService.ts index 208f441..64cfe8b 100644 --- a/server/emailService.ts +++ b/server/emailService.ts @@ -94,6 +94,7 @@ export async function sendInscriptionConfirmation(params: { apprenantEmail: string; apprenantNom: string; apprenantPrenom: string; + apprenantFonction: string; formationNom: string; sessionNom: string; dateDebut: Date; @@ -102,10 +103,12 @@ export async function sendInscriptionConfirmation(params: { statut: 'confirmee' | 'liste_attente'; }): Promise { const isConfirmed = params.statut === 'confirmee'; + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : 'Autre'; const content = `

Confirmation d'inscription

-

Bonjour ${params.apprenantPrenom} ${params.apprenantNom},

+

Bonjour ${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom},

${isConfirmed ? '

Votre inscription a été confirmée !

' @@ -175,13 +178,19 @@ export async function sendInscriptionConfirmation(params: { export async function sendTeaserEmail(params: { apprenantEmail: string; apprenantPrenom: string; + apprenantNom: string; + apprenantFonction: string; formationNom: string; sessionNom: string; dateDebut: Date; }): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + const content = `

Votre formation approche !

-

Bonjour ${params.apprenantPrenom},

+

Bonjour ${salutation},

Nous sommes ravis de vous accueillir prochainement pour la formation ${params.formationNom}.

@@ -213,14 +222,20 @@ export async function sendTeaserEmail(params: { export async function sendRappelJ7Email(params: { apprenantEmail: string; apprenantPrenom: string; + apprenantNom: string; + apprenantFonction: string; formationNom: string; sessionNom: string; dateDebut: Date; lieu: string; }): Promise { + const fonctionLabel = params.apprenantFonction === 'directeur' ? 'Directeur' : + params.apprenantFonction === 'chef_service' ? 'Chef de service' : ''; + const salutation = fonctionLabel ? `${fonctionLabel} ${params.apprenantPrenom} ${params.apprenantNom}` : params.apprenantPrenom; + const content = `

Rappel : Votre formation dans 7 jours

-

Bonjour ${params.apprenantPrenom},

+

Bonjour ${salutation},

Nous vous rappelons que votre formation ${params.formationNom} commence dans 7 jours.

@@ -256,7 +271,7 @@ export async function sendRappelJ7Email(params: { * Envoie des emails groupés à tous les inscrits d'une session */ export async function sendGroupEmail(params: { - recipients: Array<{ email: string; prenom: string }>; + recipients: Array<{ email: string; prenom: string; nom: string; fonction: string }>; formationNom: string; sessionNom: string; type: 'teaser' | 'rappel'; @@ -272,6 +287,8 @@ export async function sendGroupEmail(params: { await sendTeaserEmail({ apprenantEmail: recipient.email, apprenantPrenom: recipient.prenom, + apprenantNom: recipient.nom, + apprenantFonction: recipient.fonction, formationNom: params.formationNom, sessionNom: params.sessionNom, dateDebut: params.dateDebut, @@ -280,6 +297,8 @@ export async function sendGroupEmail(params: { await sendRappelJ7Email({ apprenantEmail: recipient.email, apprenantPrenom: recipient.prenom, + apprenantNom: recipient.nom, + apprenantFonction: recipient.fonction, formationNom: params.formationNom, sessionNom: params.sessionNom, dateDebut: params.dateDebut, diff --git a/server/routers.ts b/server/routers.ts index e63b80c..bbe902e 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -245,6 +245,7 @@ export const appRouter = router({ apprenantEmail: inscriptionApprenant.email, apprenantNom: inscriptionApprenant.nom, apprenantPrenom: inscriptionApprenant.prenom, + apprenantFonction: inscriptionApprenant.fonction, formationNom: inscriptionFormation.nom, sessionNom: inscriptionSession.nom, dateDebut: new Date(inscriptionSession.dateDebut), @@ -315,6 +316,8 @@ export const appRouter = router({ .map(i => ({ email: i.apprenant!.email, prenom: i.apprenant!.prenom, + nom: i.apprenant!.nom, + fonction: i.apprenant!.fonction, })); const result = await sendGroupEmail({ diff --git a/todo.md b/todo.md index 293fe3d..6901242 100644 --- a/todo.md +++ b/todo.md @@ -74,3 +74,11 @@ - [x] Mettre à jour le schéma de base de données - [x] Mettre à jour les formulaires d'inscription et de gestion - [x] Ajouter la fonction dans les exports PDF/Excel + +## Améliorations supplémentaires + +- [x] Ajouter un filtre par fonction sur la page des apprenants +- [x] Ajouter un filtre par fonction sur la page des inscrits +- [x] Créer des statistiques de répartition par fonction sur le tableau de bord +- [x] Ajouter des graphiques de visualisation des données +- [x] Personnaliser les templates d'emails avec la fonction de l'apprenant