261 lines
12 KiB
Markdown
261 lines
12 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)
|
|
|
|
- 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
|
|
- Nature `INTERIM`:
|
|
- pas de bonus 25%
|
|
- pas de bonus 50%
|
|
- pas de total récup
|
|
|
|
## 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
|
|
- 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%)
|
|
- affichage:
|
|
- le compteur global RTT est affiché en **heures** (format `Xh00`)
|
|
|
|
## 10) 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`
|