EPIDOCS Frontend — Présence Campus (Attendance)¶
Où trouver quoi
Le contrat backend complet (routes, droits, modèle de données, l'incident de perf sur le rendu QR, le piège cookie cross-origin, la nature asynchrone de l'envoi d'email) est déjà documenté en détail dans EPIDOCS API — Présence Campus. Cette page-ci ne le duplique pas : elle couvre uniquement l'architecture frontend — quelles pages, quels composants, quels services, et les décisions/incidents propres au frontend.
Deux services distincts pour deux populations distinctes¶
Le module présence a deux couches de service frontend séparées, reflétant les deux mécanismes d'authentification backend (voir epidocs-api/attendance.md) :
| Service frontend | Fichier | Cookie utilisé | Consommateur |
|---|---|---|---|
attendanceAdminService |
src/services/admin/attendanceAdminService.js |
auth_token (admin), via axiosInstance (withCredentials: true, cross-origin direct vers api.mydocs.epitools.bj) |
Pages sous /admin/attendance/* |
guardService |
src/services/attendance/guardService.js |
attendance_session (agent/chaperon, PIN), via attendanceAxios — baseURL relative /attendance/api, proxyée same-origin par un rewrite Next.js vers l'API (voir next.config.mjs) |
Pages sous /attendance/* (hors /admin) |
Toujours câbler une nouvelle capacité de scan dans les DEUX services
Incident vécu en construisant l'annulation de scan (2026-09-09) : la fonction cancelScan a d'abord été ajoutée uniquement à attendanceAdminService.js et câblée sur la page admin /admin/attendance/scanners. Le bouton d'annulation manquait sur la vraie console de scan (/attendance/scan, utilisée par l'agent au portail) — c'est pourtant là que l'agent réalise en temps réel qu'il vient de scanner la mauvaise personne, pas plus tard sur un écran admin auquel il n'a pas accès. Corrigé en ajoutant cancelScan à guardService.js (même route, via le proxy /attendance/api) et en câblant la prop sur les deux usages de AttendanceScanConsole.
Leçon : avant de considérer une fonctionnalité de scan « terminée », vérifier qu'elle est câblée à la fois côté attendanceAdminService/pages admin et côté guardService/console agent, si le backend l'autorise pour les deux (CanScanOrIsGuardPermission dans libs/permissions.py, backend).
Pourquoi 2 pages "historique" et pas 1 (Historique vs Suivi par scanneur)¶
Jusqu'au 2026-09-09, /admin/attendance/logs concentrait : KPIs globaux, 4 graphiques, un tableau brut des scans du jour (DayScansTable, avec sélecteur de date, filtres, export CSV) — et l'ajout des stats par agent/chaperon (GET /attendance/stats/by-scanner) l'a rendue trop chargée pour un usage pratique (retour direct de l'utilisateur : "trop d'infos déjà", besoin explicite de suivre séparément l'activité d'un agent, d'un chaperon, et de descendre jusqu'au détail par étudiant).
Décision (choisie parmi 3 options proposées) : séparer en deux pages avec des responsabilités distinctes plutôt que des onglets ou des routes par rôle :
/admin/attendance/logs— vue d'ensemble uniquement. KPIs globaux (scans, taux, refus, étudiants vus, part admin) + 4 graphiques (DailyScansChart,OriginSplitChart,ReasonsChart,SectionRateChart). Plus aucun tableau brut, plus de granularité par personne./admin/attendance/scanners(nouvelle) — le vrai outil opérationnel. Liste de tous les agents/chaperons/admins ayant scanné sur une période (recherche par nom, filtre par rôle), clic sur une personne → ses scans détaillés (paginés, filtrés surguard_idouguard_role), avec annulation directe et clic sur un scan → fiche de présence de l'étudiant (composantStudentLookupréutilisé).
DayScansTable.js (l'ancien composant, jour-par-jour, non scopé par personne) a été supprimé — sa logique de tableau/annulation/CSV a été réécrite directement dans la nouvelle page scanners/page.js, scopée par personne + période plutôt que par jour + tout le monde.
Protection des pages¶
Les 6 pages /admin/attendance/{scan,guards,reasons,photos,qrcodes,logs} n'avaient aucun ProtectedRoute avant le 2026-09-09 — voir l'incident détaillé dans frontend.md — Rôles et permissions. Toutes (plus la nouvelle scanners) sont désormais protégées :
| Page | requiredRights |
|---|---|
/admin/attendance/scan |
["can_scan_attendance"] |
/admin/attendance/guards |
["can_manage_attendance"] |
/admin/attendance/reasons |
["can_manage_attendance"] |
/admin/attendance/photos |
["can_manage_attendance"] |
/admin/attendance/qrcodes |
["can_manage_attendance"] |
/admin/attendance/logs |
["can_manage_attendance"] |
/admin/attendance/scanners |
["can_manage_attendance"] |
/attendance et /attendance/scan (écran PIN + console agent) ne portent pas de ProtectedRoute — normal, ce ne sont pas des pages admin, l'accès y est contrôlé par la possession d'un PIN valide (guardContext), pas par user.rights.
AttendanceScanConsole — composant partagé agent/admin¶
src/components/attendance/AttendanceScanConsole.js est le même composant monté par les deux pages de scan (/admin/attendance/scan/page.js et /attendance/scan/page.js), avec des props différentes :
| Prop | Admin (/admin/attendance/scan) |
Agent (/attendance/scan) |
|---|---|---|
scanPayload |
attendanceAdminService.scanPayload |
guardService.scanPayload |
updateScanNote |
attendanceAdminService.updateScanNote |
guardService.updateScanNote |
cancelScan |
attendanceAdminService.cancelScan |
guardService.cancelScan |
resolvePhotoUrl |
URL absolue directe (NEXT_PUBLIC_API_URL + chemin) |
Réécrit le préfixe /attendance → /attendance/api (photo servie uniquement via le proxy same-origin — le cookie attendance_session est host-only, un <img> pointant en direct sur l'API ne l'enverrait pas) |
identityName |
Nom/email de l'admin (useUser()) |
Nom de l'agent (useGuard()) |
onExit |
absent (l'admin quitte via la sidebar normale) | déconnexion PIN + nettoyage sessionStorage |
scanStartedStorageKey |
absent | attendance_scan_started (persiste l'état "caméra démarrée" à travers un refresh PWA) |
Flux d'annulation dans la console (le vrai point d'entrée pour un agent)¶
Après un scan autorisé, un bloc discret apparaît juste sous l'avertissement "vérifiez la photo" : « Ce n'est pas la bonne personne ? Annuler ce scan ». Cliquer ouvre un motif obligatoire (Textarea) + confirmation. Le state local result est remplacé par la réponse de l'API (qui inclut cancelled: true, cancelReason, cancelledAt) — le badge passe de vert "Accès autorisé" à orange "Scan annulé", sans recharger le scan suivant.
sequenceDiagram
participant Agent as Agent (console /attendance/scan)
participant FE as AttendanceScanConsole
participant API as Backend
Agent->>FE: Scanne un QR
FE->>API: scanPayload(decodedText)
API-->>FE: {logId, decision: "granted", student: {...photoUrl}}
FE->>Agent: Badge vert + photo — "vérifiez que ça correspond"
Agent->>FE: Remarque un décalage → clique "Annuler ce scan"
FE->>Agent: Demande un motif (requis)
Agent->>FE: Saisit le motif, confirme
FE->>API: cancelScan(logId, reason)
API-->>FE: {..., cancelled: true, cancelReason, cancelledAt}
FE->>Agent: Badge orange "Scan annulé"
QR codes (/admin/attendance/qrcodes)¶
Trois flux distincts sur la même page :
- Envoi en masse — par section (+ filtres campus/année optionnels) ou par liste de destinataires. Cette liste peut être alimentée par 3 moyens combinables : upload d'un fichier
.xlsx/.csv(détection automatique de la colonne email), copier-coller manuel dans la zone de texte, ou recherche d'un étudiant par nom (StudentSearchCombobox, ajoute son login à la liste) — ajouté le 2026-09-09 pour éviter d'avoir à connaître l'email exact ou préparer un fichier pour ajouter juste un ou deux étudiants. - Aperçu/vérification ponctuelle — recherche un login, affiche son QR (
<Image src={qrCodeUrl}>, un<img>classique). - Actions individuelles sur ce QR (ajoutées le 2026-09-09) :
- Envoi vers un email personnel (optionnel — vide = envoi au login
@epitech.eucomme avant) :POST /student/admin/<login>/qrcode/send. - Téléchargement de l'image.
Bug corrigé (2026-09-09) : le bouton Télécharger renvoyait 401
handleDownloadQr faisait fetch(previewUrl) sans option credentials. previewUrl est une URL absolue cross-origin (https://api.mydocs.epitools.bj/attendance/media/qrcodes/...) — fetch() a pour défaut credentials: 'same-origin', qui exclut l'envoi du cookie de session admin vers un sous-domaine différent. Résultat : téléchargement d'une image cassée, 401 visible dans l'onglet réseau, alors que l'aperçu <Image> (un <img> classique — pas soumis à cette règle, qui ne concerne que fetch()/XHR) s'affichait très bien avec la même session.
Fix : remplacé le fetch() nu par axiosInstance.get(previewUrl, { responseType: "blob" }) — axiosInstance (src/services/axios.js) a withCredentials: true globalement et c'est déjà par lui que transitent tous les autres appels admin authentifiés vers api.mydocs.epitools.bj. Il accepte une URL absolue telle quelle (n'y réapplique pas sa propre baseURL).
Piège générique à retenir : dès qu'un endpoint renvoie une URL cross-origin à consommer directement (image, fichier), un <img>/<a> simple fonctionne nativement, mais tout fetch()/XMLHttpRequest manuel doit explicitement passer credentials: 'include' (ou, plus simple, passer par axiosInstance qui le fait déjà globalement) — ne jamais utiliser fetch() nu pour retélécharger une ressource protégée par cookie.
Service : sendStudentQrCode, getStudentQrCodePreview, sendQrCodes, getQrAudience dans attendanceAdminService.js.
Agents & chaperons (/admin/attendance/guards)¶
Depuis le 2026-09-09, un agent porte un role (agent par défaut, ou chaperon) — sélecteur au moment de la création, badge coloré dans le tableau (bleu = agent, violet = chaperon), filtre par rôle au-dessus du tableau. attendanceAdminService.getGuards({role}) et createGuard({..., role}) prennent ce paramètre en compte ; role est simplement omis du body si non fourni (compat arrière avec un backend qui ne le connaîtrait pas encore).
Suivi par scanneur (/admin/attendance/scanners)¶
Page neuve (2026-09-09), pensée pour répondre précisément au besoin "suivre les scans d'un agent à part, d'un chaperon à part, et pouvoir descendre jusqu'au détail d'un étudiant" :
- Sélecteur de période (7/14/30 j, comme sur
/admin/attendance/logs). - Tableau de toutes les personnes ayant scanné (
GET /attendance/stats/by-scanner) — recherche par nom, filtre par rôle (agent/chaperon/admin). - Clic sur une personne → ses scans sur la période, paginés côté serveur (
GET /attendance/logsavecguard_id=<id>si la personne a unguardId, sinon repli surguard_role=<role>pour le seau "Admin" qui n'a pas d'id deSecurityGuardindividuel) — annulation directe possible sur un scan "autorisé" (même flux que dans la console de scan, motif requis). - Clic sur un scan → fiche de présence de l'étudiant (
StudentLookup, embarqué en bas de page, même mécanismependingSelectionque sur l'ancienneDayScansTable).
Piège de clé React résolu : guardId peut être null ou dupliqué
Le tableau du point 2 a d'abord été codé avec key={s.guardId}. Deux symptômes en prod : clé null dupliquée (plusieurs admins distincts scannent sans SecurityGuard associé, donc guardId: null pour chacun) et clé numérique dupliquée (une même personne peut apparaître sous plus d'une entrée agrégée selon la période/le rôle). Fix : clé composite `${s.guardId ?? "none"}-${s.role}-${idx}` (l'index du tableau comme filet de sécurité final). Réflexe à appliquer à toute liste dont la clé « naturelle » n'est pas garantie unique par le backend.
Voir aussi¶
- Flux — Présence — le même sujet, version guide pratique sans jargon technique (à donner à quelqu'un qui ne lira jamais le code)
- EPIDOCS API — Présence Campus — contrat backend complet (routes, droits, modèle de données, incident de perf QR, détail cookie cross-origin, nature asynchrone de l'envoi d'email)
- EPIDOCS API — Opérations manuelles — créer un chaperon en ligne de commande, envoyer un QR vers un email perso via
curl, etc. - frontend.md — Rôles et permissions — catalogue de droits dynamique,
ProtectedRoutevs sidebar