diff --git a/client/src/components/DashboardLayout.tsx b/client/src/components/DashboardLayout.tsx index ae26b71..5b260c0 100644 --- a/client/src/components/DashboardLayout.tsx +++ b/client/src/components/DashboardLayout.tsx @@ -174,9 +174,18 @@ function DashboardLayoutContent({ const [location, setLocation] = useLocation(); const { state, toggleSidebar } = useSidebar(); const isCollapsed = state === "collapsed"; + + // Filtrer les sections de menu selon le rôle + const filteredMenuSections = menuSections.filter(section => { + // Les formateurs ne voient pas Configuration ni Traçabilité + if (user?.role === 'formateur') { + return section.title !== 'Configuration' && section.title !== 'Traçabilité' && section.title !== 'Analyses'; + } + return true; + }); const [isResizing, setIsResizing] = useState(false); const sidebarRef = useRef(null); - const activeMenuItem = menuSections.flatMap(section => section.items).find(item => item.path === location); + const activeMenuItem = filteredMenuSections.flatMap(section => section.items).find(item => item.path === location); const isMobile = useIsMobile(); // État pour gérer les sections ouvertes/fermées (persisté dans localStorage) @@ -308,7 +317,7 @@ function DashboardLayoutContent({ - {menuSections.map((section, sectionIndex) => { + {filteredMenuSections.map((section, sectionIndex) => { const isOpen = openSections[section.title] ?? false; return (
diff --git a/server/db.ts b/server/db.ts index 324147e..b9aa4c7 100644 --- a/server/db.ts +++ b/server/db.ts @@ -137,6 +137,27 @@ export async function getFormations() { return await db.select().from(formations); } +export async function getFormationsByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les IDs de formations uniques pour lesquelles le formateur a des séquences + const seqs = await db.select({ formationId: sequences.formationId }) + .from(sequences) + .where(eq(sequences.formateurId, formateurId)); + + const formationIds = Array.from(new Set(seqs.map(s => s.formationId))); + + if (formationIds.length === 0) return []; + + // Récupérer les formations correspondantes + const result = await db.select().from(formations).where( + sql`${formations.id} IN (${sql.join(formationIds.map(id => sql`${id}`), sql`, `)})` + ); + + return result; +} + export async function getFormationById(id: number) { const db = await getDb(); if (!db) return undefined; @@ -212,6 +233,35 @@ export async function getSequencesWithFormation() { ); } +export async function getSequencesByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer uniquement les séquences de ce formateur + const seqs = await db.select().from(sequences).where(eq(sequences.formateurId, formateurId)); + + // Récupérer les informations de formation et de formateur pour chaque séquence + return await Promise.all( + seqs.map(async (seq) => { + const formation = await db.select().from(formations).where(eq(formations.id, seq.formationId)).limit(1); + + let formateur = null; + if (seq.formateurId) { + const formateurResult = await db.select().from(formateurs).where(eq(formateurs.id, seq.formateurId)).limit(1); + if (formateurResult.length > 0) { + formateur = { id: formateurResult[0].id, nom: formateurResult[0].nom }; + } + } + + return { + ...seq, + formation: formation.length > 0 ? { id: formation[0].id, nom: formation[0].nom } : null, + formateur, + }; + }) + ); +} + export async function getSequenceById(id: number): Promise { const db = await getDb(); if (!db) return undefined; @@ -304,6 +354,58 @@ export async function getApprenants() { return apprenantsWithInscriptions; } +export async function getApprenantsByFormateur(formateurId: number) { + const db = await getDb(); + if (!db) return []; + + // Récupérer les IDs de séquences du formateur + const seqs = await db.select({ id: sequences.id }) + .from(sequences) + .where(eq(sequences.formateurId, formateurId)); + + const sequenceIds = seqs.map(s => s.id); + + if (sequenceIds.length === 0) return []; + + // Récupérer les IDs d'apprenants inscrits à ces séquences + const inscriptionsData = await db.select({ apprenantId: inscriptions.apprenantId }) + .from(inscriptions) + .where( + sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})` + ); + + const apprenantIds = Array.from(new Set(inscriptionsData.map(i => i.apprenantId))); + + if (apprenantIds.length === 0) return []; + + // Récupérer les apprenants correspondants + const apprenantsData = await db.select().from(apprenants).where( + sql`${apprenants.id} IN (${sql.join(apprenantIds.map(id => sql`${id}`), sql`, `)})` + ); + + // Pour chaque apprenant, récupérer ses inscriptions (filtrées par les séquences du formateur) + const apprenantsWithInscriptions = await Promise.all( + apprenantsData.map(async (apprenant) => { + const inscriptionsFiltered = await db + .select() + .from(inscriptions) + .where( + and( + eq(inscriptions.apprenantId, apprenant.id), + sql`${inscriptions.sequenceId} IN (${sql.join(sequenceIds.map(id => sql`${id}`), sql`, `)})` + ) + ); + + return { + ...apprenant, + inscriptions: inscriptionsFiltered, + }; + }) + ); + + return apprenantsWithInscriptions; +} + export async function getApprenantById(id: number): Promise { const db = await getDb(); if (!db) return undefined; diff --git a/server/routers.ts b/server/routers.ts index ecd8d5f..d27cca4 100644 --- a/server/routers.ts +++ b/server/routers.ts @@ -36,8 +36,15 @@ export const appRouter = router({ // ===== FORMATIONS ===== formations: router({ - list: adminProcedure.query(async () => { - return db.getFormations(); + list: protectedProcedure.query(async ({ ctx }) => { + // Si l'utilisateur est formateur, ne montrer que les formations pour lesquelles il a des séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + return db.getFormationsByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + return db.getFormations(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } }), getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { @@ -140,8 +147,15 @@ export const appRouter = router({ // ===== APPRENANTS ===== apprenants: router({ - list: adminProcedure.query(async () => { - return db.getApprenants(); + list: protectedProcedure.query(async ({ ctx }) => { + // Si l'utilisateur est formateur, ne montrer que les apprenants inscrits à ses séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + return db.getApprenantsByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + return db.getApprenants(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } }), getById: adminProcedure.input(z.object({ id: z.number() })).query(async ({ input }) => { @@ -251,8 +265,17 @@ export const appRouter = router({ // ===== SÉQUENCES ===== sequences: router({ - list: adminProcedure.query(async () => { - const seqs = await db.getSequencesWithFormation(); + list: protectedProcedure.query(async ({ ctx }) => { + let seqs; + + // Si l'utilisateur est formateur, ne montrer que ses séquences + if (ctx.user.role === 'formateur' && ctx.user.formateurId) { + seqs = await db.getSequencesByFormateur(ctx.user.formateurId); + } else if (ctx.user.role === 'admin') { + seqs = await db.getSequencesWithFormation(); + } else { + throw new TRPCError({ code: 'FORBIDDEN', message: 'Accès non autorisé' }); + } // Récupérer les dates et le nombre d'inscrits pour chaque séquence const sequencesAvecDates = await Promise.all( seqs.map(async (seq) => { diff --git a/todo.md b/todo.md index 5e01675..01c4a8c 100644 --- a/todo.md +++ b/todo.md @@ -605,3 +605,12 @@ - [x] Créer colonne "Rappels pré-formation" avec badges bleus - [x] Créer colonne "Rappels post-formation" avec badges verts - [x] Tester l'affichage dans les vues complète et réduite + +## Restriction d'accès pour les formateurs +- [x] Ajouter le rôle "formateur" dans la table users +- [x] Modifier le menu DashboardLayout pour masquer Configuration et Traçabilité aux formateurs +- [x] Filtrer les formations pour ne montrer que celles du formateur connecté +- [x] Filtrer les séquences pour ne montrer que celles du formateur connecté +- [x] Filtrer les apprenants pour ne montrer que ceux inscrits aux séquences du formateur +- [x] Créer une procédure backend pour vérifier si un utilisateur est formateur +- [x] Tester avec un compte formateur