Documentation : Reservation Admin (adminReservation)
Voir aussi : Matching instantané — réservation système (1 jeune + 3 bénévoles, 15 min), affichage identique (rouge).
Vue d'ensemble
La reservation admin est un mécanisme de verrouillage temporaire qui permet à un administrateur de "réserver" un jeune ou un bénévole pendant qu'il travaille dessus (notamment lors du matching). Cela évite que plusieurs admins ne travaillent simultanément sur le même profil et ne créent des conflits.
Objectifs métier
- Éviter les conflits : Quand un admin ouvre la page de matching d'un jeune (ou bénévole), il réserve ce profil pour lui seul pendant 30 minutes.
- Blocage des autres admins : Pendant cette période, les autres admins ne peuvent pas :
- Voir la liste des candidats au matching pour ce profil
- Matcher ce profil avec quelqu'un
- Accéder à certaines actions sur la fiche
- Libération explicite : L'admin peut libérer sa réservation via le bouton "Arrêter le matching".
- Expiration automatique : La réservation expire automatiquement après 30 minutes si l'admin ne libère pas.
Modèle de données
Entité AdminReservation
| Champ | Type | Description |
|---|---|---|
id |
bigint | Identifiant unique |
reservationDate |
timestamp | Date/heure de la réservation (utilisée pour calculer l'expiration) |
deletedAt |
timestamp (soft delete) | Date de suppression (null = réservation active) |
benevoleId |
varchar | ID du bénévole réservé (nullable) |
jeuneId |
varchar | ID du jeune réservé (nullable) |
adminId |
varchar | ID de l'admin qui a réservé |
Note : Une entrée AdminReservation lie soit un bénévole soit un jeune à un admin. Les champs benevoleId et jeuneId sont mutuellement exclusifs selon le contexte.
Relations
- Benevole ↔ AdminReservation : OneToMany (un bénévole peut avoir plusieurs réservations dans l'historique)
- Jeune ↔ AdminReservation : OneToMany
- User (admin) ↔ AdminReservation : OneToMany
Logique de durée : 30 minutes
La réservation est considérée active si :
reservationDate + 30 minutes > NOW()
- Active : L'admin a l'exclusivité, les autres sont bloqués
- Expirée : Tout le monde peut à nouveau réserver
Flux fonctionnels
1. Réservation lors du matching (bénévole → jeunes)
Contexte : Un admin ouvre la page de matching d'un bénévole pour lui trouver des jeunes.
- GET
/rating/jeuneList/:benevoleId(RatingController) - Vérification : le bénévole est-il déjà réservé par un autre admin ?
- Si oui et réservation active → retourne
{ success: false, message: 'reserved' } - Sinon → appelle
reserveBenevole(benevoleId, adminId) reserveBenevolecrée ou met à jour une entréeAdminReservation(voir section "Logique de reserve")- Retourne la liste des jeunes matchables (filtrés des jeunes déjà réservés par d'autres admins)
2. Réservation lors du matching (jeune → bénévoles)
Contexte : Un admin ouvre la page de matching d'un jeune pour lui trouver des bénévoles.
- GET
/rating/benevoleList/:jeuneId(RatingController) - Vérification : le jeune est-il déjà réservé par un autre admin ?
- Si oui et réservation active → retourne
{ success: false, message: 'reserved' } - Sinon → appelle
reserveJeune(jeuneId, admin.id) - Retourne la liste des bénévoles matchables (filtrés des bénévoles déjà réservés)
3. Libération de la réservation ("Arrêter le matching")
- GET
/rating/stopResa/benevole/:idou/rating/stopResa/jeune/:id - Appelle
stopReserveBenevoleoustopReserveJeune - Soft delete de la dernière réservation de cet admin pour ce profil
4. Réservation multiple (jeunes uniquement)
Contexte : Actions en masse sur plusieurs jeunes (ex. changement de statut).
- POST
/jeunes/multiple/resaavec{ jeuneIds: [...] } - Pour chaque jeune non réservé par un autre admin, crée une réservation
- Retourne
{ success: true, errors: [ids des jeunes déjà réservés] }
Logique détaillée de reserveBenevole / reserveJeune
1. Récupérer les réservations actives (deletedAt IS NULL) pour ce benevole/jeune + cet admin
2. Si aucune réservation existante :
→ Créer une nouvelle AdminReservation (benevoleId/jeuneId, adminId, reservationDate = NOW)
3. Si réservation(s) existante(s) pour CET admin :
- Dernière réservation : dateReservation = reservationDate + 30 min
- Si today < dateReservation (réservation encore valide) :
→ Mettre à jour reservationDate = NOW() (prolonge la fenêtre de 30 min)
- Sinon (expirée) :
→ Créer une nouvelle AdminReservation
4. Si réservation existante pour UN AUTRE admin :
→ Créer une nouvelle AdminReservation (pour cet admin)
(Note : la vérification "bloquant" est faite AVANT l'appel, au niveau du controller)
Points d'entrée frontend
Store Vuex
| Store | État | Description |
|---|---|---|
benevoleAdmin |
isBinomableB |
true si le bénévole peut être matché (non réservé par un autre ou réservé par moi) |
benevoleAdmin |
isReservedToMeB |
true si le bénévole est réservé par l'admin connecté |
jeuneAdmin |
isBinomable |
idem pour jeune |
jeuneAdmin |
isReservedToMe |
idem pour jeune |
Ces états sont calculés dans getBenevole / getJeune à partir de adminReservation :
- isReservedToMe : dernière réservation par moi ET encore dans les 30 min
- isBinomable : pas de réservation active par un autre admin (ou réservé par moi)
Composants
- ButtonsAdmin.vue : Affiche "Arrêter le matching" si
isReservedToMe, désactive les boutons si!isBinomable - Pages matching (
/bo/jeunes/:id/matching,/bo/benevoles/:id/matching) : Gèrentreserved(message "réservé par un autre admin") et appellentstopResapour libérer
Services API
ratingService.getJeuneList(benevoleId)→ réserve le bénévole + retourne les jeunesratingService.getBenevoleList(jeuneId)→ réserve le jeune + retourne les bénévolesratingService.stopResaBenevole(id)/ratingService.stopResaJeune(id)→ libère la réservation
Filtrage dans les requêtes SQL
Chargement des réservations actives uniquement
Pour éviter de charger des lignes inutiles, seules les réservations actives (30 min) sont récupérées à chaque usage :
findWithRelationsForList(listes BO) :leftJoinAndSelectavec conditiondeletedAt IS NULL AND DATE_ADD(reservationDate, INTERVAL 30 MINUTE) > NOW()findJeunesWithRelationForMatching/findBenevolesWithRelationForMatching: idemfindJeune/findBenevoleavec relationadminReservation: chargement séparé viaAdminReservationRepository.findActiveForJeune/ForBenevole
BenevoleRepository.findBenevolesWithRelationForMatching
Les bénévoles retournés pour le matching d'un jeune sont filtrés via un LEFT JOIN sur adminReservation avec les conditions :
adminReservation.deletedAt IS NULL
AND adminReservation.adminId != :adminId
AND DATE_ADD(adminReservation.reservationDate, INTERVAL 30 MINUTE) > NOW()
→ Seules les réservations bloquantes (par un autre admin, encore actives) sont jointes. Si adminReservation.id est non null, le bénévole est exclu (filtrage en mémoire dans le controller).
JeuneRepository.findMatchable
Les jeunes sont filtrés en mémoire après la requête : on exclut ceux qui ont une réservation active par un autre admin.
Endpoints API
| Méthode | Route | Rôle | Réservation |
|---|---|---|---|
| GET | /rating/jeuneList/:benevoleId |
Liste des jeunes matchables pour un bénévole | Réserve le bénévole |
| GET | /rating/benevoleList/:jeuneId |
Liste des bénévoles matchables pour un jeune | Réserve le jeune |
| GET | /rating/stopResa/benevole/:id |
Libère la réservation d'un bénévole | Soft delete |
| GET | /rating/stopResa/jeune/:id |
Libère la réservation d'un jeune | Soft delete |
| POST | /jeunes/multiple/resa |
Réserve plusieurs jeunes en masse | Crée des réservations |
Checklist de conformité aux besoins
| Besoin | Implémenté ? | Détail |
|---|---|---|
| Empêcher 2 admins de matcher le même profil en même temps | ✅ | Blocage via reserved + isBinomable |
| Durée de réservation limitée | ✅ | 30 minutes |
| Prolongation automatique en restant sur la page | ⚠️ | Non : la prolongation ne se fait qu'à chaque nouvel appel API (ex. changement de filtre). Pas de heartbeat périodique. |
| Libération manuelle | ✅ | Bouton "Arrêter le matching" |
| Réservation multiple (jeunes) | ✅ | Endpoint multiple/resa |
| Réservation multiple (bénévoles) | ❌ | Pas d'équivalent pour les bénévoles |
| Superadmin bypass | ⚠️ | Le superadmin peut bypasser adminAssocieId et certaines restrictions, mais la réservation s'applique aussi à lui (il peut réserver et être bloqué par une réservation d'un autre admin). |
| Affichage "réservé par un autre" | ✅ | reserved + message sur les pages matching |
| Désactivation des boutons quand réservé | ✅ | isBinomable → désactive Matcher, etc. |
Points d'attention / limites
-
Pas de heartbeat : Si l'admin reste 30 min sur la page sans interagir, sa réservation expire. Un autre admin pourra alors réserver. Pas de renouvellement automatique.
-
Réservation multiple bénévoles : Seuls les jeunes ont un endpoint
multiple/resa. Les bénévoles n'ont pas d'équivalent pour des actions en masse. -
Soft delete : Les anciennes réservations sont soft-deleted, pas supprimées. L'historique reste en base.
-
Cohérence front/back : La logique des 30 min est dupliquée (front dans les stores, back dans les controllers/services). Une dérive est possible si l'un est modifié sans l'autre.
-
Race conditions : Entre la vérification "pas réservé" et l'appel à
reserveBenevole/reserveJeune, un autre admin pourrait théoriquement réserver entre-temps. En pratique, la fenêtre est très courte.
Fichiers clés
| Fichier | Rôle |
|---|---|
back/src/binomes/entities/AdminReservation.entity.ts |
Entité |
back/src/binomes/services/benevoles.service.ts |
reserveBenevole, stopReserveBenevole |
back/src/binomes/services/jeunes.service.ts |
reserveJeune, stopReserveJeune |
back/src/binomes/controllers/rating.controller.ts |
Endpoints rating + stopResa |
back/src/binomes/controllers/jeune.controller.ts |
multiple/resa |
back/src/binomes/repositories/benevole.repository.ts |
findBenevolesWithRelationForMatching (filtre adminReservation) |
front/store/benevoleAdmin.js |
État isBinomableB, isReservedToMeB |
front/store/jeuneAdmin.js |
État isBinomable, isReservedToMe |
front/components/bo/widgets/ButtonsAdmin.vue |
Bouton "Arrêter le matching" |
front/services/rating.js |
Appels API rating |