# 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) ## 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 ### 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 - 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. ## 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 - 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: jours acquis année civile. 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 | Comptage mois (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 ou absence sont affichés ### 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 ### Nom du fichier - Format: `{nom}_{prenom}_{annee}.pdf`