Apparence
02. Architecture fonctionnelle
Les domaines métier, les règles qui structurent le modèle, et les flux qui les traversent. Le dictionnaire de données complet — chaque table, chaque colonne, ses valeurs autorisées et ses contraintes en base — est généré dans Modèle de données ; cette section dit pourquoi il a cette forme.
Les domaines
Le sens des flèches est celui de la dépendance : le dossier du personnel ne se tient pas sans le référentiel de l'organisation, et les habilitations se définissent par établissement, donc sur ce même référentiel. Absences en dépend trois fois : d'un salarié, des types de contrat qui restreignent l'éligibilité, et des jours fériés que son décompte exclut. Le calendrier du planning la lit — voir la section suivante — mais l'inverse est faux : un salarié sans trame peut être absent, ce qui est la raison pour laquelle le domaine ne vit pas sous la planification.
Évènements dépend du référentiel pour son rattachement facultatif et du dossier du personnel pour son responsable et ses participants. Absences lit ce domaine au moment d'une validation afin de produire une alerte, sans intégrer pour autant les évènements au contrat de lecture du planning. Un évènement est soit à la journée sur une période inclusive, soit horaire sur une seule date ; deux heures structurées évitent au portail salarié d'avoir à interpréter un libellé libre. GET /api/events/mine/?from=&to= expose au compte connecté le seul sous-ensemble en lecture nécessaire à son planning, sans révéler les autres participants ni ouvrir les commandes manager.
Deux bibliothèques personnelles n'apparaissent pas sur ce schéma : Postes et Trames (Bibliothèque ▸ Postes/▸ Trames), qui appartiennent chacune à leur manager plutôt qu'à l'organisation, sans dépendance vers les domaines ci-dessus. Une trame compose un cycle de postes ; elle s'arrête là. C'est Planification qui la rattache à un salarié réel, daté (le wizard ouvert depuis « Enregistrer et créer un planning ») — le seul des deux sens de la flèche qui existe : Planification lit une trame par son identifiant, elle ne compose rien à sa place. Éditer une trame qui planifie quelqu'un la clone d'abord (Trame.parent/archived_at) pour que l'historique retrouvé plus tard reste fidèle à ce qui a réellement été planifié — une contrainte légale, pas un choix de modélisation.
Décision : une version de trame ne coupe aucune planification (APAJH-294). À chaque enregistrement d'une trame planifiée, le serveur rattachait les planifications en cours ce jour-là au clone, les coupait la veille et en rouvrait une sur le nouveau contenu : la version précédente perdait ses jours restants, et deux enregistrements le même jour laissaient des périodes vides. Désormais toutes les planifications de la trame passent au clone sans toucher à leurs dates, et un enregistrement qui ne change pas le cycle n'en crée aucun. C'est à l'affectation que les périodes se règlent : la nouvelle ne retire à une autre que les jours qu'elle couvre. Une planification antérieure s'arrête la veille et, si elle courait au-delà, reprend le lendemain de la nouvelle sur une ligne à part (APAJH-326) — sans quoi une reconduction tacite recouverte par trois semaines de remplacement s'arrêtait avec elles ; une postérieure garde ses jours suivants, sur la même semaine de son cycle ; une planification sans chevauchement reste entière. Un salarié peut ainsi suivre plusieurs trames, ou plusieurs versions d'une trame, dans l'année : le calendrier, les alertes, le théorique et l'annualisation lisent chaque jour sur la planification en vigueur ce jour-là.
Le calendrier se calcule, il ne se stocke pas
Ce que Planification produit — qui, sur quelle trame, depuis quand — se lit jour par jour par un endpoint de période, GET /api/planning/?from=&to=, qui alimente l'écran manager Activité ▸ Planning (vue Général : une ligne par salarié, une colonne par jour).
Décision structurante : une journée n'a pas de ligne en base. Elle se dérive, à la lecture, de la trame que le salarié suit à cette date — trame, puis case du cycle, puis poste — et ce qui a une ligne, c'est l'exception : la modulation (ShiftOverride), qui remplace entièrement la journée qu'elle nomme. La raison est que la trame est déjà l'expression complète du prévu : matérialiser un enregistrement par salarié et par jour créerait une seconde vérité à tenir synchronisée avec elle, pour un volume qui croît sans borne avec le temps, et qui se recalcule à coût constant. Le prix payé est que la lecture d'une période est un calcul : elle est donc plafonnée à 62 jours, ses deux bornes obligatoires.
Décision : remplacée, mais pas oubliée. La journée que la trame prévoyait voyage à côté de la journée modulée, et le panneau de détail la restitue sous « Prévu initialement » : sans elle, l'écran pouvait dire qu'une journée avait été modifiée mais pas ce qu'elle était. C'est une description — rien ne la compte, aucune règle CCN-66 ne la lit — et non une seconde journée, qui serait précisément la seconde vérité que la décision ci-dessus refuse d'entretenir.
La règle qui décide si le champ est renseigné est « la case ne montre plus ce que la journée était », et elle vaut pour les deux choses qui prennent une journée :
- la modulation qui réécrit les horaires. Celle qui déplace la journée sans y toucher garde ceux de la trame, donc rien n'a été prévu à la place ;
- l'absence accordée qui couvre la journée entière, qui porte alors la journée planifiée — modulation comprise, puisque ce sont ces heures-là que le salarié aurait travaillées. Une absence d'une demi-journée ne renseigne rien : les créneaux restants sont dessinés à côté d'elle, et les nommer comme abandonnés dans la même case dirait l'inverse.
Décision : l'absence est la troisième couche, et elle prime. Une Absencevalidée sur un salarié et une date passe devant la modulation comme devant la trame : une absence est un fait sur la personne, les deux autres sont des plans sur la journée, et le fait gagne. Rien n'est effacé pour autant — supprimer l'absence rend la journée à ce qu'elle était. La précédence ne porte cependant que sur les demi-journées couvertes : une demi-journée d'absence laisse dans la case les créneaux prévus, à côté d'elle, parce que quelqu'un a travaillé l'autre moitié.
Décision révisée (APAJH-291) : une absence neutralise les créneaux qu'elle recouvre. La décision précédente additionnait l'absence et le poste entier, au motif qu'il restait à l'encadrant de dire combien de temps avait été travaillé ; le Total d'une journée coupée par une demi-journée de congé était donc faux tant que personne ne modulait. Or les créneaux le disent déjà. Chacun perd, au calcul, la part que l'absence lui prend — ses heures pour une absence en heures, et pour une demi-journée la moitié du temps travaillé de la journée, la première moitié pour un matin, la dernière pour un après-midi (APAJH-289). Un créneau entièrement recouvert reste dessiné, atténué, sans rien compter. Rien n'est écrit : supprimer l'absence rend la journée telle quelle. Une seule découpe sert le Total, les alertes CCN-66 et l'annualisation, pour que la grille et les compteurs ne divergent pas.
Décision révisée (APAJH-289) : la coupure d'une demi-journée suit le temps travaillé, et non plus l'horloge. APAJH-291 coupait à 12:00 : sur la journée 08:00–09:00, 10:00–12:30, 13:30–17:30 (7 h 30), un matin d'absence laissait 4 h 30 travaillées et un après-midi 3 h, alors que l'un comme l'autre vaut 3 h 45. Couper au milieu du temps travaillé fait que la demi-journée retire exactement ce qu'elle vaut — 3 h 45 restent dans les deux cas — et règle au passage le cas des équipes de nuit, dont le créneau de la veille au soir échappait au matin d'absence.
Décision : une demande en attente ne prime sur rien. Tant qu'une Absence est « en attente », la journée garde le poste qu'elle devait travailler et la demande la marque — un cerclage orange sur la case, et un clic qui ouvre le panneau d'arbitrage au lieu du détail de la journée. Dessiner un congé sur une journée que personne n'a accordée annoncerait une décision prise ; la question posée, elle, doit rester visible sur l'écran où on y répond. La conséquence est que trois lecteurs prennent trois positions différentes sur la même journée, et chacune est juste pour sa question :
| Lecteur | Compte la journée contestée ? | Pourquoi |
|---|---|---|
| Le Total de la ligne | non | il annonce ce qui est acquis, et rien ne l'est encore |
Les règles de temps de travail (apps.planning.rules) | oui | le poste reste planifié : un repos quotidien trop court reste un fait à signaler |
| La valorisation en compteurs | oui | c'est précisément la demande que le panneau d'arbitrage chiffre avant de la trancher |
La troisième ligne est une conséquence directe de la première : la valorisation demandait auparavant à la grille ce qu'une journée valait, et aurait donc chiffré zéro sur toute demande non tranchée. Elle interroge désormais l'absence elle-même, ce qui la rend indépendante de ce que l'écran choisit de dessiner.
Décision : le panneau d'arbitrage dit aussi ce qui serait retiré. Trancher une demande, c'est décider de ce qu'on enlève autant que de ce qu'on accorde : « trois jours de congés annuels » ne dit pas si l'on retire trois matins ou trois nuits. Le panneau porte donc le même encart « Prévu initialement » que le détail d'une journée, sur le premier jour de la demande — une période de dix jours ne tient pas dans une bande, et la période entière est déjà énoncée juste en dessous. Trois conséquences, chacune tenant à ce que la question n'est pas celle de la grille :
- la journée est résolue sans appliquer les absences, donc une demande déjà accordée nomme toujours le poste qu'elle a pris et jamais elle-même. La grille, qui répond à une autre question, dessine bien l'absence à sa place ;
- ce qui est rendu est la journée telle qu'elle était planifiée, modulation comprise : ce sont ces horaires-là que le salarié aurait travaillés, pas ceux de la trame en dessous ;
- la résolution est servie sur la lecture d'une demande et non sur la file : elle coûte une lecture de planning, qu'une ligne peut payer et qu'une file de trente transformerait en trente — pour une colonne qu'aucun tableau n'affiche.
Décision : une journée d'absence vaut le temps qu'elle devait être travaillée (APAJH-289), quel que soit le motif : la journée modifiée si un manager l'a modulée avant la pose, sinon le poste de la trame — tous ses postes pour une journée coupée — entière, ou par moitié. Aucune proratisation selon l'ETP : un salarié à 50 % sur un poste de 7 h décompte 7 h, et non 3 h 30. La règle précédente — la durée hebdomadaire contractuelle divisée par les jours postés de la semaine — retranchait à un temps partiel la moitié de la journée qu'il ne travaillait pas. Une journée de repos, repos ponctuel compris, ne vaut rien. C'est ce que débitent les compteurs en heures théoriques (absence_day_minutes) ; aucun écran du planning ne l'affiche.
Décision : le Total de la ligne compte les heures travaillées (APAJH-289). Une absence n'y ajoute rien : sur une semaine où la salariée travaille 27 h 30 et pose un CT d'une journée de 7 h 30, le Total affiche 27 h 30 et non 35 h. Une demi-journée sur un poste de 7 h vaut 3 h 30, et le créneau en garde 3 h 30 travaillées. Une absence n'entre pas non plus dans les règles CCN-66, qui mesurent des horaires de début et de fin qu'elle ne porte pas : un congé ne peut donc pas, à lui seul, faire franchir le plafond hebdomadaire de 44 h, lequel porte sur du travail effectif.
Décision : la base de décompte se lit par établissement. Jours ouvrables (lundi au samedi) ou ouvrés (lundi au vendredi) est une donnée de l'établissement (day_count_basis, par défaut ouvrables), pas une constante de l'application. Un dimanche et un jour férié se résolvent comme si l'absence n'existait pas, et un jour que la trame laisse au repos porte le badge sans heures. La même règle sert l'affichage et le décompte du plafond annuel, ce qui est ce qui les empêche de diverger entre l'écran et le chiffre auquel on le compare.
Décision : le poste voyage avec la journée. La bibliothèque Poste appartient à son créateur — un encadrant n'accède qu'à ses propres modèles d'horaires — alors que la grille affiche les journées d'autres personnes. La réponse de période embarque donc les postes qu'elle emploie, désignés par un identifiant opaque et non par leur code : ce code se déduit de la catégorie, si bien que deux postes du matin le partagent, et une jointure sur lui afficherait les horaires de l'un pour l'autre.
Décision : un jour peut porter plusieurs postes (APAJH-244). Une case de trame (TrameCase) n'était unique que par (trame, semaine, jour) — un seul poste par jour du cycle. La contrainte a été levée : plusieurs cases kind = poste peuvent désormais partager un jour (un poste du matin, un poste de l'après-midi), résolues chacune en son propre ResolvedShift — la même « journée coupée » qu'une modulation (ShiftOverride) produisait déjà pour une seule journée exceptionnelle, maintenant possible dans le cycle récurrent lui-même. Ce qui reste interdit : un repos (repos/repos_hebdo) ne partage jamais son jour avec une autre case, poste ou repos — un jour de repos est entier ou n'en est pas un. La règle est validée côté serializer (TrameSerializer.validate), pas en base : MySQL ne sait pas exprimer « au plus une case non-poste par jour ».
Décision : un créneau modulé porte sa pause, recopiée et non liée (APAJH-291). Chaque créneau d'une modulation (ShiftOverrideSlot) a sa pause, sur le modèle de Poste : une au plus, rémunérée ou non, déduite du temps net quand elle ne l'est pas. Sans elle, remettre le poste d'origine sur une journée comptait toute son amplitude, l'heure non rémunérée comprise : 8h30–17h30 valait 9 h au lieu de 8 h. Choisir un poste recopie sa pause sur le créneau, rémunérée ou non. À l'écran, la carte d'un créneau ne saisit qu'une pause rémunérée, qui ne change pas le total : une pause non rémunérée se fait avec les horaires, par l'écart entre deux créneaux, et celle d'un poste reste en lecture — une pause rémunérée saisie par-dessus la remplace. Le back-office garde les deux types. Un lien vivant vers le poste aurait été plus court, mais une journée déjà travaillée changerait de durée — et les compteurs avec elle — chaque fois que quelqu'un retouche le poste dans sa bibliothèque. Les créneaux enregistrés avant cette décision gardent leur amplitude brute : aucune reprise, pour qu'aucun total ni compteur déjà lu ne bouge.
Décision : un poste est un ou plusieurs créneaux, et sa pause est rémunérée (APAJH-300). Un poste de la bibliothèque n'est plus une plage avec une pause rémunérée ou non : c'est une suite de créneaux (PosteSlot) le même jour, et l'écart entre deux est non travaillé — la pause non rémunérée n'existe plus, c'est cet écart. Il garde au plus une pause, rémunérée et facultative, qui doit tenir dans l'un des créneaux. Une journée avec coupure n'a plus besoin d'un « poste du matin » et d'un « poste du soir », et le planning la dessine en un bloc par créneau au lieu d'une présence continue. Poste.creneaux() est le seul endroit qui découpe un poste et rattache la pause à son créneau ; le calendrier, les règles de trame et les demandes de modification le lisent, sous la même forme qu'un créneau modulé. Côté règles, rien ne change dans le moteur : il raisonne déjà en intervalles, et un écart entre deux créneaux y est une pause — une pause rémunérée, elle, ne coupe pas l'intervalle, comme avant. Reprise : un poste qui portait une pause non rémunérée est coupé en deux créneaux autour d'elle, même durée nette ; une pause rémunérée reste la pause. Les créneaux modulés (ShiftOverrideSlot, APAJH-291) gardent leur pause rémunérée ou non, pour l'historique déjà enregistré.
Décision : une journée modulée peut être un repos ponctuel (APAJH-291). La modulation porte un booléen de journée, ShiftOverride.is_rest, et le calendrier la sert comme le repos de la trame, sous sa modulation, pour 0 h ; ce que la trame avait prévu passe dans « prévu initialement ». C'est la cible de « vider la journée » : retirer un créneau quand d'autres restent ne fait que le retirer, retirer le dernier fait de la journée un repos, après une confirmation qui tient lieu d'enregistrement, et remettre sur les cartes les postes de la trame la rend à sa trame. Un booléen plutôt que l'absence de créneau, déjà lue « la trame garde ses heures » (APAJH-251) ; plutôt qu'un « créneau repos » aussi, puisqu'un créneau est une plage horaire et qu'un repos n'en a pas. Comme dans la trame, un repos est seul sur sa journée : le serializer et le back-office refusent is_rest avec des créneaux. Un repos n'a ni lieu ni option non plus — ni établissement, ni télétravail, ni hors établissement, ni trajet : l'API et le back-office les refusent (ShiftOverride.clean()), sans quoi un repos s'affichait « Télétravail » avec le picto maison. Seuls son motif et son commentaire restent. Une absence posée sur ce repos ne vaut rien ; celle des autres jours de la semaine garde la valeur de son propre poste.
Décision : on modifie une journée entière, jamais un créneau seul (APAJH-291). Cliquer sur n'importe quel bloc d'une case ouvre la journée, et « Modifier la journée » la montre tout entière : une carte par créneau, de quoi en ajouter et en retirer, un seul enregistrement. Une journée est un seul ShiftOverride : deux portes qui en montraient chacune une partie et en enregistraient le tout laissaient partir un créneau que personne ne voyait, ce qui faisait compter 35 h une semaine de 28 h. Motif, lieu, options et commentaire restent ceux de chaque créneau, comme en base depuis APAJH-244 ; le motif reste en vue, le reste se déplie. Des cartes qui reprennent exactement la trame sous des options communes écrivent une modulation sans créneau, qui continue de suivre la trame (APAJH-251) ; dès qu'une carte diffère, la journée devient ses propres créneaux. Le retour à la trame passe par ce formulaire : des cartes remises exactement sur les postes de la trame (poste, horaires, pause) et qui ne disent plus rien suppriment la modulation. « Rétablir le poste », une seconde porte pour le même geste, a été retiré du détail. Sa limite : la carte ne choisit que dans la bibliothèque de l'appelant, si bien qu'une journée dont le poste de trame vient d'un autre encadrant ne revient à la trame qu'au back-office.
Le formulaire compare la saisie, en heures nettes, au « prévu initialement » — la journée de la trame, une seule définition côté client pour le récapitulatif, la règle « suit la trame » et l'« Ajuster » de l'arbitrage. Ce prévu est recalculé à chaque lecture, sans instantané : limite acceptée, il suit la trame et l'équipe d'aujourd'hui, et change rétroactivement si le salarié en change après une modulation. Le formulaire ne calcule aucune règle de temps de travail : c'est le moteur de règles du serveur qui les applique, et la grille les affiche après l'enregistrement.
Décision : la composition d'une unité vient du serveur. C'est elle qui décide qui apparaît dans la grille, donc le périmètre de lecture du manager ; la laisser au client en ferait une règle d'habilitation appliquée côté navigateur. Le modèle est le Service (Division) et son affectation datée (DivisionAssignment).
Les alertes conventionnelles (CCN-66) sont calculées dans cette même réponse, puisqu'elles portent sur des journées que seule la résolution connaît. Elles sont trois règles écrites en dur — durée hebdomadaire, repos quotidien, amplitude — et non un référentiel paramétrable : le paramétrage viendra quand un besoin réel le demandera.
L'écran garde un jeu de données simulées, activable par un drapeau (VITE_ENABLE_MOCK), qui sert les tests end-to-end et les situations que la base ne sait pas encore produire. Trois de ses commandes écrivent : l'enregistrement d'une modulation, l'affectation d'une journée libre — une case qu'aucune trame ne planifie, directement ou depuis la ligne « Poste à pourvoir » qui clôt chaque unité, le salarié étant alors choisi parmi ceux qui n'ont rien ce jour-là — l'attribution de cette ligne à un salarié, qui l'affecte à l'équipe du bandeau à partir du jour même, du même geste que Bibliothèque ▸ Équipes (décision : un poste vacant n'est pas un objet du modèle, il n'existe que des équipes et des journées, et une journée appartient à quelqu'un ; nommer quelqu'un sur cette ligne est donc l'affecter à l'unité — ce qui se lit dans son dossier et survit au rechargement, là où un état d'écran disparaissait) — et la déclaration d'une absence depuis le panneau de détail d'une journée : l'assistant chiffre la période et son effet sur les compteurs du salarié avant d'enregistrer, la corrige, la supprime, et reçoit ses justificatifs. Décision : une modulation n'exige pas de trame sous elle. Une journée est ce qu'on affirme qu'elle est ; la trame n'est que ce qu'elle aurait été, et son absence ne rend pas l'affirmation moins vraie. Ne sont pas construits — le parcours du salarié lui-même (demander plutôt que déclarer), les actions du bouton flottant, l'écriture d'un repos décidé hors trame depuis l'écran (il se pose au back-office), la publication et l'export, la sélection multiple de cases, et les vues Absences et Planning réalisé.
Décision (APAJH-280) : un salarié sans équipe se range sous son établissement. La grille se lit par équipe ; un salarié affecté à aucune n'y avait donc pas de ligne, et un nouvel embauché ne pouvait pas être planifié. Le calendrier ajoute, après les équipes, un bandeau calculé par établissement du périmètre, à son nom, qui réunit les salariés sous contrat sur la période dont il est le lieu de travail principal ou secondaire et qu'aucune affectation ne rattache à une équipe. Il n'est pas stocké : une équipe par défaut en base serait une équipe que personne n'a décidée, à tenir synchronisée avec chaque affectation. N'étant pas une équipe, il n'a pas de ligne « Poste à pourvoir ». Le salarié lui-même lit sa semaine dans ce bandeau, seul, et les alertes du tableau de bord comptent ces salariés comme la grille : le bandeau est calculé avec le périmètre, que les trois lecteurs partagent.
Ce qui reste à créer et ce qui reste à trancher sont détaillés dans references/planning.md.
Contrat requis pour une affectation ponctuelle
Depuis GitHub #136, les disponibilités et les créations/corrections de postes exigent un contrat couvrant la journée concernée. Les bornes sont inclusives et un seul contrat actif suffit, y compris pour un salarié ayant plusieurs contrats.
Un nouveau rattachement à une équipe exige un contrat à sa date d'ouverture, toujours aujourd'hui. Cela évite de planifier un salarié sorti ; cela refuse aussi le rattachement immédiat d'un futur embauché, même pour un poste ultérieur. Les membres existants peuvent être conservés ou retirés sans contrat actif. La suppression d'une modulation reste possible.
Décision : les journées dérivées d'une trame sont limitées aux périodes sous contrat, sans modifier l'ancrage du cycle. Un CDD connu arrête donc la production des postes et repos après sa date de fin, incluse. Une embauche en cours de semaine commence à sa date de début, même si l'affectation à la trame a commencé plus tôt. Les alertes et le planning prévu des compteurs utilisent cette même résolution.
Les contrats successifs ou simultanés sont lus comme une union de périodes : une interruption laisse des journées vides, un renouvellement reprend la position prévue du cycle, sans doublon. Une modification des dates du contrat prend effet à la prochaine lecture. L'affectation à la trame reste enregistrée avec ses dates initiales ; aucune migration ni suppression des modulations ou absences déjà enregistrées n'est engagée.
Bibliothèque de postes et évolution prévue
Depuis GitHub #137, les sélecteurs du planning proposent la bibliothèque personnelle non archivée. La période sert uniquement à décrire les journées existantes, y compris leurs postes archivés ou issus d'autres bibliothèques. Ce partage rend immédiatement utilisable un poste créé sans trame et préserve la lecture de l'historique.
Non construit : rattacher les postes et les trames à un ou plusieurs établissements. Cette évolution devra revoir leur structure, leur visibilité, leurs droits et la reprise des bibliothèques personnelles ; elle fait l'objet d'un chantier séparé. Aucune association à un établissement n'est déduite ici.
Le tableau de bord manager
Décision : le dashboard est une restitution des sources métier existantes. L’application dashboard ne possède pas de modèle ni de circuit de décision. Ses lectures indépendantes évitent qu’une panne d’agenda masque les dossiers à traiter. L’accueil manager se trouve à /tableau-de-bord, avec une URL stable sur bureau et téléphone. La bascule d’espace sur mobile est un choix explicite dans le menu ; la largeur de l’écran ne change pas les droits.
Les trois tuiles reprennent la présentation vert foncé de la maquette. Le bloc À traiter garde ses filtres au-dessus d’une liste à défilement vertical pour éviter qu’un volume important de dossiers étire tout l’accueil. Les panneaux conservent leur en-tête visible pendant le défilement et occupent toute la largeur d’un petit téléphone.
Les règles de sélection retenues sont les suivantes :
- Évènements : 30 jours calendaires, aujourd’hui inclus, en Europe/Paris. Les évènements en cours sont inclus ; un évènement horaire dont l’heure de fin est atteinte est exclu. Le périmètre est celui de l’agenda manager, évènements transversaux inclus. Le compteur et le panneau proviennent de la même lecture.
- Astreinte : celle en cours du salarié associé au compte, sinon la première à venir. Une fin exactement atteinte n’est plus en cours. Le panneau présente les dates et horaires de la période complète, les établissements couverts et un lien vers Astreintes. L’astreinte est indépendante des postes et repos du planning théorique : elle ne constitue pas du temps de travail effectué. Le temps travaillé lors d’une intervention relève d’une saisie distincte. L’absence de fiche salarié et l’absence de période sont distinguées.
- Demandes : les absences qui attendent une décision —
pending, et depuis APAJH-273 les annulations demandées (cancellation_pending), tranchées par les mêmes décideurs depuis le même panneau —, sans fenêtre annuelle, dans le périmètre manager. La lecture exigeabsences.view_absence_queue; la décision réutilise les droits, le contrôle de version et les transactions des commandes existantes. Une erreur conserve le contexte. Un conflit relit l’état sans réappliquer une décision. Les déclarations immédiatement validées restent hors de cette sélection. Depuis APAJH-323, les demandes de modification de journée en attente (ShiftChangeRequest) s’y ajoutent, sous la catégorie Modification, par la même routeGET /api/dashboard/requests/plutôt qu’une seconde : chaque ligne portekind(absenceoushift_change) et la demande sous la clé de ce nom, les deux sortes classées ensemble par leur premier jour, chaque sorte écrite par son propre sérialiseur (le schéma publie unoneOfdiscriminé surkind). Même périmètre, même permission de lecture que la file « Demandes » ; la décision ouvre le panneau « Demande à arbitrer » de la file, sousplanning.decide_shift_change. Les demandes du compte connecté en sont exclues, absences comprises : nul ne tranche les siennes (CanDecideAbsences,CanDecideShiftChanges), elles ne sont donc rien « À traiter » pour leur auteur, et restent comptées chez ses décideurs. La file « Demandes », elle, continue de les lister. - Dossiers sans matricule : salariés du périmètre avec au moins un contrat actif aujourd’hui, bornes incluses. Le contrat actif de référence est le plus récent par date de début, puis identifiant croissant. Son type Intérim exclut le dossier des alertes. Un contrat futur ou terminé ne suffit pas.
- Alertes de planning :
GET /api/dashboard/planning-alerts/restitue les alertes du moteur de règles que lit aussi la grille de Planning, sur 30 jours glissants, aujourd’hui inclus. Le périmètre vient des affectations aux équipes accessibles dans Planning, et non du seul établissement de rattachement du salarié — plus, depuis APAJH-280, les salariés affectés à aucune équipe que Planning range sous leur établissement. Les salariés sont dédupliqués entre équipes. Le calcul conserve les semaines complètes et les journées voisines nécessaires ; seules les alertes datées dans la fenêtre sont restituées. Le panneau ouvre la semaine et le salarié concernés au planning. - Classement : les gravités existantes
blocketwarndeviennent respectivement Bloquant et Forte. Les demandes et dossiers sans matricule restent Information. Chaque niveau possède un libellé et un pictogramme. L’ordre est l’urgence décroissante, puis la date métier croissante, la catégorie et l’identifiant stable. Le filtre Planning se combine avec l’urgence. - Dernière synchronisation paie : fin du dernier fichier réussi,
ImportAttempt.finished_atau statutsucceeded. Pour un rôle limité, l’import doit posséder une ligne dont le contrat appartient à un établissement de paie du périmètre courant (ImportRowResult.contract.payroll_establishment). Ce rattachement est l’état courant du contrat, pas une photographie historique. Un passage traitant plusieurs fichiers n’a pas d’horodatage agrégé affiché. Sans succès identifiable, le dashboard annonce l’absence d’historique.
Les API contrôlent le rôle manager avant de calculer son périmètre : l’ensemble vide de manager_establishments signifie toute l’association pour un compte habilité. Les attributions actives et les extensions d’astreinte existantes s’appliquent. Les caches se rafraîchissent après décision, déclaration ou correction d’absence, affectation de planning et modification de poste réussies, au retour sur le dashboard et à la reprise de la fenêtre. Une panne des alertes ne masque pas les autres sources. Une navigation ou un échec de correction ne résout aucune alerte.
Décision de raccordement APAJH-89 / APAJH-90 : conserver les calculs existants. Le cumul hebdomadaire supérieur à 44 h, le repos quotidien inférieur à 11 h et l’amplitude supérieure à 13 h sont repris sans changement de seuil ni de gravité. Ce raccordement ne certifie pas leur conformité à l’ensemble du référentiel : Référentiel_GTA_APAJH_V2.xlsx, onglet Alertes_màj_planning, indique 48 h pour AL-A06 (ligne 7), contre 44 h dans le moteur actuel et dans l’onglet Règles_Création_trames, référence R-B02. L’onglet des alertes reste la source de vérité désignée ; la correction de cet écart est reportée explicitement. La moyenne de 44 h sur 12 semaines (AL-A07, ligne 8) n’est pas calculée et n’est donc pas raccordée. L’amplitude reste le seuil existant, alors que le référentiel présente ce seuil comme à arbitrer. Aucun nouveau calcul ni blocage d’écriture n’est introduit par le dashboard.
Non construit : permutations, règles de couverture, autres alertes non calculées du référentiel et raccordement aux Exports. Les catégories concernées annoncent leur indisponibilité, sans compteur ni validation simulés. Le parcours salarié de dépôt des absences reste celui décrit dans la section Absences. L’absence de matricule est un état du dossier et ne prouve aucun échec technique d’import CEGI.
Recette : Tableau de bord manager, tickets APAJH-89, APAJH-90 et APAJH-323.
Le référentiel de l'organisation
Quatre niveaux, du plus large au plus fin :
| Niveau | Ce que c'est | Exemple |
|---|---|---|
| Site | Une implantation géographique | Un bâtiment, une adresse |
| Pôle | Un regroupement d'établissements par domaine d'accompagnement | Le pôle adultes |
| Établissement | L'entité qui reçoit le public, celle que la paye connaît | Un FAM |
| Service | Une subdivision interne d'un ou plusieurs établissements | Une unité de vie |
Un code de paye est un référentiel à part, rattaché à l'établissement. Il existe parce que la paye identifie un établissement par son code et non par son nom, et parce que ce code change sans que l'établissement change.
Décision : le site et le pôle sont deux axes indépendants. Un établissement a une implantation et une appartenance à un pôle, et rien ne garantit que deux établissements d'un même pôle soient sur le même site. Les modéliser comme une hiérarchie unique aurait forcé une des deux lectures à passer par l'autre.
Décision : un service peut couvrir plusieurs établissements. La plupart n'en couvrent qu'un, mais une équipe transverse (une astreinte partagée, par exemple) existe. Le code de paye et le pôle restent en revanche uniquement des attributs de l'établissement : un service qui en couvrirait plusieurs n'a plus de valeur unique à leur donner, et rien n'oblige les établissements d'un même service à partager le même code ou le même pôle.
Le dossier du personnel
Décision structurante : un salarié n'est pas un compte. Le compte (auth.User) porte l'authentification ; le salarié porte l'identité RH. Les deux sont des tables distinctes parce qu'elles répondent à deux questions différentes, et un compte peut exister sans salarié — un administrateur qui n'est pas dans l'effectif.
L'inverse, en revanche, n'est pas permis aujourd'hui : Employee.user est obligatoire, donc toute fiche salarié ouvre un compte. Ouvrir un dossier sans compte — un intérimaire dont on tient le dossier sans lui donner accès à l'application — demanderait de rendre ce lien optionnel, et n'est pas construit. Le compte créé sans mot de passe utilisable est le compromis retenu : il existe, il ne sert à rien tant que la personne n'a pas suivi le « mot de passe oublié ».
Décision structurante : trois axes de rattachement, autorisés à diverger.
- Le contrat porte l'établissement de paye. C'est lui qui détermine où la personne est payée, et dont le code de paye figure sur le bulletin.
- La fiche porte les lieux de travail — un principal et autant de secondaires que la personne en a. C'est une déclaration directe, saisie à la main, au grain de l'établissement et sans dates.
- Les affectations rattachent la personne à des services, avec des dates. Ce sont les équipes dans lesquelles elle intervient effectivement.
Les trois n'ont pas à concorder, et c'est le point : un chef de service technique peut être rattaché à l'organisation gestionnaire pour la paye tout en intervenant dans plusieurs établissements. Une règle qui exigerait la cohérence entre eux rendrait ce cas — courant — inexprimable. Le vocabulaire les sépare depuis APAJH-205 : lieu de travail pour la paire déclarative, établissement de paye pour celui du contrat.
Le périmètre d'une personne est donc calculé, pas stocké. À une date donnée, les services viennent des affectations actives et les établissements viennent à la fois de ces services et de l'établissement de paye des contrats actifs. La route /api/employees/<pk>/perimeter/ répond à cette question ; elle est une action dédiée et non un champ de la liste, parce que la réponse dépend d'une date.
Les données de famille sont réduites au strict nécessaire. Par défaut, un enfant à charge n'est connu que par son nombre et son âge. Nommer et dater précisément des enfants n'est pas utile à la gestion du temps de travail, et constitue une donnée personnelle de tiers mineurs qu'il n'y a pas de raison de détenir (§ 05).
Le profil personnel — APAJH-290
Décision : consulter sa propre fiche dans un parcours toujours en lecture seule. L'entrée Mon profil du menu de compte, ou la puce utilisateur mobile, ouvre /mon-profil/informations dans Mon espace. Le dossier provient de employee_id sur /api/auth/me/, sans identifiant de salarié dans l'adresse. Un compte sans dossier reçoit un message explicite.
Les cinq volets réutilisent la fiche existante : Informations, Contrat, Famille & social, Urgence, Lieux de travail & équipes. Le verrouillage est imposé au parcours personnel, même si le rôle autorise l'édition dans l'annuaire. Les permissions et le masquage des champs restent ceux des API existantes. Aucun nouvel endpoint ni nouveau droit n'est introduit.
Le bandeau garde l'identité, la fonction, l'établissement, le contrat, la quotité et l'ancienneté. Les compteurs ont déjà leur écran personnel dédié : le profil ne les affiche ni ne les charge. Les cinq adresses de volet survivent au rechargement ; un volet inconnu revient à Informations.
Les fenêtres de validité
Contrats, affectations et attributions de rôle portent tous une paire date_start / date_end, et la même sémantique :
- une date de fin vide signifie « toujours en cours », pas « terminé » ;
- une date de début vide signifie « depuis toujours » ;
- « actif » se calcule à une date, jamais « en ce moment » figé en base.
Cette logique est partagée par un module unique (apps/common/periods.py) plutôt que réécrite par domaine : trois modèles répondent à la même question, et trois implémentations auraient dérivé.
Conséquence sur les habilitations : un rôle peut être attribué pour une période et pour certains établissements seulement. Un remplacement d'été se déclare une fois, avec ses dates, et cesse de donner accès de lui-même.
Astreintes — APAJH-104/105/107
L'écran Astreinte permet aux encadrants et aux RH de consulter les salariés concernés de leur périmètre (§ 05) et de préparer un ensemble d'ajouts, corrections et suppressions. Le Service Paie consulte uniquement. L'éligibilité est portée par le contrat ; elle n'accorde aucun accès à cet écran au salarié concerné. La consultation inclut les contrats éligibles actuels ou futurs et les dossiers possédant déjà une astreinte.
Une période est une couverture continue associée à un ou plusieurs établissements, indépendamment des équipes habituelles du salarié. Du vendredi au lundi, 19 h–7 h, désigne une couverture du vendredi à 19 h au lundi à 7 h. Sur un seul jour, une fin inférieure ou égale au début désigne le lendemain. Les calculs utilisent Europe/Paris. Les nouvelles bornes ambiguës ou inexistantes lors du changement d'heure sont refusées.
La création et la correction exigent une couverture complète par un ou plusieurs contrats autorisant l'astreinte, sans interruption. Les chevauchements sont refusés, les périodes adjacentes sont acceptées. Une perte ultérieure d'éligibilité ne masque pas les périodes : leur conservation inchangée et leur suppression restent possibles. Les périodes terminées restent dans l'historique mais ne comptent pas dans Prochaine.
Les changements d'un salarié sont atomiques et versionnés. Un conflit concurrent refuse l'enregistrement sans perdre la saisie. Le back-office applique les mêmes règles et participe à la même révision. Les données antérieures sont conservées sans inventer d'établissement couvert ; une correction demande de compléter cette information.
L'astreinte se superpose au planning sans changer les postes, repos ou absences. Les contacts sont visibles dans les établissements couverts, y compris pour une personne extérieure aux équipes. Interventions, indemnités, paie, compteurs, notifications, exports et approbation ne font pas partie de ce module.
Les absences
Un référentiel que l'APAJH alimente elle-même, et les périodes déclarées dessus. Un type d'absence porte son code (le badge de la case du planning), son unité de saisie, son caractère rémunéré, son caractère fractionnable, son plafond annuel et les types de contrat qu'il concerne.
Décision : le référentiel est administrable et préconfiguré par la matrice validée. Les motifs fournis par la RH sont créés sans écraser un code existant portant un autre sens. L'APAJH conserve la possibilité de compléter, ordonner ou désactiver le catalogue au back-office.
Décision : la classification est une table, pas des colonnes. Catégorie et sous-catégorie vivent dans leur propre table à deux niveaux, et un type pointe vers une feuille de cet arbre. Deux colonnes plates auraient laissé écrire la paire incohérente (« Familiale » + « Maladie ») ; la clé étrangère l'interdit, et la catégorie se relit par le parent. Aucune règle de calcul ne s'accroche à ce classement : il sert l'affichage et le filtrage, rien d'autre.
Décision : deux unités de saisie, le jour et l'heure — la demi-journée n'en est pas une. Elle est une paire de bornes sur la déclaration (« à partir de » le matin ou l'après-midi, « jusqu'à » de même), par défaut la journée entière. Un congé payé se compte en jours et peut pourtant commencer un mercredi après-midi : une unité « demi-journée » aurait forcé chaque type à être entièrement à la demi, ou pas du tout. Le plafond annuel est exprimé dans l'unité du type, sans colonne d'unité propre qui en serait la seconde source. Révision (APAJH-302) : un type peut refuser les bornes à la demi-journée.AbsenceType.allows_half_day, décoché sur les congés (CP, CT, ANC, JRS, CET, R+), fait se poser le motif à la journée entière : le même contrôle que celui qui refuse une demi-journée sur un type en heures, et l'assistant ne propose plus la période. Une case du référentiel plutôt qu'une règle sur la catégorie « Congés », dont le libellé appartient aux RH ; les absences déjà posées par demi-journées restent valides et s'accordent telles quelles.
Décision : les contrôles à la saisie, pas de moteur. Chevauchement, cohérence entre l'unité et les heures, éligibilité au type de contrat en vigueur au premier jour, et dépassement du plafond de l'année civile — ce dernier en avertissement ou en refus, selon ce que le type déclare. Le caractère fractionnable, lui, avertit sans bloquer : la fenêtre juridique est souvent l'événement (une naissance) et non l'année, si bien que deux naissances la même année feraient refuser à tort le second congé. Les populations concernées (parents, aidants…) sont indicatives et ne conditionnent aucun contrôle — l'application ne sait pas qui appartient à quelle population, et l'apprendre serait pour certaines une donnée de santé avec sa propre base légale.
Décision : l'absence est une source versionnée, et le workflow se branche dessus. La frontière a été posée d'emblée — quatre états (pending, validated, refused, cancelled), cinq depuis l'annulation demandée (cancellation_pending), que le service de compteurs sait router, et une version que chaque écriture incrémente — pour qu'une file de validation vienne s'y appeler sans rouvrir le décompte. Elle est venue, et n'a rien rouvert : décider une demande est une écriture de plus sur la même source, donc une version de plus et un événement de compteur qui contre-passe le précédent.
Trois choses en découlent, et la dernière est la moitié qui manque.
Décider est un geste distinct de lire. Deux permissions et non une : voir la file du périmètre, et trancher ce qu'on y voit. Un directeur adjoint lit où en est son établissement sans accorder de congés — § 05.
Accorder autre chose que ce qui a été demandé est une seule écriture. Un valideur peut requalifier le motif et re-périodiser la demande avant de dire oui, et cela part en un appel, jamais en une correction suivie d'une validation : deux appels écriraient deux versions, donc deux mouvements de compteur pour un seul geste, et un échec entre les deux laisserait une demande transformée que personne n'a accordée. Ce qui est accordé rejoue toutes les règles de la déclaration — requalifier vers un motif fermé au contrat, ou compté dans une unité que la période ne peut pas porter, est refusé plutôt que tronqué. Une décision qui accorde autre chose doit son motif au salarié, dans la même colonne que celui d'un refus.
Ce qui distingue demander de déclarer, c'est l'auteur, et le serveur seul en décide. Écrire pour soi-même, c'est demander : la ligne naît « en attente » et attend qu'on la tranche. Écrire pour un salarié de son périmètre, c'est déclarer, et une déclaration est sa propre décision. La règle se déduit du compte qui écrit et de la fiche visée — jamais d'un drapeau dans la charge, qu'un client pourrait falsifier — si bien qu'un encadrant qui déclare sa propre absence dépose lui aussi une demande : la sienne est arbitrée par sa hiérarchie comme celle de n'importe qui. Une seule route et un seul sérialiseur servent les deux publics ; ce qui change est l'état de naissance, pas le chemin. C'est aussi pourquoi la file arbitre depuis deux écrans — la sienne, et le calendrier, où le lien d'une demande rouvre la décision dans la semaine qui l'entoure : trancher demande de regarder autour, et le panneau est modal.
Décision : un encadrant modifie un poste passé jusqu'au 1er décembre de l'année précédente. Modifier un poste, c'est moduler une journée ou écrire une absence ; l'un et l'autre refusent ce qui est antérieur à une borne. Pour les six rôles d'encadrement (Admin, RH n1, RH n2, DA, Manager n1, Manager n2), la borne est le 1er décembre de l'année précédente : l'année en cours se corrige entièrement, et décembre est le mois dont la paie se clôt en janvier, le dernier qu'une correction peut encore atteindre. Pour tout autre compte — Service Paie compris, qui règle une question de paie et ne réécrit pas le planning — la borne reste aujourd'hui. La règle est portée par une permission, planning.edit_past_schedule, et non par une liste de rôles : un administrateur l'ajuste depuis l'écran des groupes. Une ligne est jugée sur sa date enregistrée et sur sa nouvelle date, sans quoi déplacer vers le futur une ligne hors d'atteinte contournerait la borne. Donner un poste passé à un salarié qui n'est pas de l'équipe le fait entrer dans l'équipe à partir de ce jour-là, sous la même borne : le planning ne dessine un membre qu'à compter de son arrivée, et une journée antérieure quitterait le bandeau. Depuis l'en-tête de l'équipe, qui ne désigne aucun jour, l'arrivée reste datée d'aujourd'hui. L'affectation à une trame n'est pas un poste et garde « aujourd'hui » pour tous. Le back-office reste exempté, pour corriger une erreur plus ancienne que la fenêtre.
Décision : une demande remonte jusqu'à J-14, encadrant compris. La borne ci-dessus protège l'écriture directe dans l'histoire ; une demande n'écrit rien, elle réserve, et un humain tranche — elle peut donc remonter plus loin qu'une déclaration, sans quoi les arrêts de travail (MAL, ATMP), que l'on déclare le jour du retour, seraient hors d'atteinte pour la seule personne qui les vit. Mais pas sans fin : J-14 inclus, pour tout auteur qui demande pour lui-même. Un encadrant n'y garde pas sa fenêtre du 1er décembre : demander pour soi relève de « Mon espace », où il est un salarié comme un autre, et sa fenêtre sert à écrire le planning des autres — y compris à corriger la demande en attente d'un salarié. La borne lit le premier jour de la demande, pour tous les motifs, arrêts compris : un arrêt commencé plus tôt se déclare par l'encadrant ou au back-office, ce qui est un choix et non un oubli. La demande de modification de journée suit la même borne (section suivante). Symétriquement, une demande qu'on n'a pas encore tranchée se retire même si son premier jour est passé, alors qu'un congé accordé ne s'efface jamais par son auteur : il en demande l'annulation, et seulement tant qu'il n'a pas commencé.
Le salarié annule, il ne corrige pas. Il retire sa demande en attente (« Annuler la demande »), demande l'annulation du congé qu'on lui a accordé (« Demander une annulation »), et redépose si la période change. Corriger une demande sous arbitrage est le geste de qui la tranche ; effacer un refus ne lui appartient pas non plus — c'est la trace d'une décision que quelqu'un a prise.
Décision : le salarié demande avec l'assistant de l'encadrant. « Mon espace ▸ Mes demandes » liste ses demandes, et son bouton « + » ouvre Demander une absence (/mes-demandes/nouvelle) : les trois étapes mêmes de la déclaration depuis le planning — motif, période, récapitulatif —, et non un second formulaire qui dériverait du premier. Ce que la demande y change est porté par une seule indication de l'écran hôte, jamais déduite du rôle : le calendrier grise ce qui précède J-14 (le serveur, qui applique la borne, reste juge et son refus s'affiche tel quel), le dernier bouton envoie, et un écran « Demande envoyée » confirme le dépôt. Pour chiffrer sa demande avant de l'envoyer — durée, solde du compteur avant et après —, le salarié a accès à la prévisualisation, sur sa seule fiche ; pas à celle d'une correction, qu'il n'a pas. Le motif dit ce qu'il implique (justificatif attendu, validation) avant d'être choisi. Ce que le salarié ne peut pas faire depuis cet écran : corriger une demande déposée — il la retire et en redépose une.
Décision : sur sa propre absence, un encadrant a les droits d'un salarié. Son absence est tranchée par sa hiérarchie, et sa fenêtre du 1er décembre sert à écrire le planning des autres. Il ne corrige donc pas la sienne, en attente ou accordée — pas plus qu'il ne déplace sur sa propre fiche l'absence d'un autre, ce qui reviendrait à se déclarer une absence validée —, et il ne la retire que dans le cas du salarié : une demande en attente ; un congé accordé, il en demande l'annulation. Il ne tranche pas non plus sa propre demande, même quand il détient le droit de décider sur son établissement : elle est arbitrée par sa hiérarchie. C'est la fiche qui est lue, pas le rôle, comme pour l'état de naissance d'une demande.
Décision : une demande prévient qui peut la trancher, et lui seul. Au dépôt, la cloche sonne les comptes qui détiennent le droit de décider et dont la file affiche la demande : la règle de périmètre de la file (lieu de travail principal ou secondaire, équipe du jour), lue dans l'autre sens et non réécrite, de sorte que « qui est prévenu » et « qui voit » ne peuvent pas diverger — un compte qui peut trancher sans pouvoir lire la file n'est pas sonné. Parmi eux, ceux qui sont rattachés à un établissement du salarié passent d'abord ; les décideurs sans établissement, qui couvrent toute l'association, ne sont sonnés qu'à défaut — sans quoi chaque congé sonnerait tous les RH. L'auteur ne l'est jamais, puisqu'il ne tranche pas sa propre demande, et un superutilisateur sans rôle non plus : c'est un compte technique, pas un décideur, et le compter masquerait toute demande orpheline. La décision sonne le salarié, motif compris, et nomme ce qui a été accordé quand ce n'est pas ce qui avait été demandé. Retirer une demande encore en attente ne sonne personne : personne ne l'avait lue. Une demande qu'aucun décideur ne couvre n'est pas refusée — le salarié n'y est pour rien — mais elle ne part pas en silence : la réponse du dépôt le dit à l'écran, et la cause est signalée à qui peut la réparer au back-office — les Admin, à défaut les RH, à défaut un superutilisateur ; à défaut de tout compte, au journal applicatif. Le dépôt sonne dans sa transaction : une sonnerie qui échoue annule le dépôt, que le salarié peut alors renvoyer. Sonner après le commit répondait une erreur pour une demande pourtant déposée, que le second envoi trouvait « déjà en attente », sans que personne ait été prévenu. Le prix : la fiche du salarié, verrouillée par le dépôt, le reste le temps de quelques requêtes par décideur candidat.
Reste à construire : la demande qui devient orpheline après coup. Seul le dépôt est contrôlé. Un rôle qui expire ou un lieu de travail retiré peut laisser une demande en attente qu'aucune file n'affiche plus, sans que personne en soit averti.
Décision : annuler un congé accordé est une demande, pas un effacement (APAJH-273). Retirer une demande que personne n'a tranchée ne reprend rien à personne, et reste une suppression. Retirer un congé accordé défait une décision — l'encadrant avait arbitré, comblé le poste, prévenu l'équipe —, donc cela se demande et s'arbitre : le salarié passe la ligne en annulation demandée (cancellation_pending), avec un commentaire facultatif rangé à part (cancellation_reason), tant que le congé n'a pas commencé ; un décideur l'accorde — la ligne passe annulée et reste au dossier, compté sous la cinquième puce « Annulées » de l'écran salarié — ou la refuse sous un motif obligatoire, et le congé tient. Un état et non un drapeau posé à côté de validated : la file filtre sur l'état, l'écran salarié compte par état, et un drapeau parallèle aurait obligé chacun à dériver un état composite. Deux verbes dédiés (grant-cancellation, refuse-cancellation) plutôt que validate / refuse changeant de sens selon l'état, pour que la piste d'audit dise sans ambiguïté ce qui a été tranché ; la permission est la même, decide_absence. Tant que la décision n'est pas rendue, le congé est encore accordé : les compteurs le routent exactement comme validated — sans quoi le salarié récupérerait ses jours en demandant, et les garderait sur un refus —, le planning le dessine, et les règles le comptent. Le planning le marque pourtant du même cercle orange qu'une demande en attente, porté par les blocs de l'absence et non par le poste prévu (pending_request.kind, cancellation) : dans les deux cas un décideur a quelque chose à trancher ce jour-là, et un clic sur la case ouvre l'arbitrage. Le lien « Voir au planning › » de la file, le ?absence= du planning et le tableau de bord traitent l'annulation demandée comme une demande en attente. Le panneau qui la tranche rappelle le congé validé et annonce ce que l'accord rendrait aux compteurs — lu dans le journal, les mouvements que le congé a réellement écrits contre-passés (cancellation_counter_effects), et non une nouvelle mesure du congé : c'est exactement ce que l'accord écrira, même si les règles ou le contrat ont changé depuis. Une ligne annulée ou refusée, elle, ne retient plus rien : ni le chevauchement, ni le plafond annuel, ni le fractionnement ne la lisent, et ses jours se redemandent. La demande d'annulation sonne les mêmes décideurs qu'un dépôt, la décision sonne le salarié. Supprimer un congé accordé, c'est l'annuler : un encadrant qui le supprime du planning le passe « annulée » et le conserve, comme une annulation arbitrée, et le salarié en est prévenu — sans quoi la même fin de congé laisserait deux traces selon la porte prise. Le back-office ne propose pas « Supprimer » sur un congé accordé, mais une action « Annuler » qui fait de même : la confirmation et le message de suppression de Django annonceraient un effacement qui n'a pas lieu. Seule une ligne qui n'accordait rien (demande en attente, refus) est encore effacée par l'API. Une ligne annulée ne l'est pas : c'est la trace que le salarié retrouve sous « Annulées », et l'effacer ne lui laisserait que le journal d'audit, qu'il ne lit pas. Seul le back-office, lieu de réparation des erreurs, peut encore la supprimer.
Décision : ce que la matrice attend et ce qui a été fourni sont deux choses. Les justificatifs et validations attendus restent des métadonnées du référentiel — la prose du type d'absence, qui dit quelle pièce produire. Le stockage, lui, existe : une pièce déposée est une ligne à part, une absence en portant plusieurs (« acte de décès et justificatif de lien de parenté »), ce qu'un champ fichier unique sur l'absence n'aurait pas permis. Une pièce n'a pas d'existence hors de l'absence qu'elle justifie : la supprimer emporte ses pièces, et annuler un congé accordé aussi — la ligne reste au dossier, ses pièces non —, ce qui est la garantie qu'une absence annulée ne laisse pas un certificat médical derrière elle. Et son écriture est atomique avec le décompte — les fichiers sont enregistrés dans la même transaction que les mouvements de compteurs, avant eux, de sorte qu'une déclaration que les compteurs refusent ne laisse ni la ligne ni son certificat. Qui peut en lire le contenu est une décision de sécurité : § 05.
Les demandes de modification de planning
Le salarié demande qu'une de ses journées soit travaillée autrement : d'autres heures, un créneau de plus ou de moins, une pause rémunérée, du télétravail. La demande n'écrit rien dans le planning : la journée reste celle de la trame ou de la modulation tant que personne n'a tranché. Recette : Demandes de modification.
Décision : une demande a la forme d'une modulation. Un motif parmi les trois de « Modifier la journée » (Remplacement, Demande personnelle, Urgence), les options du jour (télétravail, hors établissement, temps de trajet inclus), un commentaire, et de zéro à plusieurs créneaux portant chacun ses heures, son poste, son établissement et sa pause. Les créneaux sont une table à part, validée par les règles mêmes des créneaux d'une modulation, plutôt qu'un champ JSON. Une demande sans créneau garde la journée telle qu'elle est — les heures de la trame ou celles d'une modulation, son établissement compris — et n'en change que les options : le salarié demande sur ce qu'il voit, et l'accord ne doit pas effacer en silence ce qu'un encadrant avait posé. Une demande de repos n'existe pas — un jour off se demande comme une absence. Elle vit dans le domaine planning (apps.planning), à côté de la modulation qu'elle deviendra si elle est accordée.
Décision : la journée est celle du compte, jamais celle de la charge. Le serveur déduit la fiche salarié du compte qui dépose ; un compte sans fiche reçoit un refus. Pour la journée d'un autre, l'encadrant écrit lui-même la modulation.
Décision : le salarié ne choisit ni poste ni établissement. La bibliothèque de postes est personnelle à l'encadrant. Le formulaire reprend les créneaux actuels de la journée, avec leur poste, leur établissement et leur pause, pour que l'accord ne les perde pas ; le serveur refuse un poste ou un établissement qui ne figure pas déjà sur la journée, et une demande qui ne change rien.
Décision : une seule demande en attente par salarié et par jour. MySQL ne sait pas exprimer une contrainte d'unicité conditionnelle ; la règle est tenue au dépôt, sous le verrou de la fiche salarié, comme le chevauchement des absences.
Décision : la demande garde une photo de la journée. Au dépôt, elle enregistre la journée planifiée en toutes lettres et sa durée nette, et la durée nette demandée. Une fois accordée, la journée est la demande : sans cette photo, « Prévu → Demandé » perdrait sa colonne de gauche.
Décision : le salarié retire, il ne corrige pas. Une demande en attente se retire sans borne de date : la ligne disparaît, le journal d'audit la garde, créneaux compris. Une demande tranchée reste : refusée, elle porte son motif ; validée, elle est devenue le planning, et revenir en arrière passe par une nouvelle demande. Personne d'autre que son auteur ne la retire.
Décision : la lecture suit celle des absences. Chacun lit ses propres demandes ; lire celles des autres exige la permission de la file des absences, sur le même périmètre d'établissements. La demande d'un collègue répond « introuvable ».
Décision : une demande accordée devient la modulation de sa journée. Valider écrit le ShiftOverride du jour, ou remplace celui qu'un encadrant y avait posé : il n'y a jamais deux réponses à « que fait-on ce jour-là ». L'écriture passe par les règles mêmes de « Modifier la journée » — contrat en cours, compte ouvert, forme des créneaux — et sous la fenêtre de la personne qui décide, non sous le J-14 du salarié : c'est elle qui écrit le planning. Le recalcul de l'annualisation et la trace au journal sont ceux d'une modulation saisie à la main. Une règle refusée annule la décision entière : la demande reste en attente et rien n'est écrit.
- Les créneaux demandés remplacent ceux de la journée, avec leur poste, leur établissement et leur pause. Le motif et les options du jour sont recopiés sur chaque créneau : dès qu'une journée a des créneaux, le planning lit ses options sur eux, et un télétravail laissé au seul niveau du jour disparaîtrait sans bruit.
- Une demande sans créneau garde la journée telle qu'elle est : les créneaux d'une modulation restent, sous les nouvelles options ; une journée sans créneau garde ses heures et son établissement.
- Une demande accordée est une journée travaillée : elle lève un repos ponctuel posé depuis le dépôt.
- La photo est réécrite à l'accord : « prévu » devient la journée telle qu'elle était juste avant d'être remplacée, et « demandé » la journée que l'accord a écrite, relue plutôt que prédite — un accord qui lève un repos ponctuel rend ses heures à la journée. C'est ce que « Prévu → Demandé » montrera.
Décision : trancher est une permission distincte de lire, et jamais sur sa propre fiche. Valider ou refuser exige planning.decide_shift_change, accordée aux cinq rôles qui tranchent une absence ; le DA lit la demande sans la trancher. Un encadrant ne tranche pas sa propre demande : sa hiérarchie le fait. Sur l'API, ces deux règles répondent avant même la lecture de la charge, et avant tout verrou ; elles sont énoncées une fois et vérifiées aussi par la décision elle-même, que le back-office appelle sans passer par l'API. Un refus exige un motif, que le salarié lira. Une décision porte la version de la demande lue : si quelqu'un a tranché entre-temps, elle est refusée (409) plutôt qu'appliquée à une demande qui a changé.
Décision : le back-office corrige et tranche une demande en attente, par la même décision que l'API. Une RH corrige le motif, les options, le commentaire et les créneaux d'une demande en attente — sans la borne J-14 ni la limite au poste de la journée, qui bornent le salarié et non elle. C'est pourquoi nul n'y corrige sa propre demande : il la retire et en dépose une autre. Chaque correction est tracée, créneaux compris, fait avancer la version, ce qui refuse la décision d'un encadrant qui lisait l'état d'avant, recalcule la durée demandée, et marque la demande « corrigée » (par qui, quand) : c'est ce marqueur, et non la version, qui rend le motif obligatoire à l'accord. « Valider la demande » et « Refuser la demande » appellent la décision de l'API, à l'identique : qui tranche, la version, l'écriture de la modulation, son annualisation et le journal. Le refus exige un motif, et l'accord d'une demande corrigée au back-office aussi : le salarié reçoit autre chose que ce qu'il a demandé, il doit savoir pourquoi. Le motif ne s'écrit qu'avec la décision : un simple enregistrement laisserait un motif de décision sur une demande que personne n'a tranchée. Toute décision refusée annule l'envoi entier, correction comprise. Une demande tranchée se lit et ne se modifie plus. On ne crée pas de demande au back-office — elle appartient au salarié qui la dépose — mais on peut en supprimer une en attente, pour réparer, sous les verrous de la décision ; la suppression est tracée. Une demande tranchée ne se supprime pas.
Décision : une demande de modification prévient comme une demande d'absence. Les mêmes règles, et le même code (apps.employee.request_notifications) : le dépôt sonne les détenteurs de planning.decide_shift_change dont la file affiche la demande, les rattachés d'abord, jamais l'auteur ; une demande que personne ne couvre est déposée quand même, l'écran d'envoi dit « enregistrée » au lieu de « transmise », et l'auteur comme ceux qui peuvent réparer sont prévenus ; la décision sonne le salarié, motif compris, et dit « validée avec modification » en nommant les créneaux accordés quand l'accord porte autre chose que la demande — ajustée par le décideur, ou corrigée au back-office avant d'être accordée. Elle sonne depuis la décision elle-même, que l'API et le back-office appellent tous deux : le back-office ne peut pas l'oublier. Le retrait ne sonne personne.
Décision : le salarié suit ses modifications avec ses absences. « Mon espace ▸ Mes demandes » lit ses deux dossiers et les mêle en une seule liste, la dernière demande déposée en premier quelle que soit sa sorte, sous les mêmes puces : une modification n'a que trois des cinq états d'une absence (en attente, validée, refusée), et jamais « annulée » — retirée, elle est effacée. Sa carte montre la photo de la journée (« Prévu » — prise au dépôt, réécrite à l'accord) contre les créneaux demandés, pauses comprises (« Demandé »), puis les options, le motif d'un refus et le commentaire du salarié. Une demande en attente se retire de là ; une demande tranchée n'y a aucun bouton. Les deux sortes sont lues côte à côte et non par un point d'entrée commun : l'écran se charge entier ou pas du tout, une liste privée d'une sorte laissant croire qu'elle n'a pas de demande.
Décision : le formulaire part de la journée que le dépôt lira. « Demander une modification » (/mes-demandes/modification, sous le « + » de « Mes demandes », aux deux densités) s'ouvre sur un jour à J-14 au plus tôt, puis sur ses créneaux tels qu'ils sont, lus sur GET /api/shift-change-requests/day/?date= : la résolution même à laquelle le dépôt compare la demande, et non la période du planning. Celle-ci décrit un créneau modulé sans le poste de bibliothèque qui le fonde : un formulaire pré-rempli depuis elle renverrait une journée qui diffère de celle du serveur sur un champ que le salarié n'a pas touché, et l'accord effacerait ce poste. Un créneau pré-rempli garde son poste et son établissement, affichés en lecture seule ; un créneau ajouté n'en porte pas ; le salarié tape les heures et une pause rémunérée comme l'encadrant. Des créneaux laissés tels qu'ils se sont ouverts partent comme aucun créneau : la journée garde alors les heures qu'elle aura au moment de l'accord, et seules ses options changent. « Envoyer la demande » reste inactif tant que rien ne change — ni le motif ni le commentaire ne comptent, le serveur non plus ; des heures passées d'un créneau servi à un autre comptent, puisque le poste ne suit pas — et une demande ne vide pas la journée : ne pas travailler se demande comme une absence. Sur un jour sans poste, une option seule ne change rien non plus. La lecture du jour applique les bornes du dépôt (J-14, contrat en cours) : le formulaire ne s'ouvre pas sur un jour que l'envoi refuserait. Elle est refaite à chaque ouverture et jamais sous le formulaire ouvert, pour que le brouillon et les postes qu'il renvoie viennent de la même lecture.
Décision : les options d'une demande sont celles du jour. L'accord les recopie sur chaque créneau. Sur une journée dont les créneaux portent des options différentes — un encadrant a mis l'après-midi seul en télétravail —, une case n'est cochée que si tous les créneaux la portent, et le formulaire prévient que la demande appliquera les options cochées à toute la journée.
Décision : l'encadrant lit les modifications dans la file des absences. Activité ▸ Absences devient Activité ▸ Demandes (/demandes, sans redirection : aucune notification ne pointait vers l'ancienne adresse) et liste les deux sortes de demande dans un seul tableau, sous les mêmes filtres — état, année et mois, établissement, équipe, salarié — plus un filtre de type (Tout, Absences, Modifications), placé en tête, avant l'état. Les deux dossiers sont lus ensemble, sous l'état et la fenêtre choisis, et mêlés par la date du jour concerné : changer de mois ou d'état remplace les deux listes d'un coup, jamais l'une avant l'autre. Une lecture des modifications qui échoue est signalée à part et laisse les absences listées. Les modifications sont bornées au périmètre de l'encadrant, comme les absences : le point d'entrée sert aussi ses propres demandes, qui n'ont rien à faire dans sa file s'il est rattaché hors de son périmètre. « Annulations » ne liste que des absences : une modification n'est jamais annulée, l'écran ne la lit pas et y fige le filtre de type sur « Absences » — figé plutôt que masqué, pour que le filtre d'état ne se déplace pas sous le clic. La page reste gardée par la permission de la file des absences, qui gouverne déjà la lecture des modifications. Les modules gardent leur nom Absences* : les renommer est un refactor à part, quand une troisième sorte de demande — la permutation — arrivera.
Décision : l'encadrant tranche une modification là où il la lit. Un clic sur la ligne ouvre « Demande à arbitrer », le panneau des absences dans sa forme « Théorique → Demandé » : la journée telle qu'elle est au moment de la lecture — ce que l'accord remplacerait, relu sur la demande seule (current_day), et non la photo du dépôt —, face aux créneaux demandés. Le panneau signale une absence accordée ce jour-là, qui primerait sur la modulation écrite : c'est le serveur qui la nomme (granted_absences), par la règle même du calendrier, plutôt qu'un écran qui en recopierait la préséance. Valider envoie la version lue (409 si quelqu'un a tranché entre-temps) ; refuser exige un motif. Le pied n'existe que pour qui détient decide_shift_change, et jamais sur sa propre fiche. « Voir au planning › » porte ?modification=, qui rouvre le même panneau dans la semaine de la demande et se consomme à la décision ou à la fermeture, comme ?absence=.
Décision : « Ajuster le poste avant validation » accorde une autre journée, en un seul appel. Le lien ouvre, sous la carte, les créneaux de « Modifier la journée » — le sélecteur de poste de l'encadrant, la pause, les options et le lieu de chaque créneau —, semés de la demande, ou de la journée actuelle quand elle n'en porte aucun : rien de ce que la journée portait ne se perd à l'ouverture. La validation emporte alors la journée remaniée (override, le corps même de POST /api/shift-overrides/), que le serveur écrit sur le salarié et le jour de la demande, quoi qu'elle nomme, par le chemin de tout accord : règles de la modulation, fenêtre du décideur, annualisation, journal. Pas de modulation enregistrée avant la décision, comme le fait l'« Ajuster » d'une absence : un accord refusé ne laisse rien derrière lui. La demande garde les créneaux et la durée demandés, et l'accord ajusté photographie à côté la journée accordée (granted_description, granted_minutes) : « Mes demandes » montre au salarié ce qui a été accordé, et de combien cela s'écarte de sa demande — même si la journée est modulée de nouveau par la suite. Accorder autre chose que la demande exige un motif — l'ajustement, et l'accord d'une demande corrigée au back-office. La règle vit dans la décision, sous le verrou, que l'API et le back-office appellent tous deux : aucun des deux ne peut l'oublier, et seule la ligne verrouillée sait si la demande a été corrigée. Le panneau le sait aussi (corrected_at) et demande le motif avant d'envoyer.
Décision : au planning, une modification demandée marque la journée sans la retirer. Comme une demande d'absence, elle ne prime sur rien : la case garde le poste prévu et prend le même cercle orange (pending_request.kind, shift_change), et un clic ouvre « Demande à arbitrer » pour qui détient decide_shift_change — le détail de la journée pour tout autre compte, y compris qui ne tranche que les absences. Deux différences, voulues :
- le Total de la ligne garde la journée. Une absence demandée la retirerait si elle était accordée ; une modification la fait travailler autrement, mais travailler quand même ;
- une case n'a qu'un anneau et qu'un clic : quand une absence et une modification sont demandées le même jour, c'est l'absence qui le porte — si elle est accordée, la question de l'horaire tombe d'elle-même. La file « Demandes » liste les deux.
Une absence accordée sur la journée entière ne laisse aucun poste prévu à cercler, et la case n'en porte donc pas ; sur une demi-journée, le créneau travaillé à côté garde l'anneau, et celui que l'absence neutralise entièrement ne le prend pas. Seule la lecture de la période pose cet anneau, en une requête : les autres lecteurs du calendrier — règles de temps de travail, disponibilités, astreintes, compteurs — ne la paient pas. Le décideur qui clique sa propre journée en attente ouvre son détail : il ne tranche pas ses propres demandes.
Non construit : des options par créneau sur une demande, qui garderaient les options différentes d'une journée coupée. Ce lot livre le dépôt, la lecture, le retrait et la décision, sur l'API et au back-office, le suivi dans « Mes demandes », le formulaire du salarié, ses notifications, la file de l'encadrant, où il lit, ajuste et tranche, l'anneau au planning et la catégorie « Modification » du tableau de bord.
Les évènements et formations
Un évènement possède un type administrable, une plage de dates inclusive, un lieu en texte libre, une description, un responsable et une liste de participants. Deux heures nulles signifient une journée entière, éventuellement sur plusieurs dates ; deux heures présentes décrivent un évènement horaire sur une seule date, avec une fin strictement postérieure au début. Une forme partielle, une période inversée ou plusieurs dates avec des heures est refusée. Le même modèle couvre les réunions, entretiens, formations, temps RH, instances et vacances scolaires : leur distinction sert le classement et la couleur, pas une règle métier différente.
Décision : le rattachement organisationnel est facultatif. Un évènement peut pointer vers un établissement unique, mais cette clé reste vide pour une coordination transverse, un CSE ou toute rencontre réunissant plusieurs sites. Le lieu ne remplace pas ce rattachement : il reste un libellé libre assez précis pour nommer un bâtiment et une salle.
Décision : responsable et participants sont des salariés réels. Le responsable facultatif reçoit les alertes liées à l'évènement. Les participants forment une relation multiple ; leur fonction, leur établissement et leur équipe sont résolus à la date de début pour alimenter le sélecteur sans figer une affectation historique dans l'évènement.
Décision : l'alerte part du chemin d'écriture de l'absence. Comme le module actuel enregistre directement les absences en état validé, leur création ou une modification de salarié, de période ou d'état cherche les évènements recouverts. Le compte qui enregistre et chaque responsable actif concerné reçoivent une alerte par le domaine notifications. Une modification sans effet sur ce conflit n'envoie pas de doublon.
Décision : la grille manager superpose les évènements sans modifier le calendrier résolu. APAJH-103/143 lit GET /api/events/?from=&to= séparément de GET /api/planning/, puis indexe chaque évènement sur les cases de ses participants. Une panne de cette lecture laisse donc postes, repos et absences utilisables. Les évènements à la journée ont aussi un compteur distinct dans l'en-tête ; les évènements horaires gardent une pastille par occurrence. Le détail est en lecture seule, en panneau droit sur bureau et en panneau bas sur mobile.
Non construit ici : la représentation salariée. GET /api/events/mine/?from=&to= restreint déjà la lecture à l'utilisateur connecté, mais l'écran Mon planning dont il serait le consommateur n'existe pas encore. La bande synthétique d'évènements indépendante des lignes du planning manager reste elle aussi reportée.
Les compteurs
Un catalogue administrable définit les codes, libellés, unités, catégories, cycles, modes de gestion et ordres d'affichage. Les codes sont uniques, immuables et jamais réutilisés ; archiver un type interdit une nouvelle attribution sans effacer son historique.
Les soldes génériques portent une période réelle, annuelle ou mensuelle. Le compte épargne-temps et l'annualisation conservent leurs formes spécialisées et sont reliés au même catalogue. L'année historique reste exposée par l'API afin de conserver le contrat des clients existants.
Décision : le catalogue est extensible sans enum applicative. Seuls les codes CP, CT, ANC et AN restent réservés aux règles automatiques. Les autres types sont des données de référentiel, et leur ajout ne demande ni évolution Python ni nouvelle version du SPA.
Décision : la paie est l'autorité du droit CA/ANC (APAJH-66, APAJH-282). Un droit unique par salarié et année de prise contient un total acquis, ancienneté comprise, son état accruing ou final, sa provenance et sa date source. Les imports sont des instantanés de totaux ; seule une finalisation explicite CEGI rend le montant utilisable. Aucun passage d'année ne finalise ou ne crédite un droit. Un import initial amorce N−1 pour prise en N ; le flux mensuel poursuit le cumul destiné à N+1 et transporte les corrections finales. Aucun import annuel distinct.
CP devient le compteur commun Congés annuels. Le SIRH débite les absences, avec un restant acquis − pris − planifiés. Seul le flux paie remplace l'acquis ; les écritures manuelles et remplacements globaux ne peuvent l'écraser. Une correction peut rendre le solde négatif sans effacer une consommation. Les nouvelles consommations sont bloquées sans droit final ou solde suffisant ; les annulations et promotions à consommation nette constante restent possibles. Les consommations courantes et futures ANC sont transférées par contre-passations et mouvements CP, avec reprise unique des consommations manuelles résiduelles. Les absences ANC historiques restent lisibles et annulables ; les seeds ne les réactivent pas.
L'annualisation utilise le droit final CA sur six jours par semaine et conserve les objectifs historiques enregistrés par provenance, sans année de bascule. Les acquisitions et simulations locales CP/ANC sont supprimées. CT conserve ses règles additives journalisées à T1, T2 et T4, avec T4 destiné à N+1 ; son total annuel RH remplace sa simulation. Les autres compteurs gardent leurs règles.
Décision : un contrat hors CCN-66 n'ouvre que les CP (APAJH-199). L'agent polyvalent relève du Code du travail et non de la convention : son droit CA est reçu de la paie comme pour tous les salariés, mais ni CT, ni ANC, ni droit enfant malade, ni repos forfait jours, et aucune attribution manuelle d'un autre compteur. Son contrat refuse l'annualisation et le forfait jours, deux dispositifs de la convention, et sa durée hebdomadaire est plafonnée à 35 h — contractuellement, et au planning, où une semaine au-delà est une alerte bloquante « hors CCN-66 » au lieu des 44 h conventionnels. Ce qui le décide est une case du référentiel des types de contrat, hors convention collective (CCN-66), jamais le libellé : un type ajouté depuis le back-office suit la même règle sans déploiement. Le tri se fait contrat par contrat et non salarié par salarié : un mois couvert par un contrat CCN-66 à côté d'un contrat d'agent polyvalent acquiert ses CT au titre du premier, et une semaine n'est plafonnée à 35 h que si aucun contrat CCN-66 ne la couvre.
Décision : individualiser l'objectif depuis les contrats, indépendamment du planning. La base additionne les jours du lundi au vendredi de chaque contrat annualisé, entrée et sortie incluses dans l'année, à raison des heures hebdomadaires applicables divisées par cinq. Les forfaits jours sont exclus. Les contrats simultanés s'additionnent ; les avenants et interruptions conservent leurs dates. Les fériés du référentiel applicable couverts par ces contrats sont déduits, y compris le week-end : leur valeur suit le contrat de leur date, pas la trame. Un calendrier annuel absent rend le calcul automatique indisponible, signalé par missing_holiday_calendar. Une période sans férié dans un calendrier annuel présent reste calculable.
solidarity_status est une déclaration RH par salarié et année : unknown, not_completed, completed_here ou completed_elsewhere. Seul completed_here ajoute un cinquième de la durée hebdomadaire moyenne pondérée sur les jours couverts, une seule fois par année, sans nouvelle proratisation d'entrée. Le défaut unknown ajoute zéro heure avec un message d'information manquante. La fiche salarié et l'administration utilisent les permissions RH existantes ; les écritures API et administrateur sont tracées. Aucun réalisé ni mouvement compensateur n'est produit par le statut.
L'objectif soustrait le droit CA définitif importé, ancienneté comprise, et les CT utilisables pendant l'année. La durée hebdomadaire moyenne convertit ces droits sur six jours par semaine : à 35 h, 34 CA = 198,33 h et 18 CT = 105 h. Le cumul CA destiné à N+1 reste indicatif jusqu'à sa finalisation explicite.
reference_mode distingue calcul automatique, valeur RH, reprise à vérifier et objectif historique conservé. Les anciens barèmes sont consultables en archive, sans effet sur le calcul. La reprise invalide reference_details sans modifier les valeurs RH, les corrections ni consolidated_through. Les consultations et modifications des contrats, de l'ancienneté, des droits, des fériés ou de la solidarité actualisent les objectifs automatiques. Une seconde réconciliation n'ajoute aucune écriture. Le statut, les périodes contractuelles et les composantes du calcul sont partagés par les trois API dans les données annuelles et reference_details.
Décision : le tableau d'annualisation est une vue calculée, adossée à des contributions journalières journalisées (APAJH-61/81). Les mêmes lignes datées alimentent les mouvements, les synthèses mensuelles et le relevé annuel. Le prévu couvre les 365 ou 366 jours. L'indicateur principal est le Solde annuel prévisionnel : reconstitué à date + prévu restant + écart des modifications futures − réserve de congés + corrections RH − objectif annuel. L'écart comptabilisé au planning reste distinct, dans balance et les écritures existantes. Les trois API exposent explicitement projected_balance. Le relevé fournit ses composantes et les heures rémunérées séparément. Les trois interfaces exposent le même objectif reference_hours, son origine, reference_details et la réserve unplanned_leave_hours. Dans les deux récapitulatifs, une loupe à côté de Objectif annuel ouvre Calcul de l'objectif annuel dans une fenêtre modale, défilable sur téléphone et refermable au clavier. Les synthèses mensuelles présentent l'écart au planning et l'écart cumulé, dans un accordéon fermé par défaut sous les journées de la grille. La projection annuelle reste dans le récapitulatif global. La grille et les synthèses affichent les zéros sous forme de tirets ; une étoile distingue toujours le réalisé provisoire du réalisé déclaré.
Décision : la trame affectée reste la référence théorique. Le théorique suit le cycle applicable à chaque date, après retrait des congés déjà déduits du seuil. Une modulation alimente le réalisé provisoire, sans réécrire ce théorique. Les heures déclarées restent prioritaires sur cette estimation. Pour une date future modifiée, la grille affiche immédiatement l'estimation étoilée et la projection intègre son écart avec la trame ; les totaux acquis et le journal restent arrêtés à aujourd'hui. Au passage de la date, cet écart quitte la prévision et entre dans l'acquis une seule fois. Les agrégats sont recalculés après modification ou annulation. Un cadre violet commun aux valeurs TH et RÉ signale toute modulation, même à durée identique ou limitée au lieu de travail, sur les relevés manager et salarié.
Les congés et absences ne sont signalés que sur les jours comptés par la convention de l'établissement, hors fériés, et lorsque l'absence couvre des heures de la trame ou du planning modifié. Un repos hebdomadaire, une journée vide ou une absence en heures hors des créneaux conserve ainsi son affichage de repos ou d'heures ; une demi-journée, qui prend la moitié du temps travaillé, en couvre toujours dès que la journée en porte (APAJH-289). Ce filtrage visuel ne modifie pas les règles de valorisation des heures ci-dessous. La présentation reprend les douze calendriers compacts de la maquette manager, avec les colonnes jour, théorique et réalisé dans chaque mois. Le réalisé est teinté de turquoise. Lorsqu'il existe du temps travaillé, la case RÉ affiche uniquement les heures réalisées, avec une bordure ambre et le motif au survol en cas d'absence, ainsi que dans sa description accessible. Aucun détail supplémentaire n'occupe la grille. Sans temps travaillé, le code du congé ou de l'absence (ABR ou ABNR compris) reste visible sur fond ambre. Les absences futures sans réalisé gardent leur code. La bordure d'absence coexiste avec le cadre violet de modulation autour de TH/RÉ. Les samedis et dimanches ont un fond discret et une initiale renforcée ; les jours fériés conservent une pastille turquoise autour de leur numéro, y compris le week-end. Le défilement reste contenu dans la grille et les synthèses restent alignées sous les mois. Le relevé manager est précédé du bandeau d'identité du salarié : fonction, ETP, établissement, congés annuels restants, projection et ancienneté. Les données d'identité reprennent celles de la fiche salarié ; les congés suivent l'année sélectionnée et la projection provient du relevé affiché. Le sélecteur d'année compact se trouve dans la carte du tableau, au-dessus de la légende, et reste utilisable pendant le chargement ou en cas d'erreur.
Décision : valoriser les absences d'annualisation sur le travail prévu couvert. ABR et ABNR alimentent le reconstitué ; seules les ABR alimentent les heures rémunérées. Le service intersecte l'absence avec les créneaux nets des pauses non payées : une journée de 8 h vaut 8 h, et une demi-journée la moitié du temps travaillé, la première pour un matin, la dernière pour un après-midi — la même découpe que la grille (APAJH-289). Un poste de nuit reste rattaché à son jour de départ. Un dimanche ou férié effectivement planifié suit ces mêmes créneaux. La consommation des compteurs de congés et la valorisation du calendrier gardent leurs propres règles, indépendantes de cette mesure des heures.
Une absence entière produit zéro réalisé. Une absence partielle conserve le réalisé déclaré ; à défaut, seule la partie travaillée restante est provisoire. Les réalisés futurs et les absences futures sont exclus des totaux à date, tandis que la grille montre les absences futures. L'effet excluded, Déjà déduite du seuil annuel, s'applique à CP, CT et ANC : les heures couvertes sont retirées du planning sans être reconstituées. Leur déclaration future libère aussi la réserve correspondante : droits prévus moins pris et posés, au minimum zéro. La réserve utilise les conversions de droits ; l'absence garde ses créneaux couverts. Elle devient nulle à la fin de la période, même si des congés restent non posés. L'effet neutre reste sans incidence. L'effet recovery, Imputée sur l'annualisation (APAJH-301), est le seul qui fait bouger le solde : les heures couvertes ne sont ni réalisées ni reconstituées, mais le seuil prévu les garde dues — l'écart est débité. C'est celui du Repos plus (R+), un repos demandé en plus de la trame pour récupérer des heures : un cinquième effet plutôt qu'un compteur dédié, parce que ces heures sont déjà comptées sur l'annualisation, et une absence plutôt qu'une sorte de case de trame, parce qu'un R+ se demande et ne se planifie jamais. Un R+ à venir baisse d'autant la projection. Un zéro ne suffit pas à qualifier un repos : l'API expose l'état de planification.
La projection dépend du planning renseigné ; elle n'attribue aucun crédit HS. Les reports N−1 et calculs particuliers de nuit ne sont pas automatisés.
Les livraisons d'une source et les révisions du calcul restent séparées. Une version reçue à l'identique ne produit aucun effet, une version contradictoire est refusée. Chaque contribution journalière modifiée compense sa précédente version. Son empreinte inclut les effets d'absence appliqués ; un recalcul aux contributions identiques ne crée pas de révision. Les corrections RH restent journalisées séparément et les objectifs fixés par les RH sont conservés.
Les services sont appelés explicitement dans la transaction de l'écriture métier. Ils verrouillent les salariés dans l'ordre des identifiants avant les sources et soldes. Les mutations d'absence (dont refus, annulation et suppression), de contrat, de planning, de trame, de poste, de règles de décompte et de fériés reprennent les salariés et exercices concernés. L'admin attend les formulaires imbriqués complets ; les imports CEGI reprennent après les écritures du fichier.
Une date interne de dernière consolidation permet de rattraper les journées nouvellement atteintes. La tâche counters.consolidate_annualization s'exécute à minuit, heure de Paris. Après contrôle des permissions, les lectures du relevé, des compteurs et de la liste des soldes rattrapent aussi les échéances. Une tâche retardée ne rend donc pas la consultation obsolète. Aucun de ces chemins ne crée un seuil manquant. L'ouverture tardive d'un solde reprend les sources présentes. La commande reconcile_annualization migre les contributions historiques et préserve les montants manuels et actualise les objectifs automatiques.
Le total annuel théorique net est conservé dans annual_planned_hours, nullable, et l'écart des modifications futures dans future_adjustment_hours, lui aussi nullable. Ils sont actualisés lors de la consolidation. L'annuaire sert ainsi la projection sans calculer un relevé par ligne. Un total, un écart futur, une réserve ou un détail absent force la consolidation, même si celle du jour a déjà eu lieu. La migration ne remet jamais consolidated_through à zéro, afin de ne pas réimporter les montants manuels. La reprise compense les anciennes contributions et une seconde réconciliation identique reste sans effet.
La restitution réutilise /api/annualization-report/, /api/counters/ et /api/annualization-balances/. La matrice d'accès reste celle des données salarié : soi-même pour un compte ordinaire, annuaire pour les rôles déjà autorisés et le personnel du back-office. Le relevé manager/RH et le relevé personnel restent en lecture seule. Dans Mon espace, Annualisation présente la grille annuelle commune, le récapitulatif global puis les compteurs sur téléphone et ordinateur. La grille dispose de son propre défilement horizontal. Mes compteurs présente les montants par catégorie et période, avec le détail de chaque carte dans un panneau de consultation. Les états en attente, refusés et expirés restent identifiables. Même les RH consultent uniquement leur propre dossier depuis ces deux pages et effectuent leurs corrections dans la fiche salarié de l'espace manager.
L'entrée Annualisation manager réutilise les données, colonnes et adaptations mobiles de Utilisateurs, avec recherche, établissement, tri, réinitialisation et nombre de résultats. L'année pilote la projection et le relevé. Toute la ligne ouvre le relevé au clic, avec Entrée ou Espace ; le retour conserve l'année. Les deux annuaires, la fiche salarié et les deux portails affichent la projection. Les champs de correction RH demeurent séparés du résultat prévisionnel.
Décision : restituer les chiffres du backend sans refaire les calculs dans le navigateur. Les pages personnelles utilisent l'identifiant de dossier fourni par le profil connecté et les mêmes API que le manager. Une valeur absente reste — ; les indicateurs de maquette sans équivalent API sont omis, notamment les heures majorées et le nombre annuel de jours travaillés. Aucun champ ou bouton d'édition n'est proposé dans l'espace personnel (APAJH-82/83).
La page globale Compteurs manager, les exports, la saisie dans la grille et le calcul d'heures majorées ne sont pas livrés.
Décision : les sources produisent des deltas compensables. Une version de source et son contenu sont uniques. Une version déjà reçue à l'identique reste sans effet, même après son remplacement. Une version contradictoire ou une ancienne version jamais reçue est refusée ; une version supérieure compense les mouvements actifs de la précédente avant d'appliquer les siens. Les soldes sont verrouillés et tous les segments d'une absence sont écrits dans une transaction. Le journal append-only référence la source, sa version, la cible, la destination et l'auteur. Une correction RH n'est jamais reconstruite par ce mécanisme.
Décision : Planifiés devient Pris au début. Une absence future, en attente ou validée, réserve Planifiés. À 01 h 15 le jour de début, une tâche idempotente compense cette réservation et écrit Pris ; la commande de reprise suit le même chemin. Une absence traversant plusieurs périodes est segmentée, mais tous ses segments réussissent ou aucun n'est conservé. La priorité choisit une seule association et un solde insuffisant n'autorise aucun repli.
Décision : absences et temps réalisé se partagent les heures théoriques. Un événement de temps écrit réalisé − théorique pour un salarié annualisé, hors intervalles couverts par une absence. Les ABR et ABNR écrivent le retrait théorique et les heures couvertes reconstituées, soit un net nul traçable. Les CP, CT et ANC retirent le prévu couvert sans le reconstituer ; ils libèrent leur part de réserve. Leur déclaration ne modifie pas l'objectif contractuel.
Décision : un compteur consommable actif possède toujours un motif actif. Le catalogue distingue les compteurs consommables des compteurs techniques AN et HS. Un nouveau type naît inactif ; son association est préparée avant son activation. Les suppressions et désactivations qui créeraient un compteur orphelin sont refusées dans le back-office. Le seed de développement synchronise les 52 motifs et 15 raccords canoniques sans supprimer les ajouts locaux ni écraser les paramètres RH hors matrice.
Décision : les mandats sont la source des crédits de délégation. Une période de mandat RH couvrant au moins un jour ouvre le crédit mensuel complet. Les rôles CSE ne se cumulent pas entre eux, tandis que DS et conseiller du salarié portent des compteurs séparés. Les partages et reports CSE sont des événements immédiats, sans approbation N+1, journalisés comme un débit et un crédit compensables. Le plafond et le délai d'information sont contrôlés avant la transaction.
Ce qui n'existe pas : les alertes de seuil, le workflow de validation des absences et les formules automatiques de crédit RC, HS et COR — sauf le repos compensateur de nuit (R-F05/AL-E03, > 8h), dont le pont vers RC est implémenté mais désactivé en production par défaut (NIGHT_COMPENSATORY_REST_ENABLED). Les autres contrats techniques RC et HS restent présents mais inactifs.
Les jours fériés
Le référentiel est alimenté par le calendrier officiel de l'État (calendrier.api.gouv.fr), pour le seul territoire que l'instance dessert.
Décision : la synchronisation est quotidienne, pas annuelle. Un calendrier change une fois par an, ce qui suggère un travail annuel — mais un travail annuel est un point unique de défaillance : si l'unique exécution échoue (API indisponible, conteneur en cours de déploiement, incident réseau), personne ne le remarque et le référentiel reste faux pendant douze mois, ce qui est exactement la ressaisie manuelle que la fonction supprime. La synchronisation est une requête HTTP idempotente : la jouer chaque jour transforme toute défaillance passagère en retard de 24 h.
L'API publie une fenêtre glissante de plusieurs années, donc les années à venir apparaissent d'elles-mêmes : une équipe qui construit le planning 2027 pendant 2026 dispose déjà de 2027.
Les années utilisées par un solde d'annualisation échappent à la purge des fériés anciens, afin qu'une consultation historique puisse refaire le calcul.
L'heure et la tâche exacte sont en annexe D.
Décision : travailler les jours fériés se décide par salarié, à la planification (APAJH-288). Une trame reconduite tacitement planifiait ses postes sur les fériés, qu'il fallait ensuite effacer à la main — au point que le client multipliait les trames pour l'éviter. La planification porte donc une case « Travaille les jours fériés », décochée par défaut : la trame ne pose alors aucun poste sur un férié, et garde ses repos. Ni la trame ni l'établissement ne peuvent en décider : dans un établissement ouvert les jours ouvrables, les soignants travaillent les fériés et les administratifs, chefs de service et direction non. Une modulation posée sur un férié l'emporte sur la case — c'est une décision explicite du manager. Les planifications existantes ont été migrées case cochée, pour qu'aucun planning, total ni compteur déjà lu ne bouge.
Les flux avec l'extérieur
| Flux | Sens | Déclencheur | Sécurité |
|---|---|---|---|
| Calendrier des jours fériés | Entrant | Tâche planifiée quotidienne | HTTPS sortant, API publique sans authentification |
| Personnes et contrats CEGI | Entrant | Dépôt SFTP, collecte quotidienne | SSH/SFTP, compte de dépôt chrooté, clé hôte épinglée |
| Courriel de réinitialisation de mot de passe | Sortant | Demande d'un utilisateur | SMTP, § 05 et § 06 |
| Accès base pour l'administration | Entrant | À la demande d'un DBA | TCP filtré par IP, § 04 |
Aucun autre flux ne sort de la plateforme. Il n'y a pas d'interface avec un annuaire d'entreprise.
Conséquence sur le matricule paie. Il est généré par le logiciel de paye, hors périmètre, puis reçu par l'import CEGI ou saisi à la main. La plateforme ne peut donc pas l'attendre pour ouvrir un dossier : Employee.staff_number est facultatif, et un salarié est enregistrable — et planifiable — avant que la paie ait répondu (APAJH-205).
Deux précisions que le vocabulaire de l'interface rend nécessaires :
- l'application signale un dossier sans matricule par un badge « Non synchronisée paie ». Le mot vient de la maquette et désigne un état de la donnée — aucun matricule paie n'a encore été reçu ou saisi. Le badge disparaît dès que le matricule est renseigné ;
- un intérimaire n'a jamais de matricule, les intérims n'entrant pas dans la paie. Chez lui le champ vide est définitif, et l'application n'affiche aucun marqueur : le badge Intérim est l'énoncé exact. Sans cette exception, tout intérimaire porterait une alerte permanente, ce qui apprend à ignorer l'alerte.
L'import des personnes et contrats
Décision : le fichier est un instantané non destructif. Chaque ligne crée ou met à jour un salarié et son contrat par leurs identifiants CEGI. L'absence d'une ligne dans un fichier ultérieur ne supprime rien : un export incomplet ne doit pas effacer un historique d'emploi.
Le fichier entier est décodé et validé avant toute écriture. Une ligne invalide, un doublon interne ou un code établissement inconnu rejette donc le lot entier, sans écriture partielle. Un autre fichier du même passage continue à être traité. L'empreinte SHA-256 du contenu assure l'idempotence, même si le même fichier est redéposé sous un autre nom.
Les champs importés restent sous l'autorité de CEGI : une valeur facultative vide les efface lors d'un nouvel instantané. L'adresse personnelle reçue dans pers_email alimente seulement l'email personnel de la fiche ; elle ne remplace jamais l'email professionnel du compte de connexion.
Le flux salariés conserve la date de naissance (pers_dnai), mais ne pilote plus le téléphone professionnel, le nombre d'enfants à charge ni le motif d'embauche. Leurs valeurs GTA sont préservées, y compris face aux anciens CSV. Les huit colonnes retirées (pers_tel1, doc_lib, per_enf, code_pay, code_emb, lib_emb, cont_sdtc, cont_datfprev) sont ignorées. Le dossier de paie est résolu par dos_code uniquement ; la fin de contrat provient de cont_datf, sans repli prévisionnel.
La qualification importée du contrat prend le libellé de fonction pos_lib, à défaut la qualification conventionnelle lib_qc, à défaut une chaîne vide. Les codes sources restent distincts. Le volet Contrat affiche ce champ, exposé en lecture seule par l'API des contrats, et non le champ fonction. L'import séparé des enfants à charge reste à construire ; ce lot ne crée ni récepteur ni planification pour ce futur flux.
Identité des types de contrat — GitHub #128 et #129
L'ID GTA est la clé des sélections et des rattachements. Le code CEGI nullable et unique est la clé de rapprochement de la paie ; le libellé n'est jamais unique. Le formulaire transmet l'ID du type actif sélectionné et lit ses trois indicateurs métier, même pour des homonymes. L'API /api/contract-types/ expose cegi_code en lecture seule et ne comporte plus nature.
L'import cherche d'abord le code. À défaut, un unique type sans code au libellé strictement identique reçoit ce code ; zéro ou plusieurs candidats provoquent la création d'une nouvelle ligne aux valeurs par défaut. Les classifications existantes sont conservées, sans inférence depuis les libellés.
La migration contract_types.0008_remove_nature supprime la contrainte (label, nature), puis la colonne nature. Elle ne fusionne ni ne renomme aucun type et conserve IDs, codes, classifications et rattachements. Les migrations historiques sont conservées pour les installations vierges. La suppression dépend de counters.0017_individual_annual_targets, dernière migration de données qui lit nature, afin que ce champ existe encore lors de la reprise des anciens compteurs. Sauvegarder avant migration : les anciennes valeurs de nature ne peuvent être restaurées qu'à partir d'une sauvegarde. Une inversion de migration ne les reconstitue pas et peut échouer sur les nouveaux homonymes ; un retour à l'ancienne version exige une restauration cohérente de la base et du code.
Consultation des EVP — APAJH-91
La page EVP (/evp) remplace la préparation d'export : la paie consulte les éléments dans le portail et les saisit manuellement dans son logiciel. Admin, RH n1, RH n2 et Service Paie y accèdent avec filtres par mois civil, établissement de paie et salarié/matricule. L'entrée est disponible dans les deux espaces pour rester accessible sur mobile. Le tableau de bord reste indépendant.
Une ligne représente un contrat qui chevauche le mois, y compris un contrat terminé. Le détail affiche identité, contrat, ETP issu des heures hebdomadaires, motif d'embauche, absences validées, dates de télétravail du planning, périodes d'astreinte et évènements auxquels le salarié participe. Les dates et quantités sont limitées à l'intersection mois/contrat. Les quantités d'absence réutilisent le décompte GTA (jours ouvrables/ouvrés, jours fériés et demi-journées), pas une règle de rémunération.
Télétravail et évènements sont prévisionnels. Une date de télétravail peut ne couvrir qu'une partie de journée. Les absences validées couvrant la journée suppriment le télétravail de cette date ; une demande en attente ne le supprime pas. Les astreintes sont des durées de couverture enregistrées, sans conversion en semaines indemnisables. Les évènements à la journée n'ont pas de durée horaire déduite.
Non construit : export CSV/Excel, dépôt SFTP, import CEGI, montant à payer, dimanches/jours fériés à payer, conversion des astreintes en semaines, saisie des remboursements/avantages en nature/monétisations et régularisations rétroactives. La fiche Excel de juillet 2026 sert de référence de contenu, sans import de ses données personnelles. Les contrats simultanés peuvent répéter les mêmes données du salarié : aucun total intercontrats n'est proposé. La validation métier reste requise avant la sortie du brouillon de la PR ; voir la recette EVP.