20 KiB
20 KiB
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_rolesavec rôleSITE_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:TIMEouPRESENCEweeklyHours(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
- table
Règles de période
CDI:- à la création d'une période:
endDatedoit être vide - en clôture d'un contrat en cours:
endDatepeut être renseignée
- à la création d'une période:
CDD/INTERIM:endDateobligatoire
endDatene 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:00et21:00-24:00
- fenêtres
- 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)
- mise à jour uniquement quand un employé (
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
AMouPM
- 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 jourPRESENCE(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
- remet
- 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_DATEdans.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
6bis) Heures Conducteurs
- Écran dédié
/driver-hourspour les employés dont le contrat est marquéisDriver = true - Les conducteurs sont exclus de l'écran
/hoursclassique - 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,nightHoursMinutesetworkshopHoursMinutes(entiers, minutes) surWorkHourhasBreakfast,hasLunch,hasDinner,hasOvernight(booleans) surWorkHour- 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 + workshopHoursMinutespour le calcul RTT (au lieu des créneaux morning/afternoon/evening)
- Le flag
isDriverest surEmployeeContractPeriod(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 contratavec 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
contractPaidLeaveSettledest 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/CDDnon forfait +FORFAIT):- exercice CP:
CDI/CDDnon forfait: du1er juin (YYYY-1)au31 mai (YYYY)(paramètreyear= année de fin d'exercice)FORFAIT: du1er janvier (YYYY)au31 décembre (YYYY)(paramètreyear= année civile)
- contrats
39h/35h/25h(et plus largement CDI/CDD non forfait hors4h):- acquis annuel CP:
25 - acquis annuel samedi:
5 - en cours d'acquisition jours:
25/12 = 2,08jours/mois - en cours d'acquisition samedis:
5/12 = 0,42samedi/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 (facteur0,80appliqué 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)
- premier mois de maladie (date début + 1 mois calendaire): acquisition normale (
- samedis acquis affiches: uniquement
opening_saturdays(report N-1)
- acquis annuel CP:
- contrat
4h:- acquis annuel CP:
10 - acquis annuel samedi:
0 - en cours d'acquisition:
0.83jour/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
- acquis annuel CP:
- 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)
- base annuelle:
- fractionné: saisie manuelle par la RH via
PATCH /employees/{id}/fractioned-days, stocké dansemployee_leave_balances.fractioned_days. Les jours fractionnés sont ajoutés aux acquis et au reste à prendre. - pour
CDI/CDDnon forfait:- pris CP: basé sur absences de type code
C(CONGÉ), en tenant compte des demi-journées - samedi pris: absences
Cposées le samedi (demi-journée incluse) - restants = acquis - pris (borné à 0)
- pris CP: basé sur absences de type code
- 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/CDDnon 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=truesur l'exercice précédent, le report vers l'exercice suivant est remis à0 - si une clôture
contractPaidLeaveSettled=trueexiste dans l'exercice courant, le calcul est réinitialisé à partir du lendemain de cette clôture (pas de continuité intra-exercice)
- le reliquat (
- 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 UIen cours d'acquisitionest arrêté au dernier jour du mois précédent
- règle de consommation:
- les absences s'imputent d'abord sur
acquis, puis suren cours d'acquisition - la prise sur
en cours d'acquisitionest autorisée (usage anticipé) en cours d'acquisitionpeut 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
- les absences s'imputent d'abord sur
- date d'arret de calcul:
reste à prendreest 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'acquisitionreste calculé jusqu'au dernier jour du mois précédent- exemple: au
11/03/2026, l'exercice2026déduit les absences posées jusqu'au31/05/2026, mais l'acquisition reste arrêtée au28/02/2026
- exercice CP:
- hors périmètre phase 1:
INTERIM(retour non supporté)
- endpoint de synthèse:
- onglet
RTT:- endpoint de synthèse:
GET /api/employees/{id}/rtt-summary?year=YYYY - exercice RTT: du
1er juin (YYYY-1)au31 mai (YYYY)(paramètreyear= 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
INTERIMet suiviPRESENCE: 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_balancespour(employee, year), le champopening_minutesest 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_minutesdu nouvel exercice - idempotent (ne recrée pas si la ligne existe)
- commande:
- 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, valeurs25ou50- les heures payées sont soustraites du disponible RTT (
availableMinutes -= totalPaidMinutes) - affichage: 2 lignes par mois dans le tableau (25% et 50%)
- saisie RH via
- 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)
- le compteur global RTT est affiché en heures (format
- endpoint de synthèse:
- onglet
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) 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(tablemileage_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
kilometersouamountdoit ê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
- Justificatif Km : upload via
- La suppression d'un frais supprime les deux fichiers justificatifs du disque
13) 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_hoursdu site à la date concernée avecisSiteValid = false - destinataires: utilisateurs
ROLE_ADMIN
- condition: plus aucune ligne