Files
SIRH/doc/functional-rules.md
tristan abdaf809f8
Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
Gestion du changement de type de contrat + correction du calcule des RTT sur un contrat qui commence en milieu de semaine (#19)
| Numéro du ticket | Titre du ticket |
|------------------|-----------------|
|                  |                 |

## Description de la PR

## Modification du .env

## Check list

- [x] Pas de régression
- [x] TU/TI/TF rédigée
- [x] TU/TI/TF OK
- [x] CHANGELOG modifié

Reviewed-on: #19
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-05-22 06:42:33 +00:00

28 KiB
Raw Blame History

Règles Fonctionnelles SIRH

Ce document centralise les règles métier actuellement implémentées dans l'application.

Documents complementaires:

  • doc/leave-rollover.md (rollover conges et checklist de lancement)
  • doc/rtt-rollover.md (rollover RTT et checklist de lancement)

1) Utilisateurs et accès

  • ROLE_ADMIN
    • accès complet aux écrans d'administration
    • vue semaine des heures
    • validation RH des lignes d'heures
  • ROLE_SELF
    • accès limité à son périmètre personnel
  • Accès "Sites" (via user_site_roles avec rôle SITE_ACCESS)
    • accès au périmètre des sites autorisés
    • validation site des lignes d'heures

2) Contrats

  • Le profil de temps de travail est porté par Contract:
    • trackingMode: TIME ou PRESENCE
    • weeklyHours (ex: 35, 39, 4, etc.)
  • La nature RH est portée par période employé:
    • CDI, CDD, INTERIM
  • Historique des contrats employé:
    • table employee_contract_periods
    • un employé peut avoir plusieurs périodes

Règles de période

  • CDI:
    • à la création d'une période: endDate doit être vide
    • en clôture d'un contrat en cours: endDate peut être renseignée
  • CDD / INTERIM:
    • endDate obligatoire
  • endDate ne peut pas être antérieure à startDate

3) Heures (vue jour)

  • Visibilité des employés:
    • vue jour: un employé sans contrat à la date sélectionnée est masqué
    • vue semaine: un employé sans contrat sur aucun jour de la semaine est masqué
    • même règle pour les heures classiques et les heures conducteurs
  • Saisie par salarié et par date:
    • matin / après-midi / soir
    • pour PRESENCE: demi-journées matin/après-midi
  • Sélecteur de temps:
    • créneaux de 15 minutes uniquement (00:00, 00:15, ..., 23:45)
    • saisie libre possible mais valeur vidée au blur si hors options
  • Calculs affichés:
    • Jour, Nuit, Total
  • Heures de nuit:
    • fenêtres 00:00-06:00 et 21:00-24:00
  • Date de modification (updatedAt):
    • mise à jour uniquement quand un employé (ROLE_SELF) modifie ses propres heures
    • non mise à jour lors de modifications admin ou chef de site
    • affichée sous le nom de l'employé (visible admin uniquement)
  • Libellé nature de contrat (CDI/CDD/Intérim) affiché sous le nom:
    • résolu à la date filtrée (période de contrat couvrant ce jour), pas à aujourd'hui
    • masqué si aucun contrat à cette date (cas rarissime en vue jour puisque l'employé est alors déjà filtré)

4) Absences

  • Les absences sont stockées par jour (découpage lors de l'écriture)
  • Une absence peut être:
    • journée complète
    • demi-journée AM ou PM
  • Colonne absence (vue jour):
    • affiche le libellé
    • fond coloré selon le type d'absence
  • Calendrier congés: fond coloré selon la couleur du type d'absence (AbsenceType.color)
    • demi-journée: dégradé diagonal
    • journée complète: fond plein
  • Visibilité des employés dans le Calendrier:
    • un employé est affiché si au moins une de ses périodes de contrat intersecte le mois affiché
    • un employé dont toutes les périodes se terminent avant le 1er du mois (ou commencent après la fin du mois) est masqué
    • même logique que l'écran Heures : « pas de contrat sur la période → masqué »

Effet absence sur les heures

  • Absence AM:
    • efface les heures du matin
  • Absence PM:
    • efface les heures d'après-midi et du soir
  • Absence journée:
    • efface toutes les plages horaires

Absences "comptées comme travaillées"

  • Si countAsWorkedHours = true:
    • TIME: crédit de minutes selon contrat actif du jour
    • PRESENCE (forfait): aucun crédit de présence (seules les checkboxes cochées comptent)

5) Validations des lignes d'heures

  • Validation RH (isValid)
    • action admin
  • Validation site (isSiteValid)
    • action chef de site

Verrouillage

  • Ligne validée RH:
    • verrouillée pour modifications heures/absences
  • Ligne validée site:
    • verrouillée pour non-admin
    • admin peut corriger
  • Toute vraie modification d'une ligne:
    • remet isSiteValid = false
    • remet isValid = false
  • Si aucun changement réel à l'enregistrement:
    • les validations existantes ne sont pas altérées

6) Heures supplémentaires (vue semaine)

  • Base de calcul:
    • dépend du contrat actif par jour
  • Tranche 25%:
    • contrats <= 35h: de 35h à 43h
    • contrats >= 39h: de 39h à 43h
  • Tranche 50%:
    • au-delà de 43h
  • Date de début RTT (RTT_START_DATE dans .env):
    • les semaines dont la fin est antérieure à cette date sont ignorées dans le calcul de récupération
    • permet d'éviter les déficits fictifs avant la mise en service du logiciel
  • Semaine en déficit (heures travaillées < heures contrat):
    • le déficit est déduit du cumul RTT : d'abord des heures à 50%, puis des heures à 25%
    • si aucun solde 50% ni 25%, les heures à 25% deviennent négatives
  • Contrats CUSTOM (heures hebdo ≠ 35h et ≠ 39h, hors INTERIM/FORFAIT):
    • référence heures sup = heures contractuelles réelles (ex: 4h → référence 4h)
    • pas de bonus 25% ni 50% : 1 heure sup = 1 heure de récupération
    • le déficit (travail < contrat) ne génère pas de récup mais n'impacte pas le solde
  • Nature INTERIM:
    • pas de bonus 25%
    • pas de bonus 50%
    • pas de total récup
    • agence d'intérim optionnelle (table interim_agencies): affichée sur la fiche employé et le détail contrat sous la forme "Intérim (NomAgence)"

6bis) Heures Conducteurs

  • Écran dédié /driver-hours pour les employés dont le contrat est marqué isDriver = true
  • Les conducteurs sont exclus de l'écran /hours classique
  • Colonnes spécifiques (vue jour):
    • Heure de jour (durée HH:MM via TimeSelect)
    • Heure de nuit (durée HH:MM via TimeSelect)
    • Heure atelier (durée HH:MM via TimeSelect)
    • Total (somme jour + nuit + atelier, calculé)
    • Petit déjeuner (checkbox)
    • Déjeuner (checkbox)
    • Dîner (checkbox)
    • Nuitée (checkbox)
  • Stockage backend:
    • dayHoursMinutes, nightHoursMinutes et workshopHoursMinutes (entiers, minutes) sur WorkHour
    • hasBreakfast, hasLunch, hasDinner, hasOvernight (booleans) sur WorkHour
    • les champs time classiques (morning/afternoon/evening) sont mis à null pour les chauffeurs
  • Absences countAsWorkedHours=true: les minutes créditées sont ajoutées aux heures de jour (vue jour et vue semaine), même logique que les employés classiques
  • Validation: même logique que les heures classiques (isValid, isSiteValid, bulk)
  • Vue semaine:
    • jour/nuit/atelier par jour + indicateurs repas/dîner/nuitée
    • panier de nuit (PN): affiché par jour si (nightMinutes > dayMinutes) OU (nightMinutes >= 240, soit au moins 4h de travail entre 21h et 6h), et total hebdo dans la colonne Jour/Nuit sem.
    • totaux hebdo: jour, nuit, atelier, total, compteurs petit déj/déjeuner/dîner/nuitée
    • les conducteurs utilisent dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes pour le calcul RTT (au lieu des créneaux morning/afternoon/evening)
  • Le flag isDriver est sur EmployeeContractPeriod (un employé peut changer de statut chauffeur selon la période)
  • Exposé en API via un getter virtuel sur Employee (employee:read) qui résout depuis la période active

7) Fériés

  • Les jours fériés sont identifiés et affichés
  • Source: API calendrier.api.gouv.fr/jours-feries/ via PublicHolidayService (cache 30j)
  • Exclusions configurables: variable d'env EXCLUDED_PUBLIC_HOLIDAYS (liste de libellés séparés par virgules). Par défaut "Lundi de Pentecôte" — journée de solidarité généralement travaillée. Le filtre s'applique à tous les consommateurs (frontend + calculs backend) en amont du retour du service.
  • Onglet congés: jours fériés affichés sur le calendrier avec fond rgb(179, 229, 252) et nom au survol
  • Écran Heures et Heures Conducteurs (vue jour): le nom du férié est affiché dans la colonne Absence sous forme de pill (fond #b3e5fc, icône mdi:calendar-star), distinct du pill absence
  • Écran Heures et Heures Conducteurs (vue semaine): la cellule du jour férié prend le fond #b3e5fc quand l'employé n'a pas d'absence ce jour-là, avec le nom du férié au survol (title). Si une absence est posée, la couleur de l'absence prime ; le title cumule les deux libellés (Absence — Férié : Nom).
  • Règle courante:
    • absences autorisées sur jour férié (création/édition depuis l'écran Heures et le Calendrier). Quand une absence est posée, le crédit virtuel férié est désactivé — c'est le countAsWorkedHours du type d'absence qui pilote
    • saisie d'heures ou de jours de présence autorisée — les heures saisies comptent normalement dans le total hebdo et le calcul RTT
    • la référence hebdomadaire n'est pas réduite par un férié: un salarié qui ne saisit rien sur un férié est en déficit de la journée correspondante

8) Impression absences (PDF)

Filtres disponibles:

  • période from / to
  • sites
  • nature de contrat (CDI, CDD, INTERIM)
  • temps de travail (contrats de type Forfait, 35h, 39h, etc.)

Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer.

Seuls les employés dont au moins une période de contrat intersecte la période imprimée [from, to] apparaissent dans le PDF (même règle que la vue Calendrier). Un salarié parti avant la période (ex. contrat terminé en avril, impression de mai) est exclu. Borne calculée sur la date seule, côté backend (AbsencePrintProvider::hasContractInRange).

9) Employés

  • Création employé:
    • prénom, nom, site
    • type de contrat (nature RH)
    • temps de travail
    • dates début/fin (selon règles nature)
  • Modification employé:
    • uniquement prénom, nom, site
    • pas de modification de contrat depuis ce drawer
  • Liste employés — filtre par statut de contrat:
    • 3 options: "Avec contrat" (défaut), "Sans contrat", "Tous"
    • "Avec contrat": employés ayant une période de contrat active à la date du jour
    • "Sans contrat": employés sans période de contrat active
    • "Tous": aucun filtrage sur le contrat
  • Détail employé:
    • onglet Suivi contrat avec affichage de l'historique des périodes de contrat
    • chaque ligne expose: nature (CDI/CDD/INTERIM), contrat/temps de travail, date de début, date de fin (ou "En cours")
    • action Modifier (clôture/solde de tout compte):
      • bouton actif s'il existe un contrat en cours non clôturé, ou si le dernier contrat est terminé (sans contrat actif après)
      • ouvre un drawer en lecture seule (type/temps de travail/date de début)
      • champs saisissables:
        • contractEndDate (prérempli à aujourd'hui si contrat en cours, à la date de fin existante si contrat terminé)
        • contractPaidLeaveSettled (checkbox "Soldé dans le solde de tout compte")
      • backend: en mode clôture, le flag contractPaidLeaveSettled est persisté sur la période clôturée
      • cas du contrat déjà terminé: permet de modifier paidLeaveSettled et le commentaire sur le dernier contrat terminé (ex: solde de tout compte CDD)
    • action Ajouter:
      • conserve le flux d'ajout d'un nouveau contrat via drawer dédié
      • disponible uniquement s'il n'y a pas de contrat en cours, ou si le contrat en cours a déjà une date de fin
    • onglet Congé:
      • endpoint de synthèse: GET /api/employees/{id}/leave-summary?year=YYYY
      • phase 1 métier (CDI/CDD non forfait + FORFAIT):
        • exercice CP:
          • CDI/CDD non forfait: du 1er juin (YYYY-1) au 31 mai (YYYY) (paramètre year = année de fin d'exercice)
          • FORFAIT: du 1er janvier (YYYY) au 31 décembre (YYYY) (paramètre year = année civile)
        • contrats 39h / 35h / 25h (et plus largement CDI/CDD non forfait hors 4h):
          • acquis annuel CP: 25
          • acquis annuel samedi: 5
          • en cours d'acquisition jours: 25/12 = 2,08 jours/mois
          • en cours d'acquisition samedis: 5/12 = 0,42 samedi/mois (non detaille en UI)
          • en cas de début/fin en cours de mois, l'acquisition est proratisée au nombre de jours calendaires couverts dans le mois
          • en cas de suspension en cours de mois, l'acquisition est proratisée en jours ouvrés (lun-ven hors fériés) travaillés / 22 (standard mensuel)
          • arrêt maladie long (absences continues de type M > 1 mois):
            • premier mois de maladie (date début + 1 mois calendaire): acquisition normale (2,50/mois)
            • après le premier mois: acquisition réduite à 2,00/mois (facteur 0,80 appliqué aux deux taux jours et samedis)
            • en cas de mois partiellement couvert par la période réduite, le prorata est calculé en jours calendaires (jours normaux × taux normal + jours réduits × taux réduit)
            • la détection est automatique à partir des absences MALADIE consécutives en base (tolérance de gap ≤ 3 jours)
          • samedis acquis affiches: uniquement opening_saturdays (report N-1)
        • contrat 4h:
          • acquis annuel CP: 10
          • acquis annuel samedi: 0
          • en cours d'acquisition: 0.83 jour/mois
          • en cas de début/fin en cours de mois, l'acquisition est proratisée au nombre de jours calendaires couverts dans le mois
          • en cas de suspension en cours de mois, l'acquisition est proratisée en jours ouvrés (lun-ven hors fériés) travaillés / 22
        • contrat FORFAIT:
          • base annuelle: jours ouvrés de l'exercice (lundi-vendredi, hors jours fériés métropole) - 218
          • bonus weekend/férié: chaque jour travaillé un weekend ou jour férié donne 1 jour de congé supplémentaire (journée ≥ 5h = 1.0 jour, demi-journée > 0h et < 5h = 0.5 jour), sans plafond
          • prorata: en cas de démarrage/fin de contrat en cours d'année civile, le calcul ne couvre que l'intervalle actif du contrat dans l'année
          • reste à prendre: acquis - absences (toutes absences, demi-journées incluses)
          • pas de samedi (0)
          • pas de jours en cours d'acquisition (0)
        • fractionné: saisie manuelle par la RH via PATCH /employees/{id}/fractioned-days, stocké dans employee_leave_balances.fractioned_days. Les jours fractionnés sont ajoutés aux acquis et au reste à prendre.
        • pour CDI/CDD non forfait:
          • pris CP: basé sur absences de type code C (CONGÉ), en tenant compte des demi-journées
          • samedi pris: absences C posées le samedi (demi-journée incluse)
          • restants = acquis - pris (borné à 0)
        • pour FORFAIT:
          • pris: basé sur toutes les absences (demi-journées incluses)
          • restants = acquis - pris (borné à 0)
          • paiement congés N-1: saisie RH via PATCH /employees/{id}/paid-leave-days (body: paidLeaveDays, year). Stocké dans employee_leave_balances.paid_leave_days. Les jours payés réduisent le stock N-1 avant l'attribution des jours pris : disponible_N-1 = max(0, acquis_N-1 - payés), puis pris_N-1 = min(disponible_N-1, total_pris), surplus pris basculé sur N. Reste à prendre N-1 = max(0, disponible_N-1 - pris_N-1). Uniquement pour les contrats forfait.
        • report annuel:
          • le reliquat (restants) de l'exercice précédent est reporté dans les acquis de l'exercice courant
          • pour CDI/CDD non forfait: report séparé jours + samedis
          • pour FORFAIT: report uniquement sur les jours
          • si un solde d'ouverture existe en base (employee_leave_balances) pour l'exercice courant, ce solde devient la source prioritaire du report
          • si une clôture de contrat est marquée contractPaidLeaveSettled=true sur l'exercice précédent, le report vers l'exercice suivant est remis à 0
          • si une clôture contractPaidLeaveSettled=true existe dans l'exercice courant, le calcul est réinitialisé à partir du lendemain de cette clôture (pas de continuité intra-exercice)
        • lecture des compteurs:
          • acquis = droits reportés de l'exercice N-1 (après application des règles de soldé)
          • en cours d'acquisition = total droits générés sur l'exercice N (jours + samedis en cours), sans detail séparé en UI
          • en cours d'acquisition est arrêté au dernier jour du mois précédent
        • règle de consommation:
          • les absences s'imputent d'abord sur acquis, puis sur en cours d'acquisition
          • la prise sur en cours d'acquisition est autorisée (usage anticipé)
          • en cours d'acquisition peut devenir négatif si la prise dépasse le généré (ex: 2.08 - 3 = -0.92), puis se reconstitue avec les acquisitions suivantes
        • date d'arret de calcul:
          • reste à prendre est calculé en prévisionnel jusqu'à la fin de l'exercice
          • les absences futures déjà posées sur l'exercice sont déduites du reste à prendre
          • en cours d'acquisition reste calculé jusqu'au dernier jour du mois précédent
          • exemple: au 11/03/2026, l'exercice 2026 déduit les absences posées jusqu'au 31/05/2026, mais l'acquisition reste arrêtée au 28/02/2026
      • hors périmètre phase 1: INTERIM (retour non supporté)
    • onglet RTT:
      • endpoint de synthèse: GET /api/employees/{id}/rtt-summary?year=YYYY
      • exercice RTT: du 1er juin (YYYY-1) au 31 mai (YYYY) (paramètre year = année de fin d'exercice)
      • affichage:
        • détail hebdomadaire (semaine ISO) regroupé par mois
        • total mensuel des minutes de récupération
        • compteur global exercice = report N-1 + acquis N
      • attribution mensuelle des semaines:
        • une semaine ISO qui chevauche deux mois est affichée dans les deux mois, avec les valeurs réparties proportionnellement aux minutes travaillées de chaque portion
        • le calcul des heures supplémentaires reste hebdomadaire (seuils 35h/39h/43h appliqués sur la semaine entière), seul l'affichage est scindé
        • exemple: S14 lundi-mardi en mars, mercredi-dimanche en avril → la S14 apparaît en mars (avec la part des heures de lun-mar) et en avril (avec la part mer-dim)
      • logique de calcul:
        • base identique aux calculs d'heures supplémentaires de la vue semaine Heures
        • minutes de récupération hebdomadaires = HS totales + bonus 25% + bonus 50%
        • contrats INTERIM et suivi PRESENCE: récupération à 0
        • date limite de calcul: uniquement les semaines terminées (jusqu'au dernier dimanche), ou la semaine en cours si tous les jours existants sont validés RH (isValid = true). En cas de fin de contrat en milieu de semaine, seuls les jours jusqu'à la date de fin sont vérifiés.
      • compteur global:
        • affiché en jours (1 jour = 7h = 420 minutes)
      • report:
        • le report N-1 correspond à la somme des minutes de récupération calculées sur l'exercice précédent
        • si une ligne existe dans employee_rtt_balances pour (employee, year), le champ opening_minutes est utilisé en priorité
        • sinon, le calcul dynamique sur l'exercice N-1 est effectué
      • rollover automatique:
        • commande: php bin/console app:rtt:rollover
        • s'exécute le 1er juin (même cron que le rollover congés)
        • calcule le total récup N-1 et le persiste en opening_minutes du nouvel exercice
        • idempotent (ne recrée pas si la ligne existe)
      • paiement RTT:
        • saisie RH via PATCH /employees/{id}/rtt-payments (body: month, minutes, rate)
        • stocké dans employee_rtt_payments (employee, year, month, minutes, rate)
        • rate: taux de majoration, valeurs 25 ou 50
        • les heures payées sont soustraites du disponible RTT (availableMinutes -= totalPaidMinutes)
        • affichage: 2 lignes par mois dans le tableau (25% et 50%)
      • colonnes Total 25% et Total 50%: somme base + bonus de chaque tranche
      • colonne Cumul (dernière colonne): solde RTT à la fin de chaque semaine = report N-1 + somme totalMinutes des semaines jusqu'à celle-ci paiements RTT des mois antérieurs au mois de la semaine. Le paiement d'un mois M n'est déduit qu'à partir des semaines du mois M+1 (cohérent avec la logique de la ligne "Report mois précédent"). Permet la comparaison ligne à ligne avec un suivi RH externe (Excel)
      • ligne Report N-1 (carry rollover): affichée en juin uniquement si carry > 0
      • ligne Report mois précédent: solde cumulé (carry N-1 + semaines antérieures paiements antérieurs), affichée à partir de juillet (masquée si nul)
      • Reste = Report cumulé + Total du mois Payé du mois (balance courante en fin de mois)
      • affichage:
        • le compteur global RTT est affiché en heures (format Xh00)

10) Export récap. congés & RTT (PDF)

  • Accessible depuis la page Employés via le bouton "Export récap. congés" (réservé ROLE_ADMIN)
  • Clic direct (pas de drawer), génère un PDF A4 portrait à la date du jour
  • Endpoint: GET /api/leave-recap/print
  • Seuls les employés avec contrat actif sont inclus
  • Données groupées par site

Colonnes du tableau

Colonne Logique
Nom lastName + firstName
Contrat Contract.name
CP N-1 restant CDI/CDD: acquis N-1 pris sur N-1. Forfait: report N-1 restant
Samedi restant CDI/CDD: samedis acquis N-1 pris. Forfait: -
CP N Forfait: restant sur quota année civile (acquis pris depuis N, sans toucher au stock N-1). Non-forfait: en cours d'acquisition
RTT Minutes disponibles (report N-1 + acquis N - payés). Format X h Y m. Forfait et INTERIM: -

10bis) Écran Récap. congés (tableau)

  • Complément de l'export PDF : même logique de calcul, mais accessible aux employés et chefs de site
  • Endpoint: GET /api/leave-recap
  • Accès conditionné au flag User.hasLeaveRecapAccess (défaut false, activé au create/edit user)
  • Le flag s'applique à tous les profils, y compris admin (pas de bypass)
  • Scoping :
    • ROLE_ADMIN : tous les employés
    • ROLE_USER (chef de site) : employés des sites autorisés (UserSiteRole)
    • ROLE_SELF : uniquement son employé lié
  • Cutoff temporel : le récap est figé à la fin de la semaine S-2 (dimanche 23:59:59)
    • Formule : cutoffDate = dimanche(lundi_semaine_courante 14 jours)
    • Exemple : mardi 14/04/2026 (S16) → dimanche 05/04/2026 (fin S14)
    • isValid n'entre PAS en compte : cutoff purement temporel
    • Les heures et absences postérieures au cutoff sont ignorées dans les calculs
  • Colonnes identiques au PDF (voir §10)
  • Détails techniques : voir doc/leave-recap-screen.md

11) Récapitulatif Salaire (PDF mensuel)

  • Accessible depuis la page Employés via le bouton "Récap. Salaire" (réservé ROLE_ADMIN)
  • Sélecteur de mois (défaut = mois courant), génère un PDF A3 paysage
  • Endpoint: GET /api/salary-recap/print?month=YYYY-MM
  • Données groupées par site, un en-tête par site

Colonnes du tableau

Colonne Source Logique
Nom Employee firstName + lastName
Base Contract.name Via EmployeeContractResolver pour le mois
Jour de présence Cadre WorkHour Uniquement FORFAIT (PRESENCE). Somme isPresentMorning (0.5) + isPresentAfternoon (0.5)
Heures de nuit WorkHour Non-chauffeurs: calcul intervalles nuit (00:00-06:00, 21:00-24:00). Chauffeurs: somme nightHoursMinutes
Panier de nuit WorkHour Nombre de jours où (nightMinutes > dayMinutes) OU (nightMinutes >= 240, soit 4h entre 21h-6h)
Heures payés EmployeeRttPayment Somme base25Minutes + base50Minutes du mois, convertie en heures
Congés - Nombre Absence code 'C' Jours (demi-journées = 0.5)
Congés - Date Absence code 'C' Dates formatées dd/mm
Maladie - Nombre Absence code 'M' ou 'AT' Jours (demi-journées = 0.5)
Maladie - Date Absence code 'M' ou 'AT' Dates formatées dd/mm
CHAUFFEUR - PDJ WorkHour.hasBreakfast Comptage mois (chauffeurs uniquement)
CHAUFFEUR - REPAS WorkHour.hasLunch + hasDinner Somme sur le mois : +1 par déjeuner coché et +1 par dîner coché (un jour avec les deux compte 2 repas, chauffeurs uniquement)
CHAUFFEUR - NUITEE WorkHour.hasOvernight Comptage mois (chauffeurs uniquement)
CHAUFFEUR - samedi WorkHour (samedi) Samedis travaillés (chauffeurs uniquement)
Observations Colonne vide pour saisie manuelle

12) Frais

  • Onglet "Frais" sur la fiche employé (icône mdi:account-cash-outline)
  • Entité MileageAllowance (table mileage_allowances)
  • Champs:
    • month (mois, obligatoire)
    • kilometers (nombre de km, optionnel)
    • amount (montant en €, optionnel)
    • comment (commentaire, optionnel)
    • receiptPath / receiptName (justificatif Km, PDF)
    • amountReceiptPath / amountReceiptName (justificatif Montant, PDF)
  • Règle de validation:
    • le mois est obligatoire
    • au moins un des deux champs kilometers ou amount doit être > 0
    • les deux peuvent être remplis simultanément
  • Tableau: colonnes Mois, Nombre de Km, Montant €, Commentaire, Justif. Km, Justif. Montant
  • Deux justificatifs distincts (upload PDF uniquement):
    • Justificatif Km : upload via /mileage_allowances/{id}/receipt, téléchargement via GET même URL
    • Justificatif Montant : upload via /mileage_allowances/{id}/amount-receipt, téléchargement via GET même URL
  • La suppression d'un frais supprime les deux fichiers justificatifs du disque

13) Observations

  • Onglet "Observation" sur la fiche employé (icône mdi:note-text-outline)
  • Entité Observation (table observations)
  • Champs:
    • month (mois, obligatoire)
    • content (texte d'observation, obligatoire)
  • Contrainte: une seule observation par mois par employé (unique sur employee_id + month)
  • Tableau: colonnes Mois | Observation
  • Drawer avec champs mois (type="month") et textarea "Observation"
  • CRUD standard: création, modification, suppression avec confirmation

14) Verrouillage utilisateur

  • Champ isLocked (boolean, default false) sur l'entité User
  • Un admin peut verrouiller/déverrouiller un utilisateur depuis la page Utilisateurs (checkbox dans le drawer)
  • Un utilisateur verrouillé ne peut plus se connecter (vérification via UserChecker sur les firewalls login et api)
  • Colonne "Statut" dans le tableau utilisateurs avec label "Actif" (vert) ou "Verrouillé" (rouge)

15) Notifications

  • Icône cloche en topbar:
    • badge = nombre de notifications non lues
    • ouverture panneau = liste des non lues
    • fermeture panneau = marquage "lu" en masse

Règle métier de déclenchement

  • Les notifications de validation site ne sont pas envoyées ligne par ligne.
  • Une notification est créée uniquement quand un chef de site termine la validation complète:
    • condition: plus aucune ligne work_hours du site à la date concernée avec isSiteValid = false
    • destinataires: utilisateurs ROLE_ADMIN

16) Export PDF des heures annuelles

  • Accessible depuis la fiche employé (bouton imprimante à droite du nom)
  • Ouvre un drawer pour choisir l'année (civile, Jan-Déc)
  • Génère un PDF avec le détail jour par jour des heures de l'employé
  • Seuls les jours avec heures saisies, absence, week-end ou jour férié sont affichés
  • Les jours fériés apparaissent toujours sur une ligne dédiée (fond bleu clair) avec la mention "Férié : {nom}" dans la colonne Absence (même si aucune saisie)

Colonnes selon le mode de suivi

  • TIME (non-chauffeur): Date | Absence | Début matin | Fin matin | Début après-midi | Fin après-midi | Début soir | Fin soir | Total
  • PRESENCE (forfait): Date | Absence | Présence matin | Présence après-midi | Total
  • Chauffeur: Date | Absence | Heures jour | Heures nuit | Heures atelier | Total

Changement de contrat en cours d'année

  • Si l'employé change de mode de suivi (TIME/PRESENCE) ou de statut chauffeur en cours d'année, le PDF affiche des sections séparées avec les colonnes adaptées à chaque période
  • Le nom du contrat est affiché en sous-titre de chaque section

Calcul du total

  • TIME non-chauffeur: somme des créneaux matin + après-midi + soir, plus minutes créditées des absences countAsWorkedHours
  • Chauffeur: dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes + minutes créditées
  • PRESENCE: 0.5 par demi-journée présente (matin/après-midi), max 1.0
  • Jour férié Lun-Ven (hors Forfait, sans absence) : total = max(saisie + crédit absence, référence contractuelle) — même règle que l'écran Heures (cf. HolidayVirtualHoursResolver). Pour Forfait : pas de crédit virtuel, la ligne férié affiche juste l'éventuelle présence saisie.

Nom du fichier

  • Format: {nom}_{prenom}_{annee}.pdf