382 lines
20 KiB
Markdown
382 lines
20 KiB
Markdown
# 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) 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
|
||
- 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
|
||
- 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`
|
||
- 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`
|
||
- 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: `-` |
|
||
|
||
## 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) 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`
|