Apparence
Absences
Le domaine des absences, de son référentiel à sa décision, et jusqu'à la demande que dépose un salarié. Les scénarios 1 à 8 se jouent dans le back-office (/admin/ sur l'hôte de l'API) : c'est là que le référentiel se tient. Les 9 et 10 s'appellent à la main sur l'API. Le 11 se joue dans l'application, sur le calendrier du planning. Le 13 part du back-office puis s'appelle sur l'API : la file de validation existe côté serveur et n'a pas encore d'écran. Le 18 s'appelle sur l'API pour écrire et se vérifie dans l'application pour lire, parce que c'est là qu'il se voit : c'est le premier qui fasse naître une demande « en attente » sans passer par le back-office. Le 19 se joue entièrement dans l'application, côté salarié : c'est le premier écran de Mon espace à lire ses propres demandes. Le 22 aussi : c'est le formulaire qui les dépose.
Pour qui : un compte is_staff disposant des permissions du domaine Absences pour les huit premiers ; un compte du périmètre manager pour les suivants ; le compte du salarié lui-même pour les 18, 19 et 22. Un super-utilisateur a tout.
Prérequis communs : la stack tourne (make up) et un compte administrateur existe (make superuser). En développement, make up exécute seed_all : le référentiel contient donc déjà les 6 catégories, 52 motifs et 15 raccords canoniques.
Le mode d'emploi complet de ces écrans est le guide d'administration ; les scénarios ci-dessous ne recettent que ce que le ticket demandait.
1. Créer un type d'absence de bout en bout, sans intervention technique
Tickets : APAJH-190 · Rôle : Admin · Prérequis : seed de développement
Critères d'acceptation du ticket
- Étant donné un administrateur, quand il paramètre un type d'absence, alors l'unité de saisie, le caractère rémunéré, le plafond annuel, les règles de proratisation, les compteurs associés et les règles d'alerte sont gérables sans solliciter le prestataire.
Les associations de compteurs se gèrent désormais dans l'écran dédié du domaine Compteurs. La proratisation générique et l'assimilation au temps de travail effectif restent hors de ce référentiel.
Connectez-vous au back-office et ouvrez Catégories d'absence.
✅ Résultat attendu : les six catégories canoniques sont présentes.
Cliquez Ajouter catégorie d'absence, saisissez le libellé
Recette, laissez Catégorie parente vide, enregistrez.Ajoutez une seconde catégorie : libellé
Naissance, Catégorie parenteRecette.✅ Résultat attendu : deux lignes, la seconde affichant « Recette » dans sa colonne Catégorie parente.
Ouvrez Types d'absence et cliquez Ajouter type d'absence.
Dépliez la liste Catégorie.
✅ Résultat attendu : « Naissance » s'affiche comme « Recette › Naissance », chemin complet, et non comme « Naissance » seul.
Renseignez Nom
Congé paternité recette, CodePAT_REC, Catégorie « Recette › Naissance », Unité de saisieJour, Rémunérée cochée, Fractionnable décochée. Enregistrez.✅ Résultat attendu : la ligne apparaît dans la liste,
PATen première colonne.
2. Refuser un code déjà porté par un poste
Tickets : APAJH-190 · Rôle : Admin · Prérequis : le scénario 1 joué
Critères d'acceptation du ticket
- Implicite : le référentiel doit rester utilisable une fois rempli en autonomie.
Les badges de la bibliothèque Postes et ceux des absences s'affichent dans la même case du planning : un code ne peut donc vouloir dire qu'une seule chose. La maquette fait exactement cette confusion, en donnant CP au poste « Journée » et au congé payé. Elle est tranchée en faveur des absences — le poste « Journée » se code désormais JO, et CP reste disponible ici.
Ouvrez Types d'absence et cliquez Ajouter type d'absence.
Renseignez Nom
Journée pleine, CodeJO, une Catégorie quelconque, et enregistrez.✅ Résultat attendu : l'enregistrement est refusé, avec un message sous le champ Code disant que « JO » est déjà le code d'un poste de la bibliothèque et que les deux badges s'affichent dans la même case du planning.
Reprenez l'étape 2 avec le code en minuscules,
jo.✅ Résultat attendu : refusé également — la vérification ignore la casse.
Vérifiez la ligne canonique
Congés annuels, codeCP.✅ Résultat attendu : elle existe et est active.
CPn'est pas un code de poste — c'est le poste « Journée » qui porteJO.
3. Plafonner un type sur l'année civile
Tickets : APAJH-190 · Rôle : Admin · Prérequis : le scénario 1 joué
Critères d'acceptation du ticket
- Étant donné un administrateur, quand il paramètre un type d'absence, alors les plafonds annuels sont gérables sans solliciter le prestataire.
Seul le plafond annuel est livré, sur l'année civile (1er janvier – 31 décembre). Mensuel et trimestriel ne sont pas construits : aucun type n'en a exprimé le besoin.
Ouvrez Types d'absence et créez
Enfant malade, codeENF, unitéJour.Saisissez Plafond annuel
3et Dépassement du plafondRefuser. Enregistrez.✅ Résultat attendu : la ligne affiche
3.00dans sa colonne Plafond annuel.Modifiez la ligne, remplacez le plafond par
0et enregistrez.✅ Résultat attendu : refusé — un plafond doit être strictement positif. Interdire l'absence entièrement se fait en décochant Actif.
Videz le plafond et enregistrez.
✅ Résultat attendu : accepté — vide signifie « pas de plafond ».
Le contrôle du plafond à la saisie se recette au scénario 6.
4. Retirer un type sans perdre l'historique
Tickets : APAJH-190 · Rôle : Admin · Prérequis : le scénario 1 joué
Critères d'acceptation du ticket
- Étant donné un administrateur, quand il fait évoluer le référentiel, alors il n'a pas besoin du prestataire.
Ouvrez Types d'absence et décochez Actif sur
Congé paternité recette, directement dans la liste. Enregistrez.✅ Résultat attendu : la ligne reste visible au back-office, case Actif décochée.
Interrogez l'API :
GET /api/absence-types/avec un compte authentifié.✅ Résultat attendu :
Congé paternité recetten'apparaît pas dans la réponse — un type retiré n'est plus proposé, tout en restant lisible au back-office.Tentez
POST /api/absence-types/avec le même compte.✅ Résultat attendu :
405 Method Not Allowed. Ce référentiel ne s'écrit qu'au back-office.Ouvrez Catégories d'absence et tentez de supprimer
Familiale.✅ Résultat attendu : refusé — elle a une sous-catégorie. Même refus sur
Naissance, qui classe un type.
5. Enregistrer une absence, à la journée et à la demi-journée
Tickets : APAJH-190 · Rôle : Admin · Prérequis : le scénario 1 joué, un salarié existant, et son lieu de travail principal renseigné sur un établissement en Jours ouvrables (lundi au samedi)
Critères d'acceptation du ticket
- Étant donné un type d'absence paramétré, quand une absence est enregistrée sur ce type, alors l'unité de saisie et les règles paramétrées s'appliquent.
Ce scénario est la contrepartie des quatre précédents : il vérifie que le paramétrage change quelque chose. Le décompte lit la Base de décompte de l'établissement du salarié, et exclut les dimanches et les jours fériés.
Prérequis de données : vérifiez que le 15 août de l'année en cours figure bien dans les jours fériés. Si l'année de recette n'est pas 2026, adaptez les dates ci-dessous en gardant la forme du cas : un mercredi au jeudi de la semaine suivante, avec un férié tombant un samedi.
Ouvrez Absences, cliquez Ajouter absence, choisissez un salarié et le type
Congé paternité. Saisissez Premier jour12/08/2026et Dernier jour20/08/2026, laissez À partir deMatinet Jusqu'àAprès-midi. Enregistrez.✅ Résultat attendu : la ligne est enregistrée. La période couvre neuf jours calendaires mais n'en décompte que sept : le dimanche 16 et le samedi 15, qui est l'Assomption, ne comptent pas.
Supprimez cette ligne, puis créez-en une autre sur le même salarié : Premier jour et Dernier jour au
12/08/2026, À partir deMatin, Jusqu'àMatin.✅ Résultat attendu : accepté — c'est une absence d'une demi-journée.
Modifiez-la : À partir de
Après-midi, Jusqu'àMatin, même date.✅ Résultat attendu : refusé — une absence ne peut pas commencer l'après-midi et finir le matin du même jour, elle ne durerait rien.
Reposez À partir de
Matin/ Jusqu'àMatin, enregistrez, puis créez une seconde absence sur le même salarié et la même date, d'un autre type, avec À partir deAprès-midiet Jusqu'àAprès-midi.✅ Résultat attendu : accepté. Deux demi-journées du même jour ne se chevauchent pas — un matin de congé et un après-midi d'enfant malade sont deux absences valables.
Tentez une troisième absence sur ce salarié, du
12/08/2026au13/08/2026.✅ Résultat attendu : refusé, avec un message nommant l'absence qui tient déjà ces jours et son numéro.
6. Faire jouer les règles du type sur une absence
Tickets : APAJH-190 · Rôle : Admin · Prérequis : les scénarios 1 et 3 joués
Critères d'acceptation du ticket
- Étant donné un administrateur, quand il paramètre les règles d'alerte et de blocage d'un type d'absence, alors elles s'appliquent sans intervention du prestataire.
Trois des cinq colonnes du référentiel se prouvent ici : le plafond annuel, le caractère fractionnable, et les types de contrat concernés. La quatrième, les populations, ne contrôle rien par décision ; la cinquième, l'unité, se recette au scénario 5.
Sur le type
Enfant malade(plafond3, Refuser), enregistrez une absence de cinq jours décomptés — du12/08/2026au18/08/2026.✅ Résultat attendu : refusé, avec un message chiffré : cinq jours demandés portent le total de l'année à cinq pour un plafond de trois.
Ramenez la période à deux jours décomptés (
12/08au13/08) et enregistrez. Puis créez une seconde absence du même type, en septembre, de deux jours.✅ Résultat attendu : la seconde est refusée — le plafond se compte sur l'année civile entière, pas par absence.
Passez Dépassement du plafond à
Avertirsur le type, et rejouez l'étape 2.✅ Résultat attendu : la seconde absence est enregistrée, et un bandeau orange annonce le dépassement. C'est le paramétrage qui décide, pas le code.
Sur
Congé paternité(Fractionnable décoché), enregistrez deux absences de la même année séparées par un trou : mars, puis août.✅ Résultat attendu : les deux sont enregistrées, et la seconde affiche un bandeau orange disant que ce type doit être pris en une seule fois. Un avertissement, pas un refus — la fenêtre légale est souvent l'événement, pas l'année.
Recommencez avec deux périodes contiguës (du 3 au 7 août, puis du 8 au 11).
✅ Résultat attendu : aucun avertissement — c'est une seule période continue saisie en deux lignes.
Sur
Congé paternité, renseignez Types de contrat concernés =CDIseulement. Enregistrez une absence pour un salarié dont le contrat en vigueur au premier jour est unStagiaire.✅ Résultat attendu : refusé, le message nommant le type de contrat réellement détenu. Videz la liste et rejouez : accepté — vide veut dire « tous les contrats », pas « aucun ».
Couverture automatique : backend/tests/test_absences.py (26 assertions) et backend/tests/absences/test_absences_declaration.py (38 assertions) couvrent ce chapitre côté serveur — le troisième niveau refusé, la collision de codes (les cinq codes de poste et la casse), CP resté disponible, le décompte ouvrable et ouvré, les fériés, les quatre formes de demi-journée, les six refus, les deux avertissements, et les filtres de /api/absences/. Ce qui reste à rejouer à la main est ce que seul l'écran montre : le chemin complet dans la liste Catégorie, l'édition en ligne de Actif, et les bandeaux orange des étapes 3 à 5 du scénario 6 — que seul le back-office affiche.
7. Décompter une absence de son compteur
Tickets : APAJH-55.3 · Rôle : Admin / RH n1 · Prérequis : la matrice absences-compteurs, un salarié avec CP ouvert et au moins 3 j restants
Créez une absence CP future de
1,5 j.✅ Résultat attendu : l'absence est Validée, version
1. Le compteur CP réserve1,5 jdans Planifiés ; le journal conserve la référence de l'absence et sa version.Modifiez la fin pour porter la demande à
2 j.✅ Résultat attendu : l'absence passe en version
2. Une compensation de1,5 jet le nouveau mouvement de2 jsont visibles ; le solde ne porte qu'une réservation nette de2 j.Faites passer ces
2 jdans Pris, puis ramenez la fin de l'absence au premier jour pour simuler un rappel du salarié le deuxième jour.✅ Résultat attendu : les
2 jpris sont compensés et remplacés par1 jpris. Le jour non pris est restitué sans supprimer l'événement ni les mouvements précédents du journal.Essayez une durée supérieure au restant, puis une association dont l'unité ne correspond pas.
✅ Résultat attendu : chaque écriture est refusée intégralement. Aucun compteur secondaire n'est essayé et aucun mouvement partiel ne reste.
Supprimez l'absence.
✅ Résultat attendu : une version d'annulation compense le
1 jnet, puis la ligne disparaît. Le journal, l'auteur et les mouvements demeurent en lecture seule.Rejouez avec CET, Enfant malade, AEH/AID, délégation et un motif sans compteur.
✅ Résultat attendu : CET et les droits spéciaux suivent les mêmes garanties. Le motif sans compteur crée seulement son événement audité.
8. Sécuriser le raccord entre catalogue et motifs
Tickets : APAJH-55.3 · Rôle : Admin / RH n1 · Prérequis : make seed-absence-referential
Vérifiez les listes : 6 catégories, 52 motifs et 15 associations canoniques sont présentes. Relancez la commande.
✅ Résultat attendu : aucun doublon ; un champ canonique modifié est restauré, tandis qu'un code CEGI, plafond, contrat, population ou motif local est conservé.
Créez un type de compteur consommable et tentez de l'activer sans raccord.
✅ Résultat attendu : activation refusée, avec un message demandant un motif d'absence actif.
Laissez ce compteur inactif, créez son association active vers un motif actif, puis activez-le. Tentez ensuite de désactiver ou supprimer cette association, puis de désactiver son motif.
✅ Résultat attendu : la préparation puis l'activation réussissent ; les trois opérations qui rendraient le compteur orphelin sont refusées.
Vérifiez AN et HS, puis les associations de temps.
✅ Résultat attendu : AN et HS sont non consommables et sans motif ;
realized_time → ANest actif,recovery_earned → RCetovertime_approved → HSrestent inactifs. Aucun motifRHSn'existe et COR reste sans compteur.Vérifiez les motifs de délégation et de représentation syndicale.
✅ Résultat attendu : DLG_CSE, DLG_SEC et DLG_TRE consomment DLG_CSE ; DLG_DS consomme DLG_DS et CNS_SAL consomme DLG_CS. SYN_PAR, SYN_CON et SYN_ELU sont rémunérés mais sans compteur. SYN_ELU bloque au-delà de 10 jours ouvrables annuels ; le plafond de SYN_CON reste documentaire.
Déclarez successivement SYN_PAR, SYN_CON et SYN_ELU pour un salarié annualisé.
✅ Résultat attendu : aucune consommation de compteur n'est créée et chaque absence rémunérée neutralise son temps théorique dans l'annualisation. Une onzième journée SYN_ELU sur l'année est refusée.
Déclarer une absence depuis le planning
Le parcours de déclaration dans l'application se construit ; ce que le serveur lui doit se recette dès maintenant, par l'API, avec un compte du périmètre manager. Le scénario écran arrivera avec le parcours.
9. Servir au planning ce qu'il faut pour déclarer et corriger une absence
Tickets : APAJH-252 · Rôle : Manager n1 · Prérequis : seed de développement, un salarié planifié sur une trame, un jeton d'un compte Manager n1
Trois manques côté serveur, sans lesquels l'écran ne peut pas être écrit : le motif ne dit pas quel compteur il consomme, l'écriture ne rend pas ses avertissements, et la case du planning ne dit pas quelle absence la couvre — son numéro n'était lisible que dans poste.id, une clé documentée comme opaque.
GET /api/absence-types/.✅ Résultat attendu : chaque motif porte une liste
counter_codes.Congés annuels(CP) porte["CP"]; un motif sans compteur —SYN_PAR, par exemple — porte une liste vide. C'est la clé de jointure avecGET /api/counters/?employee=&year=, qui répond par code : de quoi afficher un solde en regard d'un motif.Déclarez une absence :
POST /api/absences/sur un motif non fractionnable dont le salarié a déjà posé une période la même année, séparée par un trou.✅ Résultat attendu :
201, et la réponse portewarnings— la phrase disant que ce type doit être pris en une seule fois. Elle n'était jusqu'ici affichée qu'au back-office.Refaites une déclaration ordinaire, puis relisez la liste
GET /api/absences/.✅ Résultat attendu : la déclaration ordinaire répond
"warnings": []; les lignes de la liste portent"warnings": null. Nul, et non liste vide : « rien à signaler » et « pas mesuré » ne se confondent pas — la liste ne paie pas trois requêtes par ligne pour une réponse que personne ne lit.Relisez le planning sur la période de l'absence :
GET /api/planning/?from=&to=.✅ Résultat attendu : la case du jour absent porte un objet
absence— son numéro, son type, ses dates, ses demi-journées et son commentaire. Une journée travaillée porte"absence": null.Déclarez une demi-journée (matin seul) sur un jour travaillé, et relisez la même case.
✅ Résultat attendu : la case porte deux blocs ; seul celui de l'absence renseigne
absence, le poste resté à travailler à côté d'elle portenull. C'est ce qui empêchera « Modifier l'absence » de s'ouvrir sur la demi-journée travaillée.
10. Calculer une absence avant de l'enregistrer
Tickets : APAJH-252 · Rôle : Manager n1 · Prérequis : le scénario 9 joué, un salarié dont le lieu de travail principal compte en Jours ouvrables et qui dispose d'un compteur CP ouvert
L'écran doit afficher la durée pendant la saisie. Le décompte réel — jours ouvrables ou ouvrés selon l'établissement, demi-journées retirées, fériés exclus — est celui qui sert le plafond annuel : le refaire côté client ferait compter l'écran autrement que la règle. D'où POST /api/absences/preview/, qui prend la même charge qu'une création et n'enregistre rien.
POST /api/absences/preview/avec la charge d'une absence de neuf jours calendaires enjambant un dimanche et un jour férié.✅ Résultat attendu :
200,quantityvaut le nombre de jours décomptés (sept sur l'exemple du scénario 5) etunitl'unité du motif. RelisezGET /api/absences/: aucune ligne n'a été créée, et aucun mouvement de compteur non plus.Rejouez la même charge sur un motif consommant un compteur —
CPpar exemple.✅ Résultat attendu :
counter_effectsnomme le compteur, le sens et la quantité — de quoi écrire « réserve 7 jours sur CP » au récapitulatif. Le solde du salarié, lui, n'a pas bougé.Rejouez sur un motif non fractionnable dont le salarié a déjà posé une période la même année.
✅ Résultat attendu :
warningsporte la même phrase que rendrait lePOST— l'avertissement se lit avant d'enregistrer, et « Enregistrer » reste possible.Rejouez sur une période qui chevauche une absence existante.
✅ Résultat attendu : toujours
200, eterrorsporte le message sous la clé du champ à corriger. Pas de400: le panneau reste ouvert.quantityest nul — une absence que les règles refusent ne se mesure pas.Rejouez pour un salarié sans compteur ouvert sur le motif choisi.
✅ Résultat attendu :
errorsporte la clécountersavec le message que lePOSTopposerait, etquantityreste renseignée : l'absence est mesurable, c'est l'écriture qui serait refusée.Enfin, omettez le motif dans la charge.
✅ Résultat attendu :
400. Une charge illisible n'a pas de prévisualisation — la frontière entre ce qui se rapporte et ce qui se refuse.
11. Déclarer une absence depuis le calendrier
Tickets : APAJH-252, APAJH-253 · Rôle : Manager n1 · Prérequis : la stack tourne, le référentiel des absences est seedé, et le salarié visé porte un contrat en vigueur à la date choisie
Le premier scénario de ce chapitre qui se joue dans l’application : les dix précédents recettent le référentiel et l’API, celui-ci recette le parcours. Ce qui reste hors de ce lot est dit au dernier point.
Ouvrez Activité ▸ Planning, cliquez un bloc coloré, puis Déclarer une absence au bas du panneau.
✅ Résultat attendu : le panneau garde sa place et change de contenu — un titre Déclarer une absence, un lien Retour en haut, la carte du salarié, un bloc Compteurs du salarié replié, une barre de progression Type · Détails · Récap et la mention Étape 1 sur 3. Suivant est inactif et son infobulle dit « Choisissez un motif d’absence. ».
Dépliez le bloc Compteurs du salarié.
✅ Résultat attendu : une carte par compteur que le salarié détient réellement — code, intitulé, restant en gros, une barre à deux segments (pris, planifiés) sur l’acquis, et la légende Acquis · Pris · Planifiés · Restant. Comparez avec l’onglet Compteurs de sa fiche : ce sont les mêmes chiffres, servis par le même
GET /api/counters/. Le bloc reste replié par défaut à chaque ouverture du parcours, et il reste visible aux trois étapes — il vit à côté du formulaire, pas dedans.Rejouez ce point sur un salarié qui détient beaucoup de compteurs — un effectif de recette en propose selon les droits calculés (CP, CT et compteurs horaires, RC et HS manuels). ANC reste désactivé dans le référentiel.
✅ Résultat attendu : le bloc ne prend jamais plus de sa part du panneau. Les cartes qui n’y tiennent pas défilent dans le bloc — molette ou barre de défilement — et le dernier compteur est atteignable. L’étape garde sa place sous le bloc : la barre Type · Détails · Récap, la mention Étape 1 sur 3 et le formulaire restent visibles sans avoir à replier les compteurs.
Ouvrez Catégorie et choisissez une catégorie qui a des sous-catégories (
Congés familiauxsur le seed).✅ Résultat attendu : un second champ Sous-catégorie apparaît, et aucun motif n’est encore proposé — la catégorie choisie n’en porte pas elle-même.
Choisissez une sous-catégorie.
✅ Résultat attendu : le fil d’Ariane du référentiel s’affiche tel que le serveur le construit (« Famille › Naissance »), suivi des motifs de cette sous-catégorie, chacun avec son code. Suivant s’active dès qu’un motif est coché.
Regardez maintenant les soldes, dans la liste des catégories et en regard des motifs.
✅ Résultat attendu : un motif qui consomme un compteur porte une pastille « 16 j restants », lue sur le compteur que l’intégration débiterait en premier. Un motif qui ne consomme rien, ou dont le compteur n’a pas de ligne ouverte cette année, n’affiche rien — pas « 0 j », qui se lirait comme un droit épuisé. Sur une catégorie, la pastille n’apparaît que si un seul compteur se trouve derrière tous ses motifs : additionner deux compteurs donnerait un chiffre dont on ne peut rien prendre, et un motif qui ne consomme rien disqualifie sa catégorie au même titre qu’un second compteur.
Conséquence à vérifier nommément, parce que c’est celle qui manquait :
Santéne porte aucune pastille. Quatre de ses cinq motifs ne consomment rien et le cinquième débiteSANG; la catégorie affichait le solde du don du sang comme s’il était ce qu’un arrêt maladie irait prendre. Sur le référentiel livré, aucune catégorie ne remplit la condition : la liste déroulante des catégories ne montre donc aucun solde. La pastille reste au motif, où le chiffre nomme bien ce qui sera débité.Revenez à Catégorie et choisissez cette fois une catégorie sans sous-catégorie (
Maladie).✅ Résultat attendu : le champ Sous-catégorie disparaît et les motifs s’affichent directement. Le nombre de niveaux visibles est une conséquence du référentiel, pas une constante de l’écran.
Comparez la liste des motifs avec le back-office, sur un motif dont le champ Types de contrat nomme un contrat que le salarié ne détient pas.
✅ Résultat attendu : ce motif n’est pas proposé. Le filtre est jugé sur le contrat en vigueur à la date de la case cliquée — c’est le seul repère disponible, le motif se choisissant avant les dates. Si l’étape suivante déplace l’absence hors de ce contrat, c’est le serveur qui refuse, avec sa phrase.
Choisissez un motif compté en jours, puis Suivant.
✅ Résultat attendu : Étape 2 sur 3. Une paire Journée entière · Demi-journée, un champ de dates ouvert sur le jour de la case, la ligne Durée de l’absence, un Commentaire, et — si et seulement si le motif remplit « justificatifs attendus » au back-office — Justificatif attendu avec sa prose, puis Pièce justificative et son cadre de dépôt. Un motif qui n’attend aucune pièce n’en montre aucun des deux : la zone de dépôt se lirait comme une exigence que le référentiel n’a pas posée.
Le champ Validation requise est indépendant des deux autres : il suit « validation requise » au back-office, et rien d’autre. Un motif peut demander une signature sans pièce, une pièce sans signature, les deux, ou aucune des deux — une signature n’est pas un papier. Vérifiez les quatre combinaisons sur quatre motifs du référentiel.
Ouvrez le champ de dates, cliquez deux jours, puis Valider.
✅ Résultat attendu : le déclencheur écrit « Du … au … », le mois porte les couleurs des postes du salarié, et Durée de l’absence affiche la valeur rendue par
POST /api/absences/preview/— jours ouvrables ou ouvrés selon l’établissement, dimanches et fériés déduits. Aucun calcul n’est fait côté navigateur : la durée affichée est celle qui sert le plafond annuel.Rouvrez le champ et feuilletez les mois, en avant puis en arrière.
✅ Résultat attendu : chaque mois arrive avec ses couleurs, et la légende sous le mois ne nomme que les teintes qu’il montre réellement. Un mois sans planification reste gris, ce qui est la bonne réponse et non un défaut d’affichage : le calendrier lit le planning du salarié mois par mois, un mois à la fois, et un mois déjà visité revient sans nouvelle requête.
Basculez sur Demi-journée.
✅ Résultat attendu : le champ passe à une seule date et deux choix Matin · Après-midi apparaissent. La demi-journée porte sur un jour, pas sur les deux bornes d’une plage : les bornes partielles d’une période restent saisissables au back-office seulement.
Revenez à l’étape 1 par Précédent, puis repassez à l’étape 2.
✅ Résultat attendu : votre saisie est toujours là — motif, période, dates, commentaire. Retour depuis l’étape 1 rouvre le détail du poste d’où vous êtes parti ; cliquer une autre case, en revanche, abandonne le brouillon : c’est un autre jour.
Reprenez au motif et choisissez cette fois un motif compté en heures.
✅ Résultat attendu : pas de paire de période du tout — une date unique et deux champs Début / Fin. Un motif compté en heures ne se saisit pas autrement : le serveur refuse une telle absence sans heures et refuse une seconde date. La ligne Durée de l’absence disparaît, la durée étant la saisie elle-même. Son solde, s’il en a un, est exprimé en heures — le compteur impose son unité, pas l’écran.
Saisissez une heure de fin antérieure au début, puis Suivant.
✅ Résultat attendu : l’étape ne se quitte pas et le message « L’heure de fin doit être postérieure à l’heure de début. » s’affiche sous le champ Fin. Corrigez : le message s’éteint et Suivant passe.
Sur un motif en jours, passez à Étape 3 sur 3.
✅ Résultat attendu : le récapitulatif reprend salarié, catégorie, sous-catégorie, motif, période, dates, durée et commentaire — une ligne de catégorie par niveau réellement présent.
Sur ce même récapitulatif, lisez la ligne Effet compteur.
✅ Résultat attendu : elle annonce ce que l’enregistrement va bouger — « réserve 3 j sur CA » sur une absence à venir, « décompte 3 j sur CA » sur une absence déjà commencée. C’est la projection de
counter_effects, calculée par les mêmes associations que l’écriture. La ligne disparaît sur un motif qui ne consomme aucun compteur, et sur une période qu’une règle refuse : les mouvements projetés appartiennent alors à un enregistrement qui n’aura pas lieu.Déclarez une période qui déclenche un avertissement non bloquant : un droit non fractionnable déjà posé une fois cette année, ou un plafond annuel dépassé sur un motif réglé en avertir.
✅ Résultat attendu : un bandeau ambre au-dessus du récapitulatif porte la phrase du serveur, lue avant l’enregistrement. Pas de toast : un avertissement se lit. Enregistrer reste actif — c’est ce que « non bloquant » veut dire.
Déclarez maintenant une période qui chevauche une absence existante.
✅ Résultat attendu : le message du serveur s’affiche en rouge et Enregistrer est inactif, son infobulle renvoyant aux points signalés. Le panneau reste ouvert : la prévisualisation répond
200avec le motif du refus, et aucune écriture n’est tentée.Sur une saisie valide, cliquez Enregistrer.
✅ Résultat attendu : le bandeau Absence enregistrée apparaît, le panneau se ferme, et la case du planning porte désormais l’absence — grisée sur une journée entière, en deux blocs sur une demi-journée, avec le poste resté à travailler à côté. Rouvrez le parcours sur une autre case : le bloc Compteurs du salarié a bougé de ce que l’Effet compteur annonçait, et l’onglet Compteurs de la fiche dit la même chose.
Rejouez l’étape 1 sur un téléphone (390 px de large).
✅ Résultat attendu : le panneau prend toute la largeur, le bloc compteurs déplié tient dedans sans déborder latéralement, les trois étapes s’enchaînent, et Enregistrer est atteignable sans zoom.
Cliquez maintenant une case déjà absente — la déclaration que vous venez d’enregistrer fait l’affaire — puis Modifier l’absence au bas du panneau.
✅ Résultat attendu : le bouton dit bien « Modifier l’absence ». Il le lit sur la déclaration servie et non sur le fait que la journée ne soit pas travaillée, donc une case de repos propose « Déclarer une absence » : il n’y a rien à y modifier. Le panneau s’ouvre sous le titre Modifier l’absence — ce sont les mêmes trois étapes, et le titre dit lequel des deux actes on est en train de faire — et le parcours est pré-rempli : le motif coché à l’étape 1, sa catégorie et sa sous-catégorie retrouvées dans le référentiel, et à l’étape 2 la période, les dates, la demi-journée ou les heures, et le commentaire.
Allez jusqu’au récapitulatif sans rien changer.
✅ Résultat attendu : aucun bandeau rouge de chevauchement, et Enregistrer actif. C’est le point le plus important de ce scénario : la prévisualisation d’une correction interroge
POST /api/absences/<n°>/preview/et non celle de la collection, donc la ligne s’exclut elle-même du chevauchement et du plafond annuel. Sur l’endpoint de collection, l’écran s’accuserait du conflit avec l’absence qu’il est en train de modifier.Corrigez la période — un jour de plus, par exemple — puis Enregistrer.
✅ Résultat attendu : le bandeau Absence modifiée, le panneau se ferme, la case du planning suit, et le compteur bouge de la différence et non du total — l’écriture rejoue le décompte sur la même ligne. Vérifiez au back-office, Absences : il y a toujours une ligne pour ce salarié sur cette période, pas deux.
Recliquez la case, et cette fois restez sur le détail du poste : sous Modifier l’absence et Affecter un poste, une troisième commande, Supprimer l’absence. Cliquez-la.
✅ Résultat attendu : une confirmation nomme l’absence par son motif et sa période et annonce que l’action est définitive. Le bouton vit ici et pas dans le formulaire : supprimer une absence n’est pas la corriger, et l’atteindre demandait auparavant de traverser les trois étapes du parcours. Il n’apparaît que sur un jour qui porte une déclaration — un jour travaillé ne l’offre pas. Confirmez : le bandeau Absence supprimée, le panneau se ferme, la case redevient ce que la trame ou la modulation en disait, et l’onglet Compteurs du salarié est recrédité — la suppression annule la ligne et rejoue son décompte à l’envers. Depuis APAJH-273, une absence accordée ainsi supprimée n’est plus effacée : elle reste au dossier du salarié, « Annulée » (scénario 26). Un compteur qui refuserait ce recrédit ferait échouer la suppression, et sa phrase s’afficherait dans le panneau plutôt que de le refermer.
Cas particulier, à saisir au back-office : une absence du mercredi après-midi au vendredi matin — deux demi-journées différentes aux deux bornes d’une plage. Rouvrez-la depuis le planning.
✅ Résultat attendu : pas de formulaire, et aucune commande — la barre du bas disparaît, n’ayant rien à enregistrer ni où revenir. Un bandeau ambre dit que cette absence se corrige au back-office et qu’elle se supprime depuis le détail du poste, où Retour ramène en un clic. Le modèle sait stocker ces bornes, l’écran ne sait pas les saisir — la maquette ne propose la demi-journée que sur un jour — et les rouvrir puis enregistrer remettrait la plage matin→après-midi, transformant une demi-journée travaillée en absence sans que personne l’ait demandé (décision 25).
Reprenez une déclaration sur un motif qui attend un justificatif (
Arrêt maladieen porte un), revenez à l’étape Détails et joignez une pièce, par le cadre en pointillés ou en glissant un fichier dessus, puis Enregistrer.✅ Résultat attendu : le fichier s’affiche sous son nom dans la liste, et l’enregistrement l’emporte. Un fichier au mauvais format est refusé sur place, en nommant le fichier fautif, sans décrocher ceux déjà joints. Le détail de la règle d’accès — qui voit le contenu de la pièce et qui ne voit que son nom — est le scénario 12.
Enfin, constatez ce que ce lot ne couvre pas encore.
✅ Résultat attendu : depuis le bouton flottant, « Déclarer une absence » reste désactivé — il n’y a pas de salarié sélectionné, donc c’est un autre parcours.
Couverture automatique : e2e/absences.spec.ts vérifie l’ouverture du wizard, la descente catégorie → sous-catégorie → motif, la branche sans sous-catégorie, le masquage par contrat, le bloc Compteurs du salarié replié puis déplié et persistant d’une étape à l’autre, les soldes en regard des motifs et des catégories — présents, et absents là où ils ne veulent rien dire —, la plage et la demi-journée, les couleurs du mois feuilleté dans les deux sens — hors jours débordants du mois voisin, qui passaient sur le défaut même —, la forme d’un motif en heures et son heure de fin refusée, le récapitulatif, l’effet compteur et son retrait sous un refus, le bandeau d’avertissement, le refus rendu par la prévisualisation, la charge écrite et le retour sans perte — puis, sur une case dont l’absence est déjà déclarée, le pré-remplissage, la prévisualisation adressée à la ligne elle-même, le PATCH qui corrige au lieu d’écrire une seconde déclaration, la confirmation et le DELETE commandés depuis le détail — que le formulaire, lui, n’offre sur aucune de ses étapes, et qu’un jour travaillé n’offre pas du tout —, et le cas de la décision 25, qui n’ouvre plus rien et renvoie au détail. Le dépôt d’un justificatif a son propre chapitre de specs : le cadre offert par les seuls motifs qui nomment une pièce — et le même écran, sur l’autre motif, qui n’en montre aucun —, Validation requise affiché seul sur un motif qui demande un accord et pas de papier, le corps multipart et sa clé add_documents répétée une fois par fichier, le glisser-déposer sur la zone, la relecture des pièces sur file avec leur date et leur déposant, leur détachement par remove_document_ids, et la pièce servie sans URL qui n’affiche aucun lien. Le tout en 1280 px et en 390 px. La case qui change, elle, ne peut pas y être vérifiée : les tests tournent sur le jeu de démonstration, où la grille relit ses fixtures. Le mouvement réel des compteurs non plus : l’écriture y est interceptée.
12. Déposer un justificatif, et n’en montrer le contenu qu’à qui doit le voir
Tickets : APAJH-252, APAJH-253 · Rôle : Manager n1, puis RH n1 · Prérequis : une absence déclarée (scénario 11), un jeton d’un compte Manager n1 et un jeton d’un compte RH n1
Un arrêt de travail est une donnée de santé. Ce qui se vérifie et ne se déduit pas, c’est qui lit quoi — et cela ne se voit qu’en rejouant le même appel avec deux jetons différents, donc les étapes 1 à 6 se jouent par l’API, comme les scénarios 9 et 10. Les étapes 7 à 9 rejouent la même règle à l’écran, où le manager la rencontre, et l’étape 10 ferme le dossier.
Avec le jeton Manager n1, joignez deux pièces à l’absence :
PATCH /api/absences/<n°>/en multipart, la cléadd_documentsrépétée une fois par fichier (un PDF et une image).✅ Résultat attendu :
200, et la réponse portedocuments— deux entrées, chacune avec son numéro, sonfilename, sa date de dépôt et le nom du compte qui l’a déposée. La clé se répète parce qu’un motif nomme couramment plusieurs justificatifs (« acte de décès et justificatif de lien de parenté ») : les pièces s’ajoutent à celles déjà déposées.Rejouez avec un fichier
.docx, puis avec un PDF de plus de 10 Mo.✅ Résultat attendu :
400les deux fois, sous la cléadd_documents. Le second porte la phrase « Le justificatif ne doit pas dépasser 10 Mo. » — la même que le justificatif d’honorabilité de la fiche salarié, les deux champs partageant leurs limites. Relisez l’absence : aucune pièce n’a été ajoutée.Relisez l’absence avec le jeton Manager n1 :
GET /api/absences/<n°>/.✅ Résultat attendu : les deux pièces sont là — numéro,
filename, date, compte déposant — et aucune n’a de cléfile. Pas une URL vide : la clé est absente, pour qu’un client ne confonde pas « masqué » et « pas de fichier ».Rejouez le même appel avec le jeton RH n1.
✅ Résultat attendu : la même réponse, plus une clé
filepar pièce — l’URL de la route protégée. Appelez-la avec le même jeton : le fichier se télécharge. C’est là, et nulle part ailleurs, que la règle se vérifie : le nom et la date des deux côtés, l’URL d’un seul.Notez l’asymétrie et vérifiez qu’elle est bien celle-là : le compte Manager n1 a déposé la pièce à l’étape 1 et ne peut pas la relire.
✅ Résultat attendu : c’est voulu. Le manager est celui qui tient le papier, donc il dépose ; relire un arrêt de travail n’est pas son métier. L’inverse — exiger le droit de lecture pour pouvoir déposer — ouvrirait tous les certificats médicaux à tout l’espace manager, le périmètre de l’API s’arrêtant à l’établissement.
Détachez une pièce :
PATCH /api/absences/<n°>/avecremove_document_ids: [<n° de la pièce>]. Puis rejouez avec le numéro d’une pièce appartenant à une autre absence.✅ Résultat attendu : la première réponse
200et ne garde qu’une pièce ; la seconde400, « Ces justificatifs n’appartiennent pas à cette absence : … ». Refusé et non ignoré : ignorer laisserait croire que la pièce a été retirée.Passez à l’écran. Ouvrez
/planningavec un compte Manager n1, cliquez la case de l’absence déclarée, Modifier l’absence, puis Suivant jusqu’à l’étape Détails.✅ Résultat attendu : sous « Pièce justificative », un cadre en pointillés « Glissez-déposez ou parcourez · PDF, JPG, PNG — 10 Mo max ». Au-dessus, les pièces déjà déposées : chacune sous son nom, avec sa date de dépôt et le compte déposant (02/09/2026 · Léa Martins), et aucun lien « Consulter » — c’est la règle de l’étape 3, vue par le manager. Pas un lien grisé ni un lien mort : pas de lien.
Le champ est là même si le motif de cette absence n’attend rien — c’est le cas de
Congés annuels, celui du planning de démonstration. Une pièce déjà au dossier rouvre le champ à elle seule, sans quoi elle deviendrait invisible et donc indétachable.Rouvrez à l’inverse une déclaration
Congés annuelsà laquelle rien n’a été joint : ni « Justificatif attendu », ni cadre de dépôt. Le dépôt appartient aux motifs qui nomment une pièce ; l’offrir partout faisait lire le cadre comme une exigence que le référentiel n’a pas posée.Déposez une pièce, par le cadre puis en glissant-déposant un fichier dessus. Essayez aussi un
.docx. Puis retirez une pièce déjà déposée par sa croix, et Enregistrer.✅ Résultat attendu : chaque fichier accepté s’ajoute à la liste sous son nom. Le
.docxest refusé avant tout aller-retour, avec « « notes.docx » : Le justificatif doit être un fichier PDF, JPG ou PNG. » — et ne décroche pas les pièces déjà jointes. Après Enregistrer : « Absence modifiée », et en rouvrant le parcours la pièce retirée n’est plus là et les nouvelles y sont.Rouvrez le même parcours avec un compte RH n1.
✅ Résultat attendu : les mêmes pièces, plus un lien Consulter sur chacune. Ouvrez-le : le fichier s’affiche dans un nouvel onglet. Le lien porte l’adresse publique du stockage objet — servi sur son nom interne au réseau Docker, il ne s’ouvrirait pas.
Enfin, supprimez l’absence :
DELETE /api/absences/<n°>/, ou Supprimer l’absence au détail du poste, dans le planning.✅ Résultat attendu :
204, et les pièces partent avec elle. Une pièce n’a pas d’existence hors de l’absence qu’elle justifie, et annuler une absence ne doit pas laisser un certificat médical en base. Il n’y a aucune purge par ailleurs (décision 24) : la durée légale de conservation n’est pas connue, et l’inventer serait pire que son absence.
Ce que la traçabilité dit et ne dit pas. La colonne « déposé par » répond à « qui a mis ce papier là » et à rien d’autre. Le téléchargement n’est pas enregistré dans la piste d’audit, même s’il passe désormais par l’API protégée. Les écritures d’absence sont tracées : déclaration, demande, correction, retrait et décisions. La consultation reste distincte de ces écritures.
Décider d'une demande d'absence
13. Valider et refuser une demande, et n'ouvrir la décision qu'à qui la porte
Tickets : APAJH-42 · Rôle : Admin au back-office, puis Manager n1 et DA sur l'API · Prérequis : la stack tourne, un salarié rattaché à un établissement, un compteur CP ouvert pour lui sur l'année de la demande, un jeton d'un compte Manager n1 et un jeton d'un compte DA
Ce lot est entièrement serveur : la file de validation du manager n'a pas encore d'écran, donc les étapes 2 à 9 s'appellent à la main sur l'API, comme les scénarios 9 et 10. L'étape 1, elle, se joue au back-office — et ce n'est pas un détour.
Sur une base neuve, la file est vide, et c'est normal. Rien dans l'application ne crée de demande « en attente » : le parcours par lequel un salarié déposera la sienne n'est pas construit, et une absence déclarée par un manager est validée d'emblée. La première étape de ce scénario est donc d'en fabriquer une au back-office. Un recetteur qui la saute trouve une file vide et croit à une régression.
Connectez-vous au back-office, ouvrez Absences et enregistrez une absence pour le salarié : motif Congés annuels, une période de trois jours à venir. Rouvrez la ligne, réglez l'État technique sur En attente, enregistrez.
✅ Résultat attendu : la ligne porte En attente, et le filtre État technique de la liste permet de la retrouver. Motif du refus, Décidé par et Décidé le sont affichés en lecture seule : ces trois colonnes sont écrites par la décision, et une signature saisie à la main serait une signature que personne n'a donnée.
Avec le jeton Manager n1, listez la file :
GET /api/absences/?status=pending.✅ Résultat attendu :
200, et la demande y est — avec sonstatusàpending, saversionà1, soncounted_quantity(« 3.00 » sur trois jours ouvrables) et soncounted_unit(day). Le décompte est celui du serveur, le même que celui qu'un plafond annuel comparerait : un client qui le recalculerait compterait autrement que la règle.Rejouez avec
?status=validated, puis sans paramètre, puis avec?status=en-cours.✅ Résultat attendu : la demande n'apparaît que dans le premier appel ; sans paramètre la liste rend les quatre états ; l'état inconnu répond
400en nommant le paramètre. Un filtre muet aurait rendu une liste vide, ce qui se lit « rien en attente ».Avec le jeton DA, relisez la demande :
GET /api/absences/<n°>/. Puis tentez de la valider :POST /api/absences/<n°>/validate/avec{"version": 1}.✅ Résultat attendu :
200à la lecture,403à la validation, et le message « Votre rôle ne permet pas de valider ou refuser une demande d'absence. » C'est là que se vérifie la raison d'être des deux permissions : un directeur adjoint ouvre une demande pour savoir où en est son établissement, et n'accorde pas les congés. À l'écran, ce sera un panneau sans pied d'action — pas des boutons grisés.Avec le jeton Manager n1, refusez sans motif :
POST /api/absences/<n°>/refuse/avec{"version": 1, "reason": ""}.✅ Résultat attendu :
400sous la cléreason, et la demande est toujours en attente. Un refus dont le salarié ne peut pas lire la raison n'est pas un refus.Refusez avec un motif :
{"version": 1, "reason": "Effectif insuffisant cette semaine."}.✅ Résultat attendu :
200,statusàrefused,decision_reasonportant la phrase telle quelle, etversionà2. Relisez le compteurCPdu salarié : rien n'a bougé — ni « Planifiés », ni « Pris ».Rejouez le même appel, à l'identique.
✅ Résultat attendu :
409, « Cette demande a déjà été tranchée par quelqu'un d'autre. Rechargez la file avant de recommencer. » C'est le cas de deux valideurs sur la même file : la version envoyée est celle lue avant la première décision. Un400« cette demande n'est plus en attente » aurait été une énigme à résoudre ; ceci est une information.Refabriquez une demande en attente (étape 1) sur une période de dix jours, et accordez-en cinq :
POST /api/absences/<n°>/validate/avec{"version": 1, "date_end": "<le cinquième jour>", "reason": "Accordé sur cinq jours, effectif limite en fin de mois."}. Rejouez d'abord sans la cléreason.✅ Résultat attendu : sans motif,
400sous la cléreason— un salarié qui lit « validée » sur une demande qu'il ne reconnaît plus a droit à la phrase qui l'explique, et la règle est côté serveur parce qu'une règle que seul le formulaire applique n'est pas une règle. Avec le motif :200,statusàvalidated,date_endcorrigé,decision_reasonportant la phrase, et le compteurCPréservé sur cinq jours et non dix. Correction et décision dans le même appel, donc un seul mouvement de compteur : deux requêtes en auraient écrit deux pour un seul geste, et auraient laissé entre elles une demande raccourcie que personne n'a accordée.Refabriquez une demande de dix jours et tentez de la requalifier en heures de délégation :
{"version": 1, "type": <n° du motif DLG_CSE>, "reason": "…"}.✅ Résultat attendu :
400, « Ce type se saisit en heures : renseignez une heure de début et une heure de fin. » et « Une absence en heures se saisit sur une seule date. » La demande reste en attente. Requalifier change le compteur débité, donc toutes les règles de la déclaration repassent sur le couple (nouveau motif, nouvelle période) : accorder ne doit pas pouvoir écrire une ligne que la déclaration aurait refusée.Ouvrez le Journal des actions au back-office et filtrez sur l'absence refusée à l'étape 6.
✅ Résultat attendu : une ligne Modification portant le compte qui a tranché, son identité figée, l'heure, et les valeurs avant/après de
status,decision_reason,decided_byetdecided_at. Les deux décisions sont les seules écritures d'absence tracées : déclarer, corriger et supprimer ne le sont pas encore.
Le périmètre, pendant qu'on y est. Avec un jeton d'un compte Manager n2 dont le rôle porte un autre établissement que celui du salarié,
GET /api/absences/ne rend pas la demande etGET /api/absences/<n°>/répond404— « introuvable » et non « interdit », pour ne pas confirmer l'existence d'un dossier. Deux limites connues : un salarié sans lieu de travail principal devient invisible à un encadrant restreint, et la déclaration n'est pas filtrée — unPOSTpour un salarié hors périmètre est accepté, et la ligne créée n'est ensuite plus lisible par son auteur.
14. Lire la file des demandes du périmètre
Tickets : APAJH-42 · APAJH-298 · Rôle : Manager n1 · Prérequis : le scénario 13 joué jusqu'à l'étape 1 — donc au moins une demande en attente fabriquée au back-office —, et un salarié rattaché à un établissement, à une équipe et porteur d'un contrat
La file est vide sur une base neuve, et c'est normal — même raison qu'au scénario 13 : rien dans l'application ne crée de demande « en attente ». Un recetteur qui ouvre l'onglet sans avoir joué l'étape 1 voit « Aucune demande en attente sur cette période » et croit à une régression.
Connectez-vous au SPA avec le compte Manager n1 et ouvrez Activité ▸ Demandes.
✅ Résultat attendu : l'écran s'ouvre sur À valider — la file existe pour les demandes que personne n'a encore tranchées — et sur l'année en cours avec Tous les mois : une demande pour le mois prochain est justement celle qu'il faut traiter aujourd'hui, et se limiter au mois courant la cacherait.
Lisez la ligne de la demande fabriquée à l'étape 1 du scénario 13.
✅ Résultat attendu : le nom du salarié, sa fonction (elle vient de son contrat, pas de sa fiche), son établissement principal, son ou ses équipes, puis le motif, la période et le décompte en une phrase — « Congés annuels · 15 → 26 juin · 10 j ». Le décompte est celui que le serveur a servi ; il n'est pas recalculé à l'écran. Et le badge En attente en ambre.
Basculez sur Validées, puis sur Refusées.
✅ Résultat attendu : la liste change à chaque fois, et la demande en attente disparaît. Si un état n'a rien, le message le nomme — « Aucune demande validée sur cette période » et non « Aucun résultat » : une file vide qui ne dit pas ce qui était demandé se lit comme « rien en attente » quel que soit l'onglet ouvert.
Revenez sur À valider, choisissez le mois de la demande, puis un autre mois.
✅ Résultat attendu : la demande est là au premier, absente au second. Une absence qui chevauche le mois choisi est gardée — une course du 28 juin au 3 juillet apparaît en juin et en juillet, décision 5.
Filtrez par établissement, puis par équipe, puis tapez les premières lettres du nom du salarié dans Rechercher un salarié et choisissez-le dans les suggestions.
✅ Résultat attendu : chaque filtre restreint la liste ; la recherche propose au fil de la frappe les salariés que les filtres établissement et équipe laissent, et ne garde que celui choisi. Ces trois filtres ne repartent pas au serveur — il ne les connaît pas — donc la liste se réduit sans attente. Les menus ne proposent que votre périmètre : ses établissements, ses équipes, ses salariés, qu'une demande y soit en attente ou non — jamais un établissement hors périmètre. Les équipes suivent l'établissement choisi. Un établissement coché garde les salariés qui y travaillent (lieu principal ou secondaire) ou sont affectés à l'une de ses équipes ; une équipe cochée, ceux qui y sont affectés.
Avec plus de dix demandes dans la fenêtre, changez de page.
✅ Résultat attendu : dix lignes par page, « Page 1 sur N », et le compte total à gauche. Restreindre un filtre ensuite ne laisse pas la vue sur une page qui n'existe plus.
Rétrécissez la fenêtre du navigateur à la largeur d'un téléphone (moins de 768 px).
✅ Résultat attendu : les deux lignes de filtres s'empilent et chaque contrôle prend la largeur entière ; Fonction, Établissement et Équipe sortent du tableau — c'est du contexte, pas la demande — et il reste qui a demandé, quoi, et l'état. Le tableau défile horizontalement dans son cadre, jamais la page.
Reconnectez-vous avec un compte Service Paie, puis avec un compte Salarié.
✅ Résultat attendu : l'entrée Demandes n'est pas offerte au menu, et saisir
/demandesdans la barre d'adresse renvoie au portail. L'entrée est masquée plutôt que laissée à rebondir sur la garde : un bouton qui ramène à l'accueil se lit comme un menu cassé, pas comme un droit qu'on n'a pas. Avec un compte DA, en revanche, la file s'ouvre : lire et trancher sont deux droits, et c'est le second qui manque au DA.
Couverture automatique : e2e/absences-queue.spec.ts vérifie le filtre par défaut « À valider » sur l'année entière et les trois paramètres envoyés dans l'URL, la ligne complète (fonction issue du contrat, équipes hors affectation close, décompte du serveur), un motif retiré du catalogue encore listé, les cinq filtres dont la fenêtre année/mois, la recherche et ses suggestions, la pagination au-delà de dix demandes, le viewport mobile et ses colonnes retirées, la route refusée à un salarié et l'entrée de menu absente sans le droit.
15. Arbitrer une demande depuis le panneau
Tickets : APAJH-42 · APAJH-291 · Rôle : Manager n1 · Prérequis : le scénario 14 joué — la file est vide sur une base neuve, donc il faut une demande en attente créée au back-office (scénario 13, étape 1).
Sur
/demandes, cliquez la ligne d'une demande.✅ Résultat attendu : le panneau latéral s'ouvre sur « Demande à arbitrer ». Il porte l'identité du salarié, la demande encadrée — période, motif, durée, commentaire — l'effet compteur (« réserve 10 j sur CP »), les justificatifs déposés et, sous « Vérifications », les seuls avertissements : une vérification qui passe n'est pas une information.
Regardez le décompte affiché sous « Durée ».
✅ Résultat attendu : c'est le chiffre du serveur (
counted_quantity), jamais un calcul de l'écran. L'effet compteur vient dePOST /api/absences/<n°>/preview/joué sur la ligne, sans quoi la règle de chevauchement se refuserait elle-même sur l'absence qu'on est en train de trancher.Cliquez ✓ Valider.
✅ Résultat attendu : toast « Demande validée », le panneau se ferme et la ligne quitte la file « À valider ». Aucun motif n'a été demandé : la demande est accordée telle qu'elle a été déposée, et un « oui » n'explique rien.
Sur une autre demande, cliquez ✕ Refuser sans rien saisir, puis ✕ Confirmer le refus.
✅ Résultat attendu : le premier clic révèle « Commentaire du refus (obligatoire) » et change le libellé du bouton ; le second refuse le champ vide, affiche le message sous le champ et y ramène le curseur. Rien n'est parti : un refus dont le salarié ne peut pas lire la raison n'est pas un refus, et le serveur le refuserait de toute façon.
Saisissez un motif, puis confirmez.
✅ Résultat attendu : toast « Demande refusée — la case reste inchangée », la demande passe en Refusées avec le motif, et aucun compteur ne bouge — ni « Planifiés », ni « Pris ».
Rouvrez une demande en attente et cliquez ✎ Ajuster avant validation.
✅ Résultat attendu : un volet s'ouvre, pré-rempli avec ce qui a été demandé — le motif et la période. Il n'y a rien à re-saisir pour accorder ce qui a été demandé.
Changez le motif pour un motif compté en heures (heures de délégation).
✅ Résultat attendu : deux champs d'heures apparaissent et la période s'effondre sur une date. Sans eux le serveur refuserait la ligne (« Ce type se saisit en heures… ») sur une erreur que l'appelant n'aurait aucun moyen de satisfaire — c'est pourquoi les heures voyagent avec l'ajustement.
Cliquez ✓ Valider sans rien saisir de plus.
✅ Résultat attendu : « Expliquez ce qui est accordé… » sous un champ Motif de la décision (obligatoire), et rien n'est envoyé. Un salarié qui lit « validée » sur une demande qu'il ne reconnaît plus a droit à la phrase qui l'explique. Saisissez-la : la demande est accordée requalifiée, en un seul appel — donc un seul mouvement de compteur.
Ouvrez le volet et refermez-le sans rien changer, puis validez.
✅ Résultat attendu : aucun motif n'est réclamé et la charge ne porte aucun ajustement. Un volet ouvert mais intouché n'est pas un ajustement : accorder la période qu'on vient de lire écrirait une version et un mouvement de compteur pour un geste qui n'a rien changé.
Sur une demande en heures (par exemple 2 h de délégation), ouvrez le volet et descendez jusqu'à « Poste du … ».
✅ Résultat attendu : le créneau du jour est proposé, pré-rempli avec les heures du poste. Une absence en heures est une tranche de journée, jamais la journée : le poste reste donc à refaçonner sur les heures réellement travaillées. Sur une absence à la journée pleine, ce bloc est absent — il n'y a plus de poste à refaçonner.
Ramenez le début du créneau de 07:00 à 09:00, l'absence valant 2 h.
✅ Résultat attendu : « Heures théoriques 7h », « Heures saisies 5h », « Écart compteur 2h ». Les deux sont nets : la pause non rémunérée du poste tient toujours dans le créneau raccourci, elle reste déduite — un créneau ramené en deçà de sa pause la perd (APAJH-291). L'écart égale la durée de l'absence, donc rien n'est signalé. Mettez 09:30 : l'écart passe à 2 h 30 et un avertissement apparaît — c'est une erreur de saisie, et c'est le seul endroit de l'application où quelqu'un peut la voir. Avertissement, pas blocage : un manager peut avoir une raison.
Validez.
✅ Résultat attendu : la modulation part avant la décision. L'ordre inverse laisserait une absence accordée sur une journée jamais refaçonnée, qui pèse le poste entier plus l'absence dans le total de la semaine. Si la modulation passe et que la décision échoue, la journée est refaçonnée et la demande est encore dans la file : un état lisible, qu'on rejoue.
Reconnectez-vous avec un compte DA et rouvrez une demande.
✅ Résultat attendu : le panneau s'ouvre et se lit — identité, période, motif, justificatifs, effet compteur — et il n'a pas de pied. Pas de bouton grisé, pas d'infobulle « vous n'avez pas le droit », rien : lire et trancher sont deux droits, et le DA n'a que le premier.
Avec deux navigateurs, ouvrez la même demande et validez-la dans le premier, puis dans le second.
✅ Résultat attendu : le second reçoit « Cette demande a déjà été tranchée par quelqu'un d'autre. Rechargez la file avant de recommencer. », ses boutons disparaissent — il n'y a rien à réessayer — et la file se recharge sans qu'on ait à le demander.
Rétrécissez à la largeur d'un téléphone et rouvrez une demande.
✅ Résultat attendu : le panneau prend la hauteur de l'écran, ses deux commandes restent en bas, et la page ne déborde pas latéralement.
Sur une demande en heures dont la journée est coupée (deux créneaux, le second hors des heures de l'absence), refaçonnez le premier créneau comme à l'étape 11, puis validez.
✅ Résultat attendu : au planning, la journée garde ses deux créneaux — le premier refaçonné, le second tel qu'il était, avec son poste et ses options. La modulation remplace toute la journée : avant APAJH-291, elle ne portait que le créneau refaçonné et effaçait l'autre en silence.
Sur une demande en heures dont les heures tombent dans le second créneau de la journée (par exemple 2 h de délégation de 19:00 à 21:00 sur une journée 07:00 → 11:00 puis 17:00 → 21:00), cliquez Ajuster avant validation.
✅ Résultat attendu : le volet propose de refaçonner 17:00 → 21:00, le créneau que l'absence recoupe, et non le premier de la journée. Validé, le matin repart intact. Sur une demi-journée, le volet propose de même le créneau que la demi-journée coupe : le premier pour un matin, et pour un après-midi celui où commence la seconde moitié du temps travaillé (APAJH-289).
Couverture automatique : e2e/absences-queue.spec.ts vérifie l'ouverture du panneau sur la ligne cliquée, l'effet compteur et les seuls avertissements, la validation sèche et sa charge, le refus sans motif puis avec, le volet d'ajustement pré-rempli, la requalification vers un motif horaire et ses deux heures, le motif de décision exigé dès qu'un champ bouge, le volet intouché qui n'ajuste rien, la modulation du poste et son écart signalé, le second créneau d'une journée coupée renvoyé intact, le créneau que l'absence recoupe choisi plutôt que le premier, l'ordre des deux écritures, le 409, l'absence de pied sans le droit de trancher et le viewport mobile.
16. Retrouver une demande dans son planning
Tickets : APAJH-42 · Rôle : Manager n1 · Prérequis : le scénario 14 joué — au moins une demande en attente dans la file, et le salarié concerné visible sur le planning du périmètre.
Sur
/demandes, état « À valider », regardez la colonne de droite d'une ligne.✅ Résultat attendu : « Voir au planning › ». Trancher une demande demande souvent de regarder la semaine autour d'elle — qui d'autre est posé, qui couvre — et c'est le seul chemin qui y mène sans refaire la recherche à la main.
Basculez sur « Validées », puis sur « Refusées ».
✅ Résultat attendu : plus aucun lien sur ces lignes. Une demande déjà tranchée n'a plus de contexte à aller vérifier.
Revenez sur « À valider » et cliquez le lien.
✅ Résultat attendu : le calendrier s'ouvre sur la semaine qui contient le premier jour de la demande, la ligne du salarié détachée, et le panneau d'arbitrage rouvert sur cette demande — même identité, même récapitulatif, mêmes commandes qu'ici. Le lien porte les deux moitiés du geste : le contexte et la décision. Sans la seconde il déposerait un valideur dans la semaine en lui retirant ce qu'il venait faire — le panneau est modal, donc aucun lien n'est atteignable une fois ouvert, et revenir ici perdrait ses filtres de la file. Ce que la grille fait de la semaine et ce qui se passe une fois la décision prise sont le scénario 24 de Planning manager — un écran, un chapitre.
Revenez sur
/demandeset refaites-le au clic du milieu (ou Ctrl+clic).✅ Résultat attendu : le planning s'ouvre dans un nouvel onglet et la file reste où elle était. C'est une navigation, pas une commande — d'où un vrai lien, dont l'URL se copie et se partage.
Rétrécissez à la largeur d'un téléphone.
✅ Résultat attendu : la colonne du lien reste, quand la fonction, l'établissement et l'équipe disparaissent. Une colonne qui tombe est du contexte dont le lecteur peut se passer ; une commande qu'un téléphone ne peut pas atteindre est une commande absente. Elle se rejoint en faisant défiler le tableau, qui défile dans son cadre sans déborder la page.
Couverture automatique : e2e/absences-queue.spec.ts vérifie le lien sur les seules demandes en attente, l'URL qu'il porte — semaine, salarié et demande —, le clic et la touche Entrée qui naviguent au lieu d'arbitrer sur place, le panneau rouvert à l'arrivée, et le lien atteignable à 390 px. Ce que la grille en fait à l'arrivée est couvert par le scénario 21 de Planning manager.
17. Voir ce que la journée devait être avant d'accorder la demande
Tickets : APAJH-256 · Rôle : Manager n1 · Prérequis : le scénario 15 joué — une demande en attente dans la file, sur un salarié qui a une trame en cours (sans quoi ses journées ne sont prévues sur rien).
Arbitrer, c'est décider de ce qu'on retire autant que de ce qu'on accorde : un manager qui lit « 3 jours de congés annuels » ne sait pas encore s'il retire trois matins ou trois nuits. Le panneau le dit désormais, des deux portes qui y mènent.
Sur
/demandes, état « À valider », cliquez la ligne d'une demande.✅ Résultat attendu : au-dessus du cadre « Demandé », une bande grise « Prévu initialement » et, à sa droite, une puce par poste — « JM · Matin · 07:00–14:30 ». La puce porte la couleur de la catégorie du poste, la même que la case du planning : c'est le même poste, il se reconnaît à la même teinte.
Vérifiez que la file, elle, ne dit rien de tout cela : revenez à la liste et regardez les colonnes.
✅ Résultat attendu : aucune colonne « prévu ». Résoudre une journée coûte une lecture de planning, que le serveur ne fait que sur une demande lue seule (
GET /api/absences/<n°>/). Trente lignes de file en feraient trente, pour une colonne que personne ne lit dans un tableau.Ouvrez une demande qui court sur plusieurs jours.
✅ Résultat attendu : l'encart porte le premier jour de la demande, et lui seul. Dix journées ne tiennent pas dans une bande ; la période entière est déjà dans le cadre « Demandé » juste en dessous.
Ouvrez une demande dont le premier jour tombe sur un repos de la trame.
✅ Résultat attendu : « Prévu initialement : RH · Repos hebdo. ». Ce n'est pas un trou à masquer — savoir que le premier jour demandé ne coûtait rien est une information pour qui tranche.
Ouvrez une demande d'un salarié sans trame en cours, ou dont le premier jour est une case vide.
✅ Résultat attendu : pas d'encart du tout. Rien n'était prévu, donc il n'y a rien à rendre — une bande vide aurait fait croire à une lecture ratée.
Fermez le panneau, passez l'onglet réseau des outils de développement en hors ligne, puis cliquez une autre ligne.
✅ Résultat attendu : à la place de l'encart, la phrase « Ce que la journée devait être n'a pas pu être chargé. » Une lecture ratée et une journée sans rien de prévu se ressemblent trait pour trait à l'écran, et l'une des deux ferait accorder une demande sans dire ce qu'elle retire. Le reste du panneau tient debout : la demande vient de la file, pas de cette lecture.
Basculez sur « Validées » et ouvrez une demande déjà accordée.
✅ Résultat attendu : l'encart nomme toujours le poste, jamais « Congés payés ». La question posée est ce que la journée devait être, et la réponse ne change pas parce que la demande a été tranchée — alors que la grille, elle, dessine bien l'absence à la place du poste une fois qu'elle est accordée.
Revenez sur « À valider », suivez « Voir au planning › », et regardez le panneau qui se rouvre là-bas.
✅ Résultat attendu : le même encart, au même endroit. Le panneau est le même composant des deux côtés ; ce qui changeait était la lecture qui l'alimente, et les deux passent maintenant par la même. Le reste de cette arrivée est le scénario 24 de Planning manager.
Rétrécissez à la largeur d'un téléphone.
✅ Résultat attendu : le libellé reste à gauche, les puces passent à la ligne sous lui plutôt que de pousser la bande hors du panneau, et rien ne déborde.
Couverture automatique : e2e/absences-queue.spec.ts vérifie que le panneau ouvert depuis la file nomme le poste prévu et ses horaires — ce que la liste ne sert pas, donc ce qui prouve la relecture de la demande —, que la lecture en échec le dit au lieu de se taire, et que l'encart tient dans un viewport de 390 px. Les cas serveur — journée coupée, repos, journée modulée, demande accordée, jour sans rien de prévu, et la file qui ne paie pas pour un champ qu'elle ne sert pas — sont couverts par tests/planning_absences/test_planning_absence.py.
Demander une absence depuis son compte
18. Déposer une demande, la voir arriver dans la file, et la retirer
Tickets : APAJH-258, APAJH-259 · Rôle : Salarié sur l'API, puis Manager n1 dans l'application · Prérequis : la stack tourne, un salarié rattaché à un établissement et porteur d'une trame en cours, un compteur CP ouvert pour lui sur l'année de la demande, un jeton de son compte et un jeton d'un compte Manager n1
Ce lot est serveur. Le salarié n'a pas encore d'écran pour demander — il arrive avec « Mon espace ▸ Mes demandes » — donc les étapes qui écrivent s'appellent à la main sur l'API, comme les scénarios 9, 10 et 13. Ce qui est neuf se vérifie en revanche à l'écran, du côté encadrant : jusqu'ici rien dans l'application ne faisait naître une demande « en attente », et la file se peuplait au back-office.
Ce qui distingue demander de déclarer, c'est l'auteur. Écrire pour soi-même dépose une demande ; écrire pour un salarié de son périmètre déclare une absence, validée d'emblée. La règle est déduite par le serveur, jamais envoyée par le client — c'est ce que les étapes 3 et 4 vérifient.
Avec le jeton du salarié, déposez une demande pour vous-même :
POST /api/absences/avec{"employee": <sa fiche>, "type": <n° de Congés annuels>, "date_start": "<un lundi à venir>", "date_end": "<le mercredi suivant>"}.✅ Résultat attendu :
201, et la réponse portestatusàpending,versionà1et soncounted_quantity(« 3.00 »). Relisez le compteurCP: trois jours sont passés en « Planifiés » — une demande réserve, elle ne prend pas.Connectez-vous à l'application en Manager n1 et ouvrez Activité ▸ Demandes, état « À valider ». Ouvrez ensuite Activité ▸ Planning sur la semaine concernée.
✅ Résultat attendu : la demande est dans la file, avec son salarié, sa période et son décompte — sans qu'aucune ligne n'ait été fabriquée au back-office. Sur la grille, les trois journées gardent leur poste et portent l'anneau orange des demandes non tranchées (scénario 26 de Planning manager) ; un clic ouvre le panneau d'arbitrage.
Retour sur l'API avec le jeton du salarié. Refaites le même
POSTen nommant la fiche d'un collègue.✅ Résultat attendu :
400sous la cléemployee, « Vous ne pouvez demander une absence que pour vous-même. » Un refus, et non une réécriture silencieuse vers sa propre fiche : une charge inattendue se rejette.Rejouez l'étape 1 en ajoutant
"status": "validated"à la charge.✅ Résultat attendu :
201etstatusàpendingquand même. La colonne est en lecture seule et l'état se déduit de l'auteur : il n'y a rien à falsifier pour s'accorder un congé. (Supprimez cette seconde demande avant de continuer, sans quoi la suivante chevauchera.)Tentez de corriger votre demande :
PATCH /api/absences/<n°>/avec{"comment": "Finalement deux jours"}.✅ Résultat attendu :
403. Un salarié annule, il ne corrige pas : redéposer est le chemin, et corriger une demande sous arbitrage appartient à qui la tranche.Retirez votre demande :
DELETE /api/absences/<n°>/.✅ Résultat attendu :
204, la ligne disparaît, la file du manager se vide et le compteurCPretrouve ses « Planifiés » d'avant. Rechargez le planning : les trois journées ont perdu leur anneau.Déposez une demande commençant il y a quatorze jours (J-14), puis une autre commençant il y a quinze jours (J-15), sur le motif Maladie. Retirez la première, puis, avec le jeton Manager n1, déclarez la journée de J-15 pour ce salarié.
✅ Résultat attendu : la demande à J-14 passe (
201) ; celle à J-15 est refusée (400sousdate_start, « Impossible de modifier une date antérieure au JJ/MM/AAAA. », la date étant J-14) ; la déclaration de l'encadrant passe (201), sa fenêtre remontant au 1er décembre de l'année précédente (scénario 20 de la recette planning). Une demande n'écrit rien dans l'histoire — elle réserve, et un humain tranche — et remonte donc plus loin qu'une déclaration du salarié, mais pas sans fin. La borne lit le premier jour, arrêts de travail compris : un arrêt plus ancien se déclare par l'encadrant. Un intérimaire répond à la même borne, et un encadrant qui demande pour lui-même aussi — depuis « Mes demandes » comme depuis sa propre ligne du planning : demander pour soi, c'est être un salarié comme un autre.Toujours en salarié, retirez cette demande d'hier. Puis faites-vous accorder une absence commençant hier (validez-en une depuis la file, en Manager n1) et tentez de la supprimer.
✅ Résultat attendu : la demande en attente se retire (
204) — personne ne l'avait lue, rien n'était acquis. Le congé accordé, lui, répond400sousstatus, commencé ou non : un congé accordé ne s'efface pas, son annulation se demande et s'arbitre (scénario 26).Faites refuser une demande (scénario 13, étape 6), puis tentez de la supprimer avec le jeton du salarié.
✅ Résultat attendu :
400sousstatus, « Seule une demande en attente se retire. Une absence accordée se fait annuler par une demande d'annulation. » Un refus est la trace d'une décision que quelqu'un a prise ; l'effacer rendrait la file amnésique. La ligne est bien la sienne — c'est la transition qui est refusée, pas l'accès, d'où un400et non un403.Ouvrez le Journal des actions au back-office et filtrez sur les absences.
✅ Résultat attendu : une ligne Création et une ligne Suppression portant le compte du salarié, pour la demande déposée à l'étape 1 et retirée à l'étape 6 — plus une ligne Modification sur la correction d'un encadrant. Le domaine des absences est désormais tracé de bout en bout : déposer, déclarer, corriger, trancher, retirer.
Et l'encadrant qui se déclare à lui-même ? Il dépose une demande, lui aussi : depuis Activité ▸ Planning, « Déclarer une absence » sur sa propre ligne écrit « en attente » et non « validée ». C'est la conséquence assumée d'une règle qui lit l'auteur et non le rôle — l'absence d'un manager est arbitrée par sa hiérarchie comme celle de n'importe qui. Pour la même raison, sur sa propre absence il n'a que les droits d'un salarié : la corriger (« Déclarer une absence » sur une journée déjà absente de sa ligne, ou
PATCH) répond403« … et jamais la leur : retirez-la et déposez une nouvelle demande. », y placer l'absence d'un autre répond400sousemployee, un congé accordé ne se supprime pas plus que celui d'un salarié — il en demande l'annulation —, et sa propre demande ne se tranche pas : ouverte depuis la file ou le tableau de bord, elle se lit sans « Valider », « Refuser » ni « Ajuster avant validation », et l'API répond403« Vous ne pouvez pas trancher votre propre demande d'absence. », même avec le droit de décider sur son établissement.
Couverture automatique : tests/absences/test_absences_request.py couvre l'état de naissance dans les trois cas (salarié, encadrant pour autrui, encadrant pour lui-même), le refus d'écrire pour un collègue, la correction refusée, les deux retrait d'une demande en attente et les refus de retrait de tout autre état — dont celui d'une demande tranchée pendant que la suppression était en vol —, la réservation de compteur posée puis relâchée, les deux traces au journal, et les lectures de la rétroactivité : demander sur hier, demander à J-14 mais pas à J-15 (salarié comme intérimaire), l'encadrant tenu à J-14 pour lui-même, l'encadrant qui corrige la demande en attente d'un salarié sous sa propre fenêtre, déclarer hors de la fenêtre de l'encadrant, retirer une demande passée, corriger une demande en attente vers le passé ; et, pour l'encadrant sur sa propre fiche, la correction refusée (en attente comme accordée), le déplacement d'une absence sur soi refusé, le congé accordé qui ne se supprime pas, le refus qui ne s'efface pas et la décision sur sa propre demande refusée. Ce qui reste à jouer à la main est ce que les tests ne voient pas : la file et la grille du manager qui se peuplent sans passage par le back-office.
19. Suivre et annuler ses demandes depuis « Mes demandes »
Tickets : APAJH-259, APAJH-261 · Rôle : Salarié · Prérequis : la stack tourne, un compte Salarié rattaché à une fiche, et au moins une demande dans chacun des trois états — le plus simple est d'enchaîner sur le scénario 18, qui en laisse une en attente, une validée et une refusée
Le pendant à l'écran du lot précédent : la clé p_demandes du tiroir menait jusqu'ici au panneau de démonstration du portail. Elle ouvre désormais /mes-demandes, qui lit la fiche du compte connecté et rien d'autre.
Connectez-vous avec le compte Salarié, en viewport 390 × 844. Ouvrez le tiroir, puis Mon espace ▸ Mes demandes.
✅ Résultat attendu : l'URL devient
/mes-demandes, l'entrée reste surlignée, et la page liste une carte par demande — motif, période, durée telle que le serveur l'a comptée, et une pastille d'état. La carte en attente dit depuis quand elle attend (« Déposée il y a 2 j », « Déposée hier »…) ; une demande déposée avant APAJH-261 n'a pas cette ligne, sa date de dépôt n'ayant pas été conservée.Lisez la carte refusée.
✅ Résultat attendu : elle porte « Refusée », la date de la décision (« Refusée le 28/05 »), et le motif que l'encadrant a saisi dans un encadré rouge ouvert par « Commentaire du refus : », cité entre guillemets et en italique — ce sont ses mots, pas ceux de l'application. Votre propre commentaire, s'il y en avait un, est cité à part : ce que vous avez écrit et ce qu'on vous a répondu ne sont pas la même phrase.
Si l'une de vos demandes a été accordée sur autre chose que ce qui était demandé (scénario 15), lisez sa carte.
✅ Résultat attendu : elle porte « Validée le JJ/MM » et « Commentaire de la validation : … » dans un encadré gris. Une validation ne porte de motif que dans ce cas-là. Une absence déclarée directement par un encadrant, que personne n'a tranchée, n'a pas de date de décision.
Parcourez les filtres
Toutes/En attente/Validées/Refusées/Annulées.✅ Résultat attendu : chacun annonce son compte entre parenthèses, et ces comptes ne bougent pas quand on change de filtre — ils décrivent le dossier, pas la sélection. Un filtre sans ligne dit pourquoi (« Aucune demande en attente de décision »). « Annulées » compte les congés annulés (scénario 26) ; un congé dont l'annulation est seulement demandée est compté sous « En attente » : c'est une demande qui attend sa réponse. Sur téléphone les cinq puces passent à la ligne au lieu de déborder : la page ne défile jamais horizontalement, quelle que soit la largeur.
Sur la demande en attente, cliquez « Annuler la demande » — un bouton à contour, portant une croix — puis confirmez.
✅ Résultat attendu : la boîte demande « Annuler cette demande ? » et non « Supprimer cette absence ? » — ce n'est pas le même geste que celui de l'encadrant. La ligne disparaît, le compte du filtre
En attentebaisse, et le compteurCPretrouve sa réservation (vérifiable dans Mon espace ▸ Mes compteurs).Regardez la carte refusée, puis la carte validée.
✅ Résultat attendu : la carte refusée ne porte aucun bouton — un refus est la trace d'une décision que quelqu'un a prise ; l'effacer rendrait la file amnésique. La carte validée porte « Demander une annulation » et non « Annuler la demande » : un congé accordé ne se retire pas, son annulation se demande (scénario 26).
Cliquez le bouton rond « + », en bas à droite.
✅ Résultat attendu : il s'ouvre sur les deux choix de la maquette — « Demander une absence », qui mène au formulaire (scénario 22), et « Demander une modification », qui mène au sien (voir Demandes de modification, scénario 5).
Repassez en bureau (1440 × 900) sur la même URL.
✅ Résultat attendu : le même parcours, en colonne centrée, le « + » toujours en bas à droite.
Connectez-vous avec un compte Manager n1 et ouvrez la même entrée.
✅ Résultat attendu : la page ne montre que ses demandes, pas celles de son périmètre — c'est ce que le paramètre
employeede la requête garantit, en plus du rétrécissement que le serveur applique déjà à un salarié.
Couverture automatique : e2e/mes-demandes.spec.ts rejoue aux deux densités la liste et ses trois états, le décompte des filtres, la restriction à un état, le DELETE sur la bonne demande, les deux libellés selon l'état, l'absence de commande sur un refus, une annulation et une annulation demandée, les puces qui passent à la ligne sans jamais élargir la page, l'état vide et le « + » dont chacun des deux choix mène à son formulaire ; plus le compte sans fiche salarié, renvoyé vers les ressources humaines — qui n'a pas de « + » non plus. Ce qui reste à jouer à la main est ce que la spec bouchonne : la lecture réellement restreinte à sa fiche, et le compteur recrédité.
20. Consommer le compteur CA commun et annuler les anciens ANC
Tickets : APAJH-66, APAJH-282 · Rôle : gestionnaire.
Reprendre une consommation ANC et charger le droit CA définitif. Ouvrir le panneau d'absence sur bureau et mobile.
✅ Le panneau présente une seule ligne Congés annuels, avec la provenance paie et l'acquisition suivante datée. ANC n'est pas proposé pour une nouvelle absence.
Annuler une ancienne absence ANC et rapprocher le journal.
✅ Seule sa consommation est libérée. Les mouvements initiaux restent lisibles ; la reprise n'a ajouté ni double débit ni consommation manuelle répétée.
Corriger le droit paie de sorte que le restant devienne négatif. Promouvoir une absence planifiée en prise, puis tenter une nouvelle pose et une annulation.
✅ La promotion et l'annulation sont acceptées ; la nouvelle consommation est refusée. Sans droit définitif, la pose est également refusée.
Couverture automatique : backend/tests/test_payroll_leave.py, backend/tests/test_payroll_leave_migration.py et e2e/payroll-annual-leave.spec.ts.
Stockage privé des documents
21. Consulter les justificatifs par la route protégée
Rôles : RH, manager et salarié · Prérequis : un justificatif déjà déposé.
- Avec un compte RH, ouvrir une absence de son périmètre portant un justificatif. Cliquer sur Consulter, sur ordinateur puis sur téléphone. Résultat attendu : ouverture du document dans un nouvel onglet ; la requête API porte le JWT et l’onglet utilise un blob, jamais une URL publique de stockage.
- Avec un manager sans droit de consultation, ouvrir la même absence. Résultat attendu : nom, date et déposant visibles, sans bouton Consulter.
- Avec le salarié concerné, consulter sa propre pièce par l’API. Résultat attendu : accès autorisé, même si un gestionnaire l’a déposée.
- Révoquer la permission d’un lecteur tiers puis retenter une consultation. Résultat attendu : refus et message explicite ; aucun fichier affiché.
- Depuis le back-office, supprimer une pièce puis une absence portant plusieurs pièces. Résultat attendu : suppression des lignes et des fichiers après validation.
Couverture automatique : e2e/absences.spec.ts (desktop et mobile) et tests/absences/test_document_files.py (droits, contenu, transactions, admin).
Déposer sa demande depuis Mon espace
22. Demander une absence depuis « Mes demandes »
Tickets : APAJH-261 · Rôle : Salarié, puis Manager n1 · Prérequis : la stack tourne, un compte Salarié rattaché à une fiche, un contrat en cours et un compteur CP ouvert (le jeu de démonstration de make up les fournit)
L'écriture du scénario 18, à l'écran : jusqu'ici une demande ne se déposait que sur l'API. Le formulaire est l'assistant même du planning (scénario 11), hébergé sur une page à lui, /mes-demandes/nouvelle — avec ce que la demande y ajoute : le solde du compteur avant et après, l'info-bulle du motif, la borne à J-14 et l'écran « Demande envoyée ».
Ce que le salarié ne peut pas faire ici : corriger. Le formulaire ne rouvre jamais une demande déjà déposée ; on la retire depuis « Mes demandes » et on en redépose une (scénario 19).
Connectez-vous avec le compte Salarié, en viewport 390 × 844. Ouvrez Mon espace ▸ Mes demandes, cliquez le « + », puis Demander une absence.
✅ Résultat attendu : l'URL devient
/mes-demandes/nouvelle, l'entrée « Mes demandes » reste surlignée dans le tiroir, la page s'intitule « Demander une absence » et s'ouvre sur l'étape Type (1 sur 3), en une colonne qui occupe la largeur du téléphone sans jamais défiler horizontalement.Choisissez la catégorie Congés, puis touchez le ⓘ à droite d'un motif qui en porte un.
✅ Résultat attendu : une bulle s'ouvre au toucher et dit ce que le motif implique — « Justificatif attendu » et/ou « Validation », tels que le référentiel les énonce. Un motif sans aucune de ces deux phrases n'a pas de ⓘ du tout.
Choisissez un motif décompté en jours sur
CP, puis Suivant.✅ Résultat attendu : l'étape Détails montre le bloc « Solde restant (Congés annuels) » → « Durée de l'absence » → « Solde après l'absence ». La durée et le solde après viennent du serveur ; ils se recalculent à chaque période choisie. Les deux soldes sont ceux de l'année de congés que l'absence débite : une demande faite en avril pour juillet se lit sur l'année qui commence en juin, pas sur celle en cours. Une période à cheval sur deux années de congés n'affiche pas de solde après (« — »), aucun compteur ne tenant à lui seul le résultat.
Ouvrez le calendrier des Dates.
✅ Résultat attendu : les jours sont teintés par votre poste de chaque jour, et tous les jours avant J-14 sont grisés et ne se sélectionnent pas. J-14 lui-même se sélectionne.
Choisissez une période de trois jours ouvrables à venir, Valider, puis saisissez un commentaire et Suivant.
✅ Résultat attendu : le récapitulatif reprend motif, période, dates, durée et commentaire, et son bouton dit « Confirmer et envoyer », pas « Enregistrer ».
Cliquez Confirmer et envoyer.
✅ Résultat attendu : l'écran « Demande envoyée » — « Votre demande de motif (durée) a été transmise à votre responsable. » — et un bouton « Voir mes demandes » qui ramène à la liste, où la demande figure En attente. Aucun toast ne répète « Demande envoyée » : l'écran est la confirmation. « Mes compteurs » montre les jours planifiés sur
CP: la demande les réserve.Refaites une demande sur Arrêt maladie (ou tout motif qui demande un justificatif), datée d'hier, avec un fichier PDF joint.
✅ Résultat attendu : la zone de dépôt est là ; le récapitulatif nomme le fichier sous « Justificatif » ; la demande part (une demande remonte jusqu'à J-14).
Choisissez une période qui chevauche la demande du pas 6.
✅ Résultat attendu : dès l'étape Détails, sous les dates, un bandeau rouge dit pourquoi le serveur la refuse — « Une absence du … au … existe déjà. Modifiez ou annulez cette absence plutôt que d'en créer une seconde. », la même phrase que lit un encadrant qui déclare — et Suivant est grisé : on corrige la période là où on la choisit. Un simple avertissement (bandeau orange) laisserait passer. Dès qu'on change la période, le bandeau rouge disparaît et Suivant se rallume le temps du nouveau chiffrage : un refus ne retient que les dates qu'il a jugées.
Revenez d'une étape avec « Étape précédente », puis encore une fois.
✅ Résultat attendu : ce qui avait été saisi est conservé ; depuis la première étape, le lien devient « Retour à mes demandes » et ramène à la liste.
Reconnectez-vous en Manager n1, sur bureau (1440 × 900).
✅ Résultat attendu : la demande du pas 6 est dans Activité ▸ Demandes, En attente. Depuis Mon espace ▸ Mes demandes, le même formulaire s'ouvre en carte centrée, et la demande qu'il y dépose pour lui-même naît elle aussi en attente : sa propre absence se tranche par sa hiérarchie.
Couverture automatique : e2e/demande-absence.spec.ts rejoue aux deux densités le parcours complet jusqu'à l'écran d'envoi et la charge qui part (sa fiche, aucun état), le bloc de solde — lu sur l'année de congés débitée, sans « après » sur deux années —, l'info-bulle ouverte au clic et absente d'un motif muet, l'étape 2 d'un motif en heures, l'avertissement qui laisse passer et le refus qui retient, le justificatif en multipart nommé au récapitulatif, le calendrier borné à J-14 avec le refus du serveur affiché tel quel, le retour d'étape qui garde la saisie ; plus le compte sans fiche, renvoyé vers les RH. tests/absences/test_absences_request.py couvre le chiffrage ouvert au salarié pour sa seule fiche, et fermé sur une correction. Ce qui reste à jouer à la main est ce que la spec bouchonne : la réservation réelle sur le compteur, et l'arrivée de la demande dans la file du manager.
Règles des accords sur une absence
23. Être averti des règles des accords à la déclaration d'une absence
Tickets : APAJH-181 · Rôle : Manager n1 · Prérequis : un salarié dont la fiche porte une date d'entrée dans l'entreprise
Sur le planning, déclarez au salarié un don du sang de 2 h.
✅ L'absence s'enregistre, avec l'avertissement « Un don du sang est limité à 1 h 30, déplacement compris ». Un second don dans le même trimestre civil ajoute « Un seul don du sang est autorisé par trimestre civil ».
Déclarez un congé sans solde de plus de 3 mois, puis un congé sabbatique de moins de 6 mois.
✅ Chacun s'enregistre avec son avertissement (3 mois au plus ; 6 à 11 mois).
Déclarez un congé parental de plus d'un an, ou à un salarié entré il y a moins d'un an.
✅ Avertissements « accordé pour 1 an au plus » et « 1 an d'ancienneté à la naissance ».
Déclarez plus de 21 h d'aidant familial sur l'année.
✅ Avertissement de dépassement du plafond annuel de 21 h.
Déclarez une absence à un agent polyvalent pendant sa période d'essai.
✅ Avertissement « Absence pendant la période d'essai : elle peut être prolongée d'autant, la CDAPH en est alors informée ».
Aucun de ces avertissements ne refuse l'absence : c'est le choix du métier pour toutes les règles des accords. Les délais de prévenance (congés annuels 1 mois, au titre des congés d'ancienneté qu'ils comprennent depuis la fusion des droits CA/ANC — APAJH-66/282 —, aidant 72 h, don du sang 10 jours ouvrables, formation CPF/PTP 60 ou 120 jours, congé parental 1 ou 2 mois) ne portent que sur une demande du salarié, et aucun écran de demande n'existe encore : ils se vérifient par l'API.
Couverture automatique : tests/absences/test_absences_agreement_rules.py.
24. Être rappelé d'un justificatif non déposé
Tickets : APAJH-181 · Rôle : salarié, puis RH n1
Déclarez une maladie commençant il y a 3 jours, sans justificatif, puis lancez
docker compose exec backend python manage.py send_missing_document_reminders.✅ Le salarié reçoit dans la cloche « Justificatif à transmettre » (l'arrêt de travail devait être transmis sous 48 h) ; la RH reçoit « Justificatifs manquants » avec la liste. Relancer la commande n'envoie rien de plus.
Déposez un justificatif sur une autre absence du même type, puis relancez.
✅ Pas de rappel pour celle-ci.
Délais : arrêt de travail, certificat enfant malade et attestation de don du sang sous 48 h ; justificatifs d'aidant familial sous 2 jours ouvrables après l'absence. Le rappel part chaque matin à 5 h 37 et ne regarde que les délais échus dans la dernière semaine.
Couverture automatique : tests/absences/test_absences_document_reminders.py.
Être prévenu
25. Recevoir la demande, puis la décision, dans la cloche
Tickets : APAJH-262 · Rôle : Salarié, Manager n1, puis Admin au back-office · Prérequis : la stack tourne, le jeu de démonstration chargé (make seed-all) — Léa Martins est Manager n1 et Alice Bernard travaille, comme elle, à la MAS Glycines
Une demande déposée sonne qui peut la trancher, et seulement lui : les décideurs dont la file d'Activité ▸ Demandes affiche la demande. Ceux qui sont rattachés à un établissement du salarié — lieu principal, lieu secondaire ou équipe du jour — passent d'abord ; un Admin ou un RH sans établissement, qui voit tous les salariés, n'est sonné qu'à défaut. L'auteur ne l'est jamais. La décision, elle, sonne le salarié.
Au back-office, ouvrez le rôle Manager n1 de Léa Martins et restreignez-le à la MAS Glycines.
✅ Résultat attendu : le rôle n'est plus « valable sur toute l'association ».
Connectez-vous en Alice Bernard (Salarié), en viewport 390 × 844, et déposez une demande de congé à venir (scénario 22, pas 1 à 6).
✅ Résultat attendu : l'écran « Demande envoyée » dit qu'elle a été transmise à votre responsable.
Connectez-vous en Léa Martins, puis en Admin sans établissement, et ouvrez la cloche de chacun.
✅ Résultat attendu : Léa lit « Nouvelle demande d'absence — Alice Bernard a déposé une demande de motif du … au … ». L'Admin n'a rien : il voit la demande dans sa file, mais quelqu'un de rattaché peut la trancher.
En Léa Martins, refusez la demande depuis Activité ▸ Demandes avec le motif « Effectif insuffisant ».
✅ Résultat attendu : dans la cloche d'Alice Bernard, « Demande d'absence refusée » en rouge, qui se termine par « Motif : Effectif insuffisant ». Une demande validée arrive de la même façon, en information : « Demande d'absence validée ». Validée avec ajustement (scénario 15), la phrase nomme les deux : « … a été validée avec modification : ce qui a été accordé ».
En Alice Bernard, déposez une seconde demande, puis retirez-la depuis « Mes demandes » avant toute décision.
✅ Résultat attendu : la cloche de Léa a sonné au dépôt, et pas au retrait : personne n'avait encore lu la demande.
Remettez le rôle de Léa Martins sur toute l'association.
La demande que personne ne peut trancher. Une fiche sans lieu de travail ni équipe, sur une base où aucun décideur ne couvre toute l'association — ou un encadrant seul décideur de son site qui demande pour lui-même — ne s'affiche dans aucune file. Le dépôt passe quand même : l'écran « Demande envoyée » garde son récapitulatif mais dit « a été enregistrée », puis « Aucun responsable ne peut traiter cette demande pour l'instant. Contactez votre service RH. » ; le salarié en garde une trace dans sa cloche, et chaque Admin reçoit « Demande d'absence sans valideur » avec la cause — à défaut d'Admin, les RH n1/n2, à défaut un superutilisateur. Un superutilisateur sans rôle n'est jamais compté comme décideur. Le jeu de démonstration compte des décideurs sans établissement, ce qui rend le cas injouable à la main sans le défaire : il est couvert par la suite automatique.
Couverture automatique : tests/absences/test_absences_notifications.py vérifie que les décideurs sonnés sont exactement ceux, rattachés, dont la file montre la demande — lieu principal, lieu secondaire, équipe ; ni le DA, ni le Service Paie, ni un manager d'un autre site —, le repli sur les non-rattachés, l'auteur jamais sonné, la déclaration qui ne sonne personne, les deux cas d'orphelin et leur cause, le superutilisateur sans rôle qui ne masque pas un orphelin, le repli de l'alerte (Admin, RH, superutilisateur, journal), les deux décisions — dont la validation ajustée, qui nomme les deux — et le retrait silencieux. e2e/demande-absence.spec.ts rejoue aux deux densités l'écran d'envoi d'une demande orpheline, récapitulatif compris.
Annuler un congé accordé
26. Demander l'annulation d'un congé accordé, et la trancher
Tickets : APAJH-273 · Rôle : Salarié, puis Manager n1 · Prérequis : la stack tourne, le jeu de démonstration chargé (make seed-all), et un congé accordé à venir pour Alice Bernard sur le motif Congés annuels — le plus simple est d'en déposer une demande (scénario 22) et de la valider en Léa Martins depuis Activité ▸ Demandes
Retirer une demande que personne n'a tranchée ne reprend rien à personne : elle s'efface (scénario 19). Retirer un congé accordé défait une décision — le poste a été comblé, l'équipe prévenue —, donc cela se demande et s'arbitre.
Connectez-vous en Alice Bernard, en viewport 390 × 844, et ouvrez Mon espace ▸ Mes demandes. Notez le solde Congés annuels dans Mes compteurs.
✅ Résultat attendu : la carte en attente porte « Annuler la demande », la carte validée « Demander une annulation ». Deux libellés, deux gestes.
Sur la carte validée, cliquez « Demander une annulation », écrivez « Voyage annulé. » dans le commentaire facultatif, puis « Envoyer la demande ».
✅ Résultat attendu : la feuille rappelle le motif et les dates, et dit que l'absence reste accordée jusqu'à la décision. Un toast « Demande d'annulation envoyée » ; la carte porte la pastille orange « Annulation demandée », « Annulation demandée le JJ/MM » et « Votre demande d'annulation : « Voyage annulé. » », et plus aucun bouton. Elle passe sous le filtre « En attente » — une demande qui attend sa réponse — et quitte « Validées ». Le solde de Mes compteurs n'a pas bougé : le congé tient tant que personne n'a tranché. Si aucun responsable ne couvre Alice, le toast le dit à la place — « aucun responsable ne peut la traiter pour l'instant. Contactez votre service RH. » — et la demande est enregistrée quand même.
Connectez-vous en Léa Martins et ouvrez la cloche, puis le tableau de bord, puis Activité ▸ Demandes et le filtre « Annulations ».
✅ Résultat attendu : la cloche porte « Demande d'annulation d'absence ». Le tableau de bord compte la demande sous « Demandes en attente » et la liste sous « À traiter » : « Demande d'annulation — Congés annuels » ; un clic l'ouvre en « Annulation à arbitrer ». La file liste la ligne « Annulation demandée », avec son lien « Voir au planning › ».
Cliquez « Voir au planning › », puis fermez le panneau et survolez la case du congé.
✅ Résultat attendu : le planning s'ouvre sur la semaine du congé, la ligne d'Alice mise en évidence et le panneau « Annulation à arbitrer » ouvert. Une fois fermé, le congé est toujours dessiné — demander n'est pas annuler — et sa case porte le cercle orange d'une demande en attente, l'infobulle « Congés annuels · Annulation demandée ». Un clic sur la case rouvre l'arbitrage. Sur une demi-journée, seul le bloc de l'absence est cerclé, pas le poste travaillé à côté.
Revenez à la file et ouvrez la ligne. Lisez le panneau, puis cliquez « ✕ Refuser » et « Confirmer le refus » sans motif, puis avec « Le remplacement est organisé. ».
✅ Résultat attendu : le panneau s'intitule « Annulation à arbitrer ». Sous l'identité, l'encart « Absence validée » rappelle le congé accordé (motif · période · durée), puis « Rétabli si accordée » le poste que la journée retrouverait. La carte s'intitule « Demandé : annulation de l'absence » : la période y est barrée, la Durée en négatif (« −5 j »), l'Effet compteur en recrédit (« recrédite 5 j sur CP » — le compteur retrouve ce que le congé lui avait pris) et le Motif est le commentaire d'Alice. Pas de « Vérifications » — elles concernent l'octroi du congé, pas son annulation —, et pas de « Ajuster avant validation » : ce qui se tranche, c'est si le congé part, pas ce qu'il est. Sans motif, le refus est bloqué ; avec, un toast « Annulation refusée — l'absence est maintenue ». Côté Alice, la carte redevient « Validée », porte « Annulation refusée le JJ/MM » et le motif dans l'encadré rouge, et la cloche « Annulation d'absence refusée ».
En Alice, redemandez l'annulation ; en Léa, cliquez cette fois « ✓ Accorder l'annulation ».
✅ Résultat attendu : la ligne quitte « Annulations ». Chez Alice, la carte passe « Annulée », « Annulée le JJ/MM », et reste au dossier, sous la cinquième puce « Annulées » ; la cloche porte « Annulation d'absence acceptée », et le solde de Mes compteurs a retrouvé ses jours. Le planning ne dessine plus le congé, et Alice peut redemander les mêmes jours : une ligne annulée ne retient plus rien.
Avec le jeton d'Alice, tentez
DELETE /api/absences/<n°>/sur un autre de ses congés accordés, puisPOST /api/absences/<n°>/request-cancellation/sur un congé accordé commencé il y a quinze jours. Dans Mes demandes, comparez la carte de ce congé à celle d'un congé accordé commencé il y a quatorze jours.✅ Résultat attendu :
400sousstatusdans le premier cas — un congé accordé ne s'efface pas par son auteur ;400sousdate_startdans le second — l'annulation se demande jusqu'à J-14, comme toute demande (APAJH-259), pas au-delà. La carte du congé commencé il y a quinze jours ne porte aucun bouton ; celle du congé commencé il y a quatorze jours porte toujours « Demander une annulation » : le salarié revenu plus tôt d'un congé peut encore rendre les jours qu'il n'a pas pris.En Léa Martins, ouvrez le planning sur un autre congé accordé d'Alice Bernard, à venir, et cliquez « Supprimer l'absence » au détail du poste, puis confirmez. Recommencez au back-office sur un troisième congé accordé : Absences, cochez la ligne, action « Annuler les absences accordées sélectionnées ».
✅ Résultat attendu : dans les deux cas, la case du planning redevient ce que la trame en disait et le compteur est recrédité — mais la ligne n'est pas effacée : chez Alice, elle apparaît « Annulée » sous la puce « Annulées », « Annulée le JJ/MM », et la cloche porte « Absence annulée — Votre absence de … a été annulée par Léa Martins ». Supprimer un congé accordé, c'est l'annuler : il laisse la même trace qu'une annulation arbitrée. Une demande en attente ou un refus, qui n'accordaient rien, sont toujours effacés. Une ligne déjà annulée, elle, ne s'efface pas par l'API :
DELETE /api/absences/<n°>/répond400sousstatus— c'est la trace qu'Alice lit sous « Annulées ». Au back-office, le congé accordé n'offre pas « Supprimer » : l'action dit ce qu'elle fait — « 1 absence(s) annulée(s) et conservée(s) au dossier » — au lieu d'annoncer une suppression qui n'a pas eu lieu. L'action groupée « Supprimer les absences sélectionnées » sur ce congé est refusée tout entière (page « Impossible de supprimer »), et le champ Statut de la fiche ne propose ni « Annulée » ni « Annulation demandée » — il n'est même plus modifiable sur une ligne dans l'un de ces états.
Les justificatifs. Annuler un congé accordé, par l'arbitrage ou par une suppression, supprime les pièces déposées sur l'absence : la ligne reste au dossier, pas les données de santé qui la justifiaient. Tant que l'annulation est seulement demandée, elles restent.
Couverture automatique : tests/absences/test_absences_cancellation.py vérifie les quatre transitions ; la suppression d'un congé accordé par un encadrant, ou son annulation par l'action du back-office, qui le conservent annulé et préviennent le salarié ; la suppression refusée au back-office sur un congé accordé, à l'unité comme par l'action groupée ; le refus d'effacer par l'API une ligne annulée ; l'erreur d'une ligne que l'action ne peut annuler, affichée sans défaire les autres ; le statut du formulaire, fermé aux états d'annulation ; le refus de demander l'annulation d'une ligne qui n'est pas accordée, qui n'est pas la sienne ou qui a commencé avant J-14 ; le motif obligatoire au refus, la fenêtre du décideur à l'accord, la correction refusée d'une annulation ; les compteurs inchangés entre accordé et annulation demandée puis recrédités à l'accord, et ce recrédit servi au panneau (cancellation_counter_effects) ; le journal des écritures, la suppression des justificatifs, les jours rendus aux règles de chevauchement, et les notifications. tests/planning_absences/test_planning_absence.py vérifie que le planning dessine une annulation demandée, cercle son bloc (pending_request.is_cancellation) sans cercler le poste travaillé à côté d'une demi-journée, et ne dessine pas une annulation accordée. tests/test_dashboard.py vérifie que le tableau de bord liste les annulations à trancher. e2e/mes-demandes.spec.ts rejoue aux deux densités les cinq puces, les deux libellés, l'absence du bouton sur un congé commencé avant J-14, l'envoi de la demande et le toast d'une annulation que personne ne peut traiter ; e2e/absences-queue.spec.ts le filtre « Annulations », son lien « Voir au planning », le panneau d'une annulation (absence validée, durée négative, recrédit) aux deux densités et les deux commandes du panneau. Le cercle, le clic qui ouvre l'arbitrage et le lien qui rouvre une annulation sont couverts au chapitre Planning manager ; l'annulation listée et arbitrée depuis le tableau de bord, au chapitre Tableau de bord manager.
Le Repos plus
27. Poser un « Repos plus » et le voir imputé sur l'annualisation
Tickets : APAJH-301 · Rôle : Manager n1, puis RH, puis Salarié · Prérequis : la stack tourne, le jeu de démonstration chargé (make seed-all), dont le compte annualisé annualization.demo (voir Annualisation)
Le Repos et le Repos hebdomadaire sont prévus par la trame. Le R+ est un repos en plus, que le salarié demande pour récupérer des heures faites au-delà de son annualisation : ce n'est jamais une case de trame, c'est une absence.
Au back-office, ouvrez Types d'absence et cherchez
R+.✅ Résultat attendu : le motif Repos plus, code R+, est rangé sous Congés, juste après « Récupération », en jours, actif, avec l'effet sur l'annualisation Imputée sur l'annualisation. Il n'est relié à aucun compteur : il ne consomme ni CP, ni RC.
En manager, ouvrez le Planning sur un jour passé où le salarié annualisé de démonstration travaille, ouvrez la journée et Déclarer une absence : catégorie Congés, motif Repos plus, ce jour-là, puis Enregistrer. Rejouez sur téléphone (390 × 844).
✅ Résultat attendu : le motif s'affiche avec son code R+ ; l'étape 2 n'annonce aucun solde de compteur. Dans la grille, la case du jour porte le badge R+ à la place du poste, et le Total de la ligne perd les heures du jour.
En RH, ouvrez Compteurs ▸ Annualisation, puis le relevé de ce salarié, sur le mois du jour posé.
✅ Résultat attendu : le jour affiche R+, avec un réalisé de 0 h alors que le prévu reste de 7 h : l'écart, −7 h, est imputé sur le solde d'annualisation — rien n'est reconstitué, contrairement à une absence rémunérée ou non rémunérée. Un R+ posé sur un jour à venir ne change pas encore le solde à date, mais baisse d'autant le solde projeté.
Connectez-vous en salarié, Mon espace ▸ Mes demandes,
+▸ Demander une absence, motif Repos plus, puis Confirmer et envoyer. Validez-la ensuite en manager depuis Activité ▸ Demandes.✅ Résultat attendu : le R+ se demande puis se valide comme toute autre absence ; tant qu'il est en attente, la journée garde son poste.
Ouvrez Bibliothèque ▸ Trames et cliquez une case.
✅ Résultat attendu : la case ne propose que Poste, Repos et Repos hebdomadaire — aucun motif d'absence, donc jamais de R+ dans une trame.
Couverture automatique : tests/test_annualization_updates.py vérifie qu'un R+ accordé laisse le jour non travaillé et débite le solde des heures prévues, et qu'un R+ à venir ne baisse que le solde projeté — sur la ligne que crée la migration ; tests/test_absence_referential_seed.py, le référentiel de développement qui la contient. e2e/demande-absence.spec.ts rejoue aux deux densités la demande d'un « Repos plus » sous son code R+, sans solde de compteur.
Un congé à la journée entière
28. Ne plus poser un congé à la demi-journée
Tickets : APAJH-302 · Rôle : Manager n1, puis Salarié, puis RH · Prérequis : la stack tourne, le jeu de démonstration chargé (make seed-all)
Un congé se pose à la journée entière, jamais au matin ni à l'après-midi. Les autres motifs en jours — maladie, jury, enfant malade… — gardent la demi-journée.
Au back-office, ouvrez Types d'absence et regardez la colonne Demi-journée possible.
✅ Résultat attendu : elle est décochée pour CP, CT, ANC, JRS, CET et R+, cochée pour tous les autres motifs.
Au Planning, ouvrez la journée d'un salarié ▸ Déclarer une absence : catégorie Congés, motif Congés annuels, puis Suivant. Rejouez sur téléphone (390 × 844).
✅ Résultat attendu : l'étape 2 ne propose ni Journée entière / Demi-journée, ni Matin / Après-midi — seulement les dates. Une absence d'un jour est décomptée 1 j sur le compteur de congés, et le relevé d'annualisation retire la journée entière.
Recommencez avec le motif Maladie, choisissez Demi-journée puis Matin, revenez avec Précédent et choisissez Congés annuels.
✅ Résultat attendu : la demi-journée ne suit pas — l'étape 2 du congé repart sur la journée entière, et c'est elle qui part à l'enregistrement.
Connectez-vous en salarié, Mon espace ▸ Mes demandes,
+▸ Demander une absence ▸ Congés annuels.✅ Résultat attendu : même étape 2, sans demi-journée.
En manager, dans Activité ▸ Demandes, ouvrez une demande de Maladie d'une demi-journée, Ajuster, et requalifiez-la en Congés annuels.
✅ Résultat attendu : le motif accordé repart sur la journée entière ; la demi-journée n'est plus proposée.
Au back-office, Absences ▸ Ajouter absence : un Congés annuels d'un seul jour, À partir de Matin et Jusqu'à Matin, puis enregistrez.
✅ Résultat attendu : refusé, sous À partir de : « Ce motif se pose à la journée entière : la demi-journée ne s'y applique pas. » L'API répond de même à
POST /api/absences/et à l'aperçu de durée. Un congé déjà posé à la demi-journée avant la mise à jour reste affiché et s'accorde tel quel.
Couverture automatique : tests/absences/test_absences_declaration.py vérifie le refus d'un matin comme d'un après-midi sur un motif à la journée entière, l'absence d'une journée décomptée 1 j, la réponse de l'aperçu, et les motifs que la migration décoche ; tests/absences/test_absences_workflow.py, qu'une demande déposée à la demi-journée avant APAJH-302 s'accorde telle quelle et qu'un ajustement doit la prendre à la journée entière. e2e/demande-absence.spec.ts rejoue aux deux densités un congé sans demi-journée, la demi-journée d'un autre motif qui ne le suit pas, et l'envoi en journée entière ; e2e/absences-queue.spec.ts, aux deux densités aussi, la requalification d'un matin de maladie en congé, accordé sur la journée (étape 5).