# 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 - Nature `INTERIM`: - pas de bonus 25% - pas de bonus 50% - pas de total récup ## 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, 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 - pas de calcul d'heures supplémentaires pour les conducteurs - 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 - Onglet congés: jours fériés affichés sur le calendrier avec fond `rgb(179, 229, 252)` et nom au survol - Règle courante: - absences bloquées sur jour férié - saisie d'heures autorisée ## 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 `Clôturer`: - bouton actif uniquement s'il existe un contrat en cours non déjà clôturé à la date du jour - ouvre un drawer en lecture seule (type/temps de travail/date de début) - champs saisissables: - `contractEndDate` (prérempli à aujourd'hui) - `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 - 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 - 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 - contrat `FORFAIT`: - base annuelle: `jours ouvrés de l'exercice (lundi-vendredi, hors jours fériés métropole) - 218` - 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) - 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 est affichée une seule fois, dans le mois qui contient le **samedi** de cette semaine - si le weekend tombe en début de mois suivant, c'est le mois suivant qui porte la semaine - 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` - 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: `-` | ## 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 | | 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) 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`