Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c298f66993 | |||
| 7187989003 | |||
| 4b22270c60 | |||
| acbf1ccecb |
@@ -33,7 +33,7 @@
|
|||||||
- Contract nature (per period): CDI, CDD, INTERIM
|
- Contract nature (per period): CDI, CDD, INTERIM
|
||||||
- **Agence d'intérim** (`InterimAgency` entity, table `interim_agencies`): optionnelle sur `EmployeeContractPeriod` quand nature = INTERIM. Pas de CRUD UI — gérée en BDD. API lecture seule `GET /interim_agencies`. Affichée "Intérim (NomAgence)" sur la liste employés et l'historique contrat.
|
- **Agence d'intérim** (`InterimAgency` entity, table `interim_agencies`): optionnelle sur `EmployeeContractPeriod` quand nature = INTERIM. Pas de CRUD UI — gérée en BDD. API lecture seule `GET /interim_agencies`. Affichée "Intérim (NomAgence)" sur la liste employés et l'historique contrat.
|
||||||
- Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver`
|
- Employee contract history: `employee_contract_periods`, resolved by `EmployeeContractResolver`
|
||||||
- **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). Idem pour le **mode de suivi (TIME/PRESENCE), les heures hebdo et le libellé de contrat** sur la vue Jour : résolus à la date filtrée via `WorkHourDayContext` (`trackingMode`/`weeklyHours`/`contractType`/`contractName`, peuplés depuis `EmployeeContractResolver::resolveForEmployeeAndDate`), pas via `employee.contract` (résolu à aujourd'hui). Côté front, `resolveDayContract()` (`useHoursPage.ts`) pilote l'affichage et `handleSave` (heures vs présence par date).
|
- **Écrans Heures / Heures Conducteurs (vue jour)** : le libellé nature (CDI/CDD/Intérim) sous le nom de l'employé est résolu **à la date filtrée** via `WorkHourDayContext.contractNature` (alimenté par `EmployeeContractResolver::resolveNatureForEmployeeAndDate`), pas via `Employee.currentContractNature` (qui est résolu à aujourd'hui). Idem pour le **mode de suivi (TIME/PRESENCE), les heures hebdo et le libellé de contrat** sur la vue Jour : résolus à la date filtrée via `WorkHourDayContext` (`trackingMode`/`weeklyHours`/`contractType`/`contractName`, peuplés depuis `EmployeeContractResolver::resolveForEmployeeAndDate`), pas via `employee.contract` (résolu à aujourd'hui). Côté front, `resolveDayContract()` (`useHoursPage.ts`) pilote l'affichage et `handleSave` (heures vs présence par date). **Jours travaillés (CUSTOM)** : le libellé sous le nom affiche en suffixe les jours du planning `workDaysHours` au format court `LU,MA,ME,JE,VE` (ex. `BUREAU — CDI — LU,JE`). Exposé via `WorkHourDayContext.workDaysHours` (peuplé par `EmployeeContractResolver::resolveWorkDaysMinutesForEmployeeAndDate`, à la date filtrée), formaté front par `formatWorkedDaysShort` (`utils/contract.ts`) et accédé via `getRowWorkedDaysLabel` (`useHoursPage.ts`). Affiché **uniquement écran Heures** (`HoursDayView.vue`, mobile + desktop) ; naturellement limité aux CUSTOM (seuls eux ont `workDaysHours` → null sinon, rien affiché). Pas sur Heures Conducteurs (pas de planning workDaysHours).
|
||||||
- **Exports heures annuelles** (par salarié `EmployeeYearlyHoursPrintProvider` + tous `EmployeeYearlyHoursBulkPrintProvider`, via `YearlyHoursExportBuilder`) : **tous les jours sous contrat sont affichés**, même vides ou non saisis (jusqu'à aujourd'hui). Seuls les jours hors contrat sont omis (`buildSegments` : un seul filtre `!$hasData && null === $contract`). Ne pas réintroduire de saut des jours de semaine vides. Samedis/dimanches grisés (`#c0c0c0`) dans les templates `employee-yearly-hours/print*.html.twig`. NB : l'export *tous employés* sur l'année peut dépasser `memory_limit=256M` (Dompdf) — limitation pré-existante, voir avec l'infra si besoin.
|
- **Exports heures annuelles** (par salarié `EmployeeYearlyHoursPrintProvider` + tous `EmployeeYearlyHoursBulkPrintProvider`, via `YearlyHoursExportBuilder`) : **tous les jours sous contrat sont affichés**, même vides ou non saisis (jusqu'à aujourd'hui). Seuls les jours hors contrat sont omis (`buildSegments` : un seul filtre `!$hasData && null === $contract`). Ne pas réintroduire de saut des jours de semaine vides. Samedis/dimanches grisés (`#c0c0c0`) dans les templates `employee-yearly-hours/print*.html.twig`. NB : l'export *tous employés* sur l'année peut dépasser `memory_limit=256M` (Dompdf) — limitation pré-existante, voir avec l'infra si besoin.
|
||||||
- **Export heures vue Jour** (`WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=`, `ROLE_USER`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="(isAdmin || isSiteManager) && viewMode === 'day'"`, masqué en vue Semaine et pour `ROLE_SELF`). **Accessible aux admins ET aux chefs de site** : le périmètre est résolu côté backend via `EmployeeRepository::findScoped($user)` (admin → tous les sites, chef de site → ses sites uniquement, cf. `EmployeeScopeService`), donc un `siteIds` hors périmètre est ignoré ; le drawer front ne propose que les sites visibles (`sites` dérivé des employés scopés). PDF A4 portrait d'**une seule journée**, **regroupé par site**, colonnes de la vue Jour **sans « Valider »** (colonne **Total en gras**). Mêmes employés que l'écran : non-conducteurs, sous contrat à la date, sites cochés et dans le périmètre (lignes vides incluses). **Tri intra-site identique au calendrier** : `displayOrder` (ordre manuel), puis nom, puis prénom (cf. `compareEmployeesInSite` front). Calcul des cellules mutualisé via `YearlyHoursExportBuilder::buildDayRowsForEmployees` (Jour/Nuit/Total incluent crédit absence + crédit virtuel férié). Colonne **Statut = code** du type d'absence (`AbsenceType::getCode`, ex. `AT`) sur sa couleur de fond ; férié sans absence → nom du férié sur `#b3e5fc`. Chaque row porte `statut` (code), `statutLabel` (libellé, pour la légende) et `statutColor`. **Légende** sous le tableau (carré coloré contenant le code + libellé à droite), construite côté provider à partir des codes présents (hors férié, dédupliquée par code, triée). Gabarit `templates/work-hour-day-export/print.html.twig`.
|
- **Export heures vue Jour** (`WorkHourDayExportProvider`, endpoint `GET /work-hours/day-export?workDate=&siteIds=`, `ROLE_USER`) : bouton « Exporter » à droite du titre « Heures », **visible uniquement en vue Jour** (`v-if="(isAdmin || isSiteManager) && viewMode === 'day'"`, masqué en vue Semaine et pour `ROLE_SELF`). **Accessible aux admins ET aux chefs de site** : le périmètre est résolu côté backend via `EmployeeRepository::findScoped($user)` (admin → tous les sites, chef de site → ses sites uniquement, cf. `EmployeeScopeService`), donc un `siteIds` hors périmètre est ignoré ; le drawer front ne propose que les sites visibles (`sites` dérivé des employés scopés). PDF A4 portrait d'**une seule journée**, **regroupé par site**, colonnes de la vue Jour **sans « Valider »** (colonne **Total en gras**). Mêmes employés que l'écran : non-conducteurs, sous contrat à la date, sites cochés et dans le périmètre (lignes vides incluses). **Tri intra-site identique au calendrier** : `displayOrder` (ordre manuel), puis nom, puis prénom (cf. `compareEmployeesInSite` front). Calcul des cellules mutualisé via `YearlyHoursExportBuilder::buildDayRowsForEmployees` (Jour/Nuit/Total incluent crédit absence + crédit virtuel férié). Colonne **Statut = code** du type d'absence (`AbsenceType::getCode`, ex. `AT`) sur sa couleur de fond ; férié sans absence → nom du férié sur `#b3e5fc`. Chaque row porte `statut` (code), `statutLabel` (libellé, pour la légende) et `statutColor`. **Légende** sous le tableau (carré coloré contenant le code + libellé à droite), construite côté provider à partir des codes présents (hors férié, dédupliquée par code, triée). Gabarit `templates/work-hour-day-export/print.html.twig`.
|
||||||
- **Export Contingent heures de nuit** (`NightHoursContingentPrintProvider`, endpoint `GET /night-hours-contingent/print?year=YYYY`, `ROLE_USER`) : option « Contingent H.nuit » du drawer Export de la liste employés. PDF **A4 paysage**, lignes = employés **groupés par site** et triés `displayOrder`/nom/prénom (comme le day-export), colonnes = 12 mois civils, chacun avec 2 sous-colonnes **H.nuit** et **N.jours**. Heures de nuit = minutes dans la fenêtre **21h→6h** via le service partagé `App\Service\WorkHours\NightHoursCalculator` (source unique mutualisée avec `WorkHourWeeklySummaryProvider`, `YearlyHoursExportBuilder`, `RttRecoveryComputationService` et `SalaryRecapPrintProvider`). Conducteurs inclus via `WorkHour.nightHoursMinutes`. **N.jours** = nb de jours où minutes de nuit ≥ 240 (4h). **Aucun crédit** absence/férié. Agrégation : `App\Service\WorkHours\NightContingentExportBuilder`. Gabarit `templates/night-hours-contingent/print.html.twig`.
|
- **Export Contingent heures de nuit** (`NightHoursContingentPrintProvider`, endpoint `GET /night-hours-contingent/print?year=YYYY`, `ROLE_USER`) : option « Contingent H.nuit » du drawer Export de la liste employés. PDF **A4 paysage**, lignes = employés **groupés par site** et triés `displayOrder`/nom/prénom (comme le day-export), colonnes = 12 mois civils, chacun avec 2 sous-colonnes **H.nuit** et **N.jours**. Heures de nuit = minutes dans la fenêtre **21h→6h** via le service partagé `App\Service\WorkHours\NightHoursCalculator` (source unique mutualisée avec `WorkHourWeeklySummaryProvider`, `YearlyHoursExportBuilder`, `RttRecoveryComputationService` et `SalaryRecapPrintProvider`). Conducteurs inclus via `WorkHour.nightHoursMinutes`. **N.jours** = nb de jours où minutes de nuit ≥ 240 (4h). **Aucun crédit** absence/férié. Agrégation : `App\Service\WorkHours\NightContingentExportBuilder`. Gabarit `templates/night-hours-contingent/print.html.twig`.
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
- Contracts <= 35h: +25% from 35h to 43h, +50% beyond
|
- Contracts <= 35h: +25% from 35h to 43h, +50% beyond
|
||||||
- Contracts >= 39h: +25% from 39h to 43h, +50% beyond
|
- Contracts >= 39h: +25% from 39h to 43h, +50% beyond
|
||||||
- CUSTOM contracts (weeklyHours ≠ 35 and ≠ 39, not INTERIM/FORFAIT): reference = actual contractual hours, no 25%/50% bonuses (1h overtime = 1h recovery). **Le déficit (heures travaillées < heures contractuelles) réduit le cumul RTT 1:1** (peut devenir négatif, reporté à l'exercice suivant). Implémenté via `WeekRecoveryDetail::isFlatRecovery` / `EmployeeRttWeekSummary::isFlatRecovery` : ces semaines portent leur récup/déficit signé dans `totalMinutes` (`RttRecoveryComputationService::buildWeekRecoveryDetail`) et `EmployeeRttSummaryProvider::applyDeficitCascade` **ne draine pas** les tranches 25/50 pour elles (colonnes 25%/50% restent à 0). Le `RttClosingBalanceService::fold` reporte le déficit en N+1.
|
- CUSTOM contracts (weeklyHours ≠ 35 and ≠ 39, not INTERIM/FORFAIT): reference = actual contractual hours, no 25%/50% bonuses (1h overtime = 1h recovery). **Le déficit (heures travaillées < heures contractuelles) réduit le cumul RTT 1:1** (peut devenir négatif, reporté à l'exercice suivant). Implémenté via `WeekRecoveryDetail::isFlatRecovery` / `EmployeeRttWeekSummary::isFlatRecovery` : ces semaines portent leur récup/déficit signé dans `totalMinutes` (`RttRecoveryComputationService::buildWeekRecoveryDetail`) et `EmployeeRttSummaryProvider::applyDeficitCascade` **ne draine pas** les tranches 25/50 pour elles (colonnes 25%/50% restent à 0). Le `RttClosingBalanceService::fold` reporte le déficit en N+1.
|
||||||
- **Jour de solidarité (Lundi de Pentecôte) — CUSTOM < 35h** : le jour est neutralisé et chargé d'un déficit forfaitaire `7/35 × weeklyHours` = **12 min par heure hebdo** (4h→48 min, 25h→5h, 28h→5h36), retranché du cumul RTT (signé, reporté N+1, ne draine pas les tranches 25/50 qui restent à 0). Net = exactement −prorata quel que soit ce qui est posé ce jour-là (RTT, heures, vide) → pas de double comptage avec le RTT que la RH pose aussi sur ce jour. Hors périmètre : 35h/39h/Forfait/Intérim et CUSTOM ≥ 35h (inchangés ; la RH y pose un RTT qui draine ~7h). Date via `App\Service\Rtt\SolidarityDayResolver` (computus, indépendant d'`EXCLUDED_PUBLIC_HOLIDAYS`). Appliqué dans `RttRecoveryComputationService::{resolveSolidarityDatesInRange, computeSolidarityDeficitAdjustment}`.
|
- **Jour de solidarité (Lundi de Pentecôte) — CUSTOM < 35h** : le jour est neutralisé et chargé d'un déficit forfaitaire `7/35 × weeklyHours` = **12 min par heure hebdo** (4h→48 min, 25h→5h, 28h→5h36), retranché du cumul RTT (signé, reporté N+1, ne draine pas les tranches 25/50 qui restent à 0). Net = exactement −prorata quel que soit ce qui est posé ce jour-là (RTT, heures, vide) → pas de double comptage avec le RTT que la RH pose aussi sur ce jour. **Garde : uniquement si le salarié travaille le lundi** (`workDaysHours[lundi] > 0`, i.e. `expectedMinutes > 0`) ; un temps partiel ne travaillant jamais le lundi (ex. Nadia, Mar+Ven) **ne porte aucun déficit** (sinon `(0 − 0) − prorata` lui facturerait à tort le prorata). Hors périmètre : 35h/39h/Forfait/Intérim et CUSTOM ≥ 35h (inchangés ; la RH y pose un RTT qui draine ~7h). Date via `App\Service\Rtt\SolidarityDayResolver` (computus, indépendant d'`EXCLUDED_PUBLIC_HOLIDAYS`). Appliqué dans `RttRecoveryComputationService::{resolveSolidarityDatesInRange, computeSolidarityDeficitAdjustment}`.
|
||||||
- **Ancre de semaine (type de contrat)** : le type/nature de contrat d'une semaine RTT est résolu sur le **premier jour contracté** de la semaine, pas sur le lundi (`RttRecoveryComputationService::resolveWeekAnchorDate`). Sinon une semaine d'embauche en milieu de semaine (lundi hors contrat) serait classée CUSTOM → bonus 25%/50% désactivés à tort. Ex. CDD 39h embauché le jeudi : la semaine reste 39h, le seuil 25% est proraté aux jours contractés (`computeWeeklyOvertime25StartMinutes`), donc les heures au-delà ouvrent bien le +25%.
|
- **Ancre de semaine (type de contrat)** : le type/nature de contrat d'une semaine RTT est résolu sur le **premier jour contracté** de la semaine, pas sur le lundi (`RttRecoveryComputationService::resolveWeekAnchorDate`). Sinon une semaine d'embauche en milieu de semaine (lundi hors contrat) serait classée CUSTOM → bonus 25%/50% désactivés à tort. Ex. CDD 39h embauché le jeudi : la semaine reste 39h, le seuil 25% est proraté aux jours contractés (`computeWeeklyOvertime25StartMinutes`), donc les heures au-delà ouvrent bien le +25%.
|
||||||
- **Plafond 25%/50% proraté (mi-semaine)** : le plafond séparant 25% et 50% n'est **pas** codé en dur à 43h mais vaut `seuil_départ_proraté + largeur_bande_25%` (`RttRecoveryComputationService::{resolveOvertime25BandWidthMinutes, computeOvertimeBaseMinutes}`). Largeur = 43h − base (4h pour un 39h, 8h pour un 35h). Pour une semaine pleine le plafond redonne 43h (aucune régression) ; pour une embauche mi-semaine il se décale avec le départ, ouvrant la tranche 50%. Témoin Dylan (CDD 39h embauché jeudi, 22h) : 4h à 25% + 3h à 50%. **Hors périmètre** : l'écran Heures (`WorkHourWeeklySummaryProvider`) n'a pas cette proratisation (calcul dupliqué, laissé tel quel par décision métier).
|
- **Plafond 25%/50% proraté (mi-semaine)** : le plafond séparant 25% et 50% n'est **pas** codé en dur à 43h mais vaut `seuil_départ_proraté + largeur_bande_25%` (`RttRecoveryComputationService::{resolveOvertime25BandWidthMinutes, computeOvertimeBaseMinutes}`). Largeur = 43h − base (4h pour un 39h, 8h pour un 35h). Pour une semaine pleine le plafond redonne 43h (aucune régression) ; pour une embauche mi-semaine il se décale avec le départ, ouvrant la tranche 50%. Témoin Dylan (CDD 39h embauché jeudi, 22h) : 4h à 25% + 3h à 50%. **Hors périmètre** : l'écran Heures (`WorkHourWeeklySummaryProvider`) n'a pas cette proratisation (calcul dupliqué, laissé tel quel par décision métier).
|
||||||
- INTERIM: no overtime bonuses, no recovery time
|
- INTERIM: no overtime bonuses, no recovery time
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.117'
|
app.version: '0.1.119'
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ Documents complementaires:
|
|||||||
- Libellé nature de contrat (CDI/CDD/Intérim) affiché sous le nom:
|
- Libellé nature de contrat (CDI/CDD/Intérim) affiché sous le nom:
|
||||||
- résolu à la date filtrée (période de contrat couvrant ce jour), pas à aujourd'hui
|
- résolu à la date filtrée (période de contrat couvrant ce jour), pas à aujourd'hui
|
||||||
- masqué si aucun contrat à cette date (cas rarissime en vue jour puisque l'employé est alors déjà filtré)
|
- masqué si aucun contrat à cette date (cas rarissime en vue jour puisque l'employé est alors déjà filtré)
|
||||||
|
- **Jours travaillés (contrats CUSTOM)** : pour un contrat CUSTOM (planning `workDaysHours` renseigné), les jours effectivement travaillés sont affichés en suffixe du libellé `Site — Nature`, au format court `LU,MA,ME,JE,VE` (ex. `BUREAU — CDI — LU,JE` pour un temps partiel travaillant lundi et jeudi). Résolu à la date filtrée. Les contrats 35h/39h/Forfait/Intérim n'ont pas de planning → aucun suffixe. Écran Heures uniquement (pas Heures Conducteurs).
|
||||||
- **Vue Jour (Heures) — contrat à la date affichée** : le mode de suivi (saisie d'heures vs cases de présence), le libellé de contrat et la logique de sauvegarde sont résolus selon la période de contrat valable à la date filtrée (champs `trackingMode`/`weeklyHours`/`contractType`/`contractName` portés par `WorkHourDayContext`, alimentés par `EmployeeContractResolver::resolveForEmployeeAndDate`), et non selon le contrat courant de l'employé. Un salarié passé 39h/35h → Forfait conserve donc la saisie d'heures sur ses dates antérieures à la bascule, et bascule en cases de présence à partir de la date de passage en forfait. La vue Semaine était déjà résolue par date.
|
- **Vue Jour (Heures) — contrat à la date affichée** : le mode de suivi (saisie d'heures vs cases de présence), le libellé de contrat et la logique de sauvegarde sont résolus selon la période de contrat valable à la date filtrée (champs `trackingMode`/`weeklyHours`/`contractType`/`contractName` portés par `WorkHourDayContext`, alimentés par `EmployeeContractResolver::resolveForEmployeeAndDate`), et non selon le contrat courant de l'employé. Un salarié passé 39h/35h → Forfait conserve donc la saisie d'heures sur ses dates antérieures à la bascule, et bascule en cases de présence à partir de la date de passage en forfait. La vue Semaine était déjà résolue par date.
|
||||||
- **Exports heures annuelles (par salarié et tous salariés)** : affichent **tous les jours sous contrat**, même vides ou non saisis, jusqu'à la date du jour ; seuls les jours hors contrat (avant embauche, après départ, suspension) sont omis. Les samedis et dimanches sont grisés (gris foncé), les jours fériés en bleu.
|
- **Exports heures annuelles (par salarié et tous salariés)** : affichent **tous les jours sous contrat**, même vides ou non saisis, jusqu'à la date du jour ; seuls les jours hors contrat (avant embauche, après départ, suspension) sont omis. Les samedis et dimanches sont grisés (gris foncé), les jours fériés en bleu.
|
||||||
- **Récap salaire (export PDF mensuel)** : seuls les salariés ayant un contrat couvrant tout ou partie du mois imprimé apparaissent (filtre `hasContractInRange`). Un salarié dont le contrat est terminé avant le mois (ex. parti en février) n'est pas listé sur le récap des mois suivants.
|
- **Récap salaire (export PDF mensuel)** : seuls les salariés ayant un contrat couvrant tout ou partie du mois imprimé apparaissent (filtre `hasContractInRange`). Un salarié dont le contrat est terminé avant le mois (ex. parti en février) n'est pas listé sur le récap des mois suivants.
|
||||||
@@ -154,6 +155,10 @@ soit ce qui y est saisi) et applique un déficit forfaitaire `7/35 × heuresHebd
|
|||||||
déficits/surplus de la semaine. Date calculée par computus (Pâques + 50 jours),
|
déficits/surplus de la semaine. Date calculée par computus (Pâques + 50 jours),
|
||||||
indépendante de la liste `EXCLUDED_PUBLIC_HOLIDAYS`.
|
indépendante de la liste `EXCLUDED_PUBLIC_HOLIDAYS`.
|
||||||
|
|
||||||
|
Le déficit ne s'applique **que si le salarié travaille le lundi** (jour de solidarité
|
||||||
|
planifié au contrat, `workDaysHours[lundi] > 0`). Un temps partiel ne travaillant jamais
|
||||||
|
le lundi (ex. Mar+Ven) n'est pas concerné : aucun déficit n'est imputé.
|
||||||
|
|
||||||
- Nature `INTERIM`:
|
- Nature `INTERIM`:
|
||||||
- pas de bonus 25%
|
- pas de bonus 25%
|
||||||
- pas de bonus 50%
|
- pas de bonus 50%
|
||||||
|
|||||||
+4
-2
@@ -32,8 +32,10 @@ Techniquement : `WeekRecoveryDetail::isFlatRecovery` marque ces semaines ;
|
|||||||
Sur la semaine du Lundi de Pentecôte, un contrat CUSTOM < 35h porte un déficit
|
Sur la semaine du Lundi de Pentecôte, un contrat CUSTOM < 35h porte un déficit
|
||||||
forfaitaire de `7/35 × heuresHebdo` (12 min/h hebdo, ex. 4h → −0h48) dans les colonnes
|
forfaitaire de `7/35 × heuresHebdo` (12 min/h hebdo, ex. 4h → −0h48) dans les colonnes
|
||||||
Heure / Total / Cumul (25 %/50 % restent à 0). Le montant est fixe et inconditionnel :
|
Heure / Total / Cumul (25 %/50 % restent à 0). Le montant est fixe et inconditionnel :
|
||||||
il ne dépend pas des heures saisies ni du RTT que la RH pose ce jour-là. Les contrats
|
il ne dépend pas des heures saisies ni du RTT que la RH pose ce jour-là. Un salarié qui
|
||||||
35h/39h ne sont pas concernés ici (leur RTT posé draine le cumul normalement).
|
ne travaille pas le lundi (lundi non planifié au contrat) n'est pas concerné : aucun
|
||||||
|
déficit. Les contrats 35h/39h ne sont pas concernés ici (leur RTT posé draine le cumul
|
||||||
|
normalement).
|
||||||
|
|
||||||
## Sélecteur d'année
|
## Sélecteur d'année
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ Puis `buildWeekRecoveryDetail(...)` est appelé tel quel : pour un CUSTOM,
|
|||||||
| Jour de solidarité avant `rttStartDate` | Pas de déficit (semaine zéro-ée en amont). |
|
| Jour de solidarité avant `rttStartDate` | Pas de déficit (semaine zéro-ée en amont). |
|
||||||
| Changement de contrat dans la semaine | Contrat lu **au jour de solidarité**, pas à l'ancre de semaine. |
|
| Changement de contrat dans la semaine | Contrat lu **au jour de solidarité**, pas à l'ancre de semaine. |
|
||||||
| Salarié non contracté ce jour-là | `contractAtS = null` → pas de déficit. |
|
| Salarié non contracté ce jour-là | `contractAtS = null` → pas de déficit. |
|
||||||
|
| Salarié CUSTOM < 35h ne travaillant pas le lundi (ex. Mar+Ven) | `expectedMinutes = workDaysHours[lundi] = 0` → pas de déficit (garde `0 === $expectedMinutes`). |
|
||||||
| CUSTOM ≥ 35h (36–38h) | Hors périmètre → pas de déficit. |
|
| CUSTOM ≥ 35h (36–38h) | Hors périmètre → pas de déficit. |
|
||||||
| 35h/39h avec RTT posé | Inchangé (drainage ~7h via la cascade existante). |
|
| 35h/39h avec RTT posé | Inchangé (drainage ~7h via la cascade existante). |
|
||||||
| Autre déficit/surplus la même semaine | Le forfait s'y cumule. |
|
| Autre déficit/surplus la même semaine | Le forfait s'y cumule. |
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
<span class="font-normal text-neutral-600 text-sm">({{ contractLabel(employee) }})</span>
|
<span class="font-normal text-neutral-600 text-sm">({{ contractLabel(employee) }})</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="text-sm text-neutral-500 truncate">
|
<p class="text-sm text-neutral-500 truncate">
|
||||||
{{ employee.site?.name ?? 'Sans site' }}<span v-if="getRowContractNature(employee.id)"> — {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}</span>
|
{{ employee.site?.name ?? 'Sans site' }}<span v-if="getRowContractNature(employee.id)"> — {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}</span><span v-if="getRowWorkedDaysLabel(employee.id)"> — {{ getRowWorkedDaysLabel(employee.id) }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p class="text-neutral-500 truncate inline-flex items-center gap-2">
|
<p class="text-neutral-500 truncate inline-flex items-center gap-2">
|
||||||
<span>
|
<span>
|
||||||
{{ employee.site?.name ?? 'Sans site' }}<span v-if="getRowContractNature(employee.id)"> — {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}</span>
|
{{ employee.site?.name ?? 'Sans site' }}<span v-if="getRowContractNature(employee.id)"> — {{ contractNatureLabel(getRowContractNature(employee.id) ?? undefined) }}</span><span v-if="getRowWorkedDaysLabel(employee.id)"> — {{ getRowWorkedDaysLabel(employee.id) }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="isAdmin && (rows[employee.id]?.isSiteValid ?? false)"
|
v-if="isAdmin && (rows[employee.id]?.isSiteValid ?? false)"
|
||||||
@@ -406,6 +406,7 @@ const props = defineProps<{
|
|||||||
hasRowFormation: (employeeId: number) => boolean
|
hasRowFormation: (employeeId: number) => boolean
|
||||||
getRowFormationLabel: (employeeId: number) => string
|
getRowFormationLabel: (employeeId: number) => string
|
||||||
getRowContractNature: (employeeId: number) => 'CDI' | 'CDD' | 'INTERIM' | null
|
getRowContractNature: (employeeId: number) => 'CDI' | 'CDD' | 'INTERIM' | null
|
||||||
|
getRowWorkedDaysLabel: (employeeId: number) => string | null
|
||||||
getRowUpdatedAt: (employeeId: number) => string
|
getRowUpdatedAt: (employeeId: number) => string
|
||||||
getPresenceDayValue: (employeeId: number) => string
|
getPresenceDayValue: (employeeId: number) => string
|
||||||
onAbsenceClick: (employeeId: number) => void
|
onAbsenceClick: (employeeId: number) => void
|
||||||
|
|||||||
@@ -517,6 +517,11 @@ export const useHoursPage = () => {
|
|||||||
return dayContextByEmployeeId.value.get(employeeId)?.contractNature ?? null
|
return dayContextByEmployeeId.value.get(employeeId)?.contractNature ?? null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Jours travaillés du planning (contrats CUSTOM uniquement), ex. "LU,VE". null sinon.
|
||||||
|
const getRowWorkedDaysLabel = (employeeId: number): string | null => {
|
||||||
|
return formatWorkedDaysShort(dayContextByEmployeeId.value.get(employeeId)?.workDaysHours)
|
||||||
|
}
|
||||||
|
|
||||||
const getRowUpdatedAt = (employeeId: number): string => {
|
const getRowUpdatedAt = (employeeId: number): string => {
|
||||||
const raw = rows.value[employeeId]?.updatedAt
|
const raw = rows.value[employeeId]?.updatedAt
|
||||||
if (!raw) return ''
|
if (!raw) return ''
|
||||||
@@ -1207,6 +1212,7 @@ export const useHoursPage = () => {
|
|||||||
hasRowFormation,
|
hasRowFormation,
|
||||||
getRowFormationLabel,
|
getRowFormationLabel,
|
||||||
getRowContractNature,
|
getRowContractNature,
|
||||||
|
getRowWorkedDaysLabel,
|
||||||
getRowUpdatedAt,
|
getRowUpdatedAt,
|
||||||
getPresenceDayValue,
|
getPresenceDayValue,
|
||||||
openAbsenceDrawer,
|
openAbsenceDrawer,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export const documentationSections: DocSection[] = [
|
|||||||
{ type: 'list', content: 'Boutons "Hier" / "Aujourd\'hui" / "Demain" pour naviguer rapidement\nSélecteur de date pour choisir une date spécifique\nFiltrage par site si vous avez accès à plusieurs sites' },
|
{ type: 'list', content: 'Boutons "Hier" / "Aujourd\'hui" / "Demain" pour naviguer rapidement\nSélecteur de date pour choisir une date spécifique\nFiltrage par site si vous avez accès à plusieurs sites' },
|
||||||
{ type: 'paragraph', content: 'Seuls les employés ayant un contrat actif à la date sélectionnée sont affichés.' },
|
{ type: 'paragraph', content: 'Seuls les employés ayant un contrat actif à la date sélectionnée sont affichés.' },
|
||||||
{ type: 'note', content: 'Sous le nom de l\'employé, la nature du contrat (CDI / CDD / Intérim) affichée correspond à la période couvrant la date filtrée, et non à aujourd\'hui.' },
|
{ type: 'note', content: 'Sous le nom de l\'employé, la nature du contrat (CDI / CDD / Intérim) affichée correspond à la période couvrant la date filtrée, et non à aujourd\'hui.' },
|
||||||
|
{ type: 'paragraph', content: 'Pour un contrat à temps partiel avec un planning de jours travaillés (contrat « personnalisé »), les jours travaillés sont rappelés à la suite du libellé site et nature, en abrégé : par exemple « BUREAU — CDI — LU,JE » pour un salarié travaillant le lundi et le jeudi. Les contrats 35h, 39h, forfait et intérim n\'affichent pas ce rappel.' },
|
||||||
{ type: 'paragraph', content: 'Sur la vue Jour, l\'affichage (saisie d\'heures ou cases de présence) et le libellé de contrat correspondent au contrat de l\'employé à la date consultée. Si un salarié a changé de type de contrat (par exemple un passage en forfait), les jours antérieurs à ce changement restent affichés selon l\'ancien contrat.' },
|
{ type: 'paragraph', content: 'Sur la vue Jour, l\'affichage (saisie d\'heures ou cases de présence) et le libellé de contrat correspondent au contrat de l\'employé à la date consultée. Si un salarié a changé de type de contrat (par exemple un passage en forfait), les jours antérieurs à ce changement restent affichés selon l\'ancien contrat.' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -537,7 +538,7 @@ export const documentationSections: DocSection[] = [
|
|||||||
{ type: 'note', content: 'Au passage à l\'exercice suivant (1er juin), le « Report N-1 » du nouvel exercice reprend exactement le « Disponible » de fin d\'exercice précédent, c\'est-à-dire report précédent + acquis − RTT payés. Le report déjà présent en début d\'année n\'est donc jamais perdu.' },
|
{ type: 'note', content: 'Au passage à l\'exercice suivant (1er juin), le « Report N-1 » du nouvel exercice reprend exactement le « Disponible » de fin d\'exercice précédent, c\'est-à-dire report précédent + acquis − RTT payés. Le report déjà présent en début d\'année n\'est donc jamais perdu.' },
|
||||||
{ type: 'paragraph', content: 'La colonne "Cumul" affiche le solde RTT à la fin de chaque semaine : Report N-1 + somme des heures hebdomadaires jusqu\'à la semaine concernée − paiements RTT des mois précédents. Un paiement enregistré sur le mois M n\'est déduit qu\'à partir des semaines du mois M+1. Permet la comparaison ligne à ligne avec un suivi RH externe (Excel).' },
|
{ type: 'paragraph', content: 'La colonne "Cumul" affiche le solde RTT à la fin de chaque semaine : Report N-1 + somme des heures hebdomadaires jusqu\'à la semaine concernée − paiements RTT des mois précédents. Un paiement enregistré sur le mois M n\'est déduit qu\'à partir des semaines du mois M+1. Permet la comparaison ligne à ligne avec un suivi RH externe (Excel).' },
|
||||||
{ type: 'note', content: 'Contrats CUSTOM (ex. 4h) : une semaine travaillée sous les heures contractuelles génère un déficit qui réduit le cumul RTT (1h manquante = -1h), sans tranches 25/50. Le cumul peut devenir négatif et est reporté à l\'exercice suivant.' },
|
{ type: 'note', content: 'Contrats CUSTOM (ex. 4h) : une semaine travaillée sous les heures contractuelles génère un déficit qui réduit le cumul RTT (1h manquante = -1h), sans tranches 25/50. Le cumul peut devenir négatif et est reporté à l\'exercice suivant.' },
|
||||||
{ type: 'paragraph', content: 'Jour de solidarité : pour un contrat de moins de 35h, le Lundi de Pentecôte applique un déficit fixe proportionnel (7/35 des heures hebdomadaires, soit 12 minutes par heure : 4h donne 48 min). Ce déficit réduit le cumul RTT, quel que soit ce qui est saisi ce jour-là.' },
|
{ type: 'paragraph', content: 'Jour de solidarité : pour un contrat de moins de 35h, le Lundi de Pentecôte applique un déficit fixe proportionnel (7/35 des heures hebdomadaires, soit 12 minutes par heure : 4h donne 48 min). Ce déficit réduit le cumul RTT, quel que soit ce qui est saisi ce jour-là. Un salarié qui ne travaille pas le lundi n\'est pas concerné : aucun déficit ne lui est imputé.' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -87,6 +87,7 @@
|
|||||||
:has-row-formation="hasRowFormation"
|
:has-row-formation="hasRowFormation"
|
||||||
:get-row-formation-label="getRowFormationLabel"
|
:get-row-formation-label="getRowFormationLabel"
|
||||||
:get-row-contract-nature="getRowContractNature"
|
:get-row-contract-nature="getRowContractNature"
|
||||||
|
:get-row-worked-days-label="getRowWorkedDaysLabel"
|
||||||
:get-row-updated-at="getRowUpdatedAt"
|
:get-row-updated-at="getRowUpdatedAt"
|
||||||
:get-presence-day-value="getPresenceDayValue"
|
:get-presence-day-value="getPresenceDayValue"
|
||||||
:on-absence-click="openAbsenceDrawer"
|
:on-absence-click="openAbsenceDrawer"
|
||||||
@@ -215,6 +216,7 @@ const {
|
|||||||
hasRowFormation,
|
hasRowFormation,
|
||||||
getRowFormationLabel,
|
getRowFormationLabel,
|
||||||
getRowContractNature,
|
getRowContractNature,
|
||||||
|
getRowWorkedDaysLabel,
|
||||||
getRowUpdatedAt,
|
getRowUpdatedAt,
|
||||||
getPresenceDayValue,
|
getPresenceDayValue,
|
||||||
openAbsenceDrawer,
|
openAbsenceDrawer,
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export type WorkHourDayContextRow = {
|
|||||||
weeklyHours?: number | null
|
weeklyHours?: number | null
|
||||||
contractType?: ContractType | null
|
contractType?: ContractType | null
|
||||||
contractName?: string | null
|
contractName?: string | null
|
||||||
|
workDaysHours?: Record<number, number> | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkHourDayContext = {
|
export type WorkHourDayContext = {
|
||||||
|
|||||||
@@ -37,6 +37,26 @@ export const requiresWorkDaysHours = (
|
|||||||
|
|
||||||
const DAY_SHORT_LABELS: Record<number, string> = { 1: 'Lun', 2: 'Mar', 3: 'Mer', 4: 'Jeu', 5: 'Ven' }
|
const DAY_SHORT_LABELS: Record<number, string> = { 1: 'Lun', 2: 'Mar', 3: 'Mer', 4: 'Jeu', 5: 'Ven' }
|
||||||
|
|
||||||
|
const DAY_TINY_LABELS: Record<number, string> = { 1: 'LU', 2: 'MA', 3: 'ME', 4: 'JE', 5: 'VE' }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Very compact worked-days summary for the day view header, e.g. "LU,VE".
|
||||||
|
* Lists the iso days actually worked (minutes > 0), uppercase 2-letter, comma-separated.
|
||||||
|
* Returns null when the schedule is empty/unset (non-CUSTOM contracts have no schedule).
|
||||||
|
*/
|
||||||
|
export const formatWorkedDaysShort = (
|
||||||
|
workDaysHours: Record<number, number> | null | undefined
|
||||||
|
): string | null => {
|
||||||
|
if (!workDaysHours) return null
|
||||||
|
const days = Object.entries(workDaysHours)
|
||||||
|
.map(([iso, minutes]) => [Number(iso), Number(minutes)] as const)
|
||||||
|
.filter(([iso, minutes]) => iso >= 1 && iso <= 5 && minutes > 0)
|
||||||
|
.sort(([a], [b]) => a - b)
|
||||||
|
.map(([iso]) => DAY_TINY_LABELS[iso])
|
||||||
|
if (days.length === 0) return null
|
||||||
|
return days.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compact human-readable summary of a per-day schedule, e.g. "Lun 2h, Jeu 2h".
|
* Compact human-readable summary of a per-day schedule, e.g. "Lun 2h, Jeu 2h".
|
||||||
* Returns null when the schedule is empty/unset.
|
* Returns null when the schedule is empty/unset.
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ final class WorkHourDayContext
|
|||||||
* trackingMode:?string,
|
* trackingMode:?string,
|
||||||
* weeklyHours:?int,
|
* weeklyHours:?int,
|
||||||
* contractType:?string,
|
* contractType:?string,
|
||||||
* contractName:?string
|
* contractName:?string,
|
||||||
|
* workDaysHours:?array<int, int>
|
||||||
* }>
|
* }>
|
||||||
*/
|
*/
|
||||||
public array $rows = [];
|
public array $rows = [];
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ final class DayContextRow
|
|||||||
public ?int $weeklyHours = null,
|
public ?int $weeklyHours = null,
|
||||||
public ?string $contractType = null,
|
public ?string $contractType = null,
|
||||||
public ?string $contractName = null,
|
public ?string $contractName = null,
|
||||||
|
/** @var null|array<int, int> iso day (1=Mon..5=Fri) → minutes, planning des jours travaillés (CUSTOM uniquement) */
|
||||||
|
public ?array $workDaysHours = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function setFormation(string $label): void
|
public function setFormation(string $label): void
|
||||||
@@ -87,7 +89,8 @@ final class DayContextRow
|
|||||||
* trackingMode:?string,
|
* trackingMode:?string,
|
||||||
* weeklyHours:?int,
|
* weeklyHours:?int,
|
||||||
* contractType:?string,
|
* contractType:?string,
|
||||||
* contractName:?string
|
* contractName:?string,
|
||||||
|
* workDaysHours:?array<int, int>
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public function toArray(): array
|
public function toArray(): array
|
||||||
@@ -111,6 +114,7 @@ final class DayContextRow
|
|||||||
'weeklyHours' => $this->weeklyHours,
|
'weeklyHours' => $this->weeklyHours,
|
||||||
'contractType' => $this->contractType,
|
'contractType' => $this->contractType,
|
||||||
'contractName' => $this->contractName,
|
'contractName' => $this->contractName,
|
||||||
|
'workDaysHours' => $this->workDaysHours,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -498,6 +498,14 @@ final readonly class RttRecoveryComputationService
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Le salarié ne travaille pas le jour de solidarité (lundi non planifié au contrat,
|
||||||
|
// workDaysHours[lundi] absent → attendu = 0) : le jour ne le concerne pas, aucun
|
||||||
|
// déficit n'est imputé. Sans cette garde, (0 − 0) − prorata facturerait à tort le prorata
|
||||||
|
// à un temps partiel qui ne travaille jamais le lundi (ex. Nadia, Mar+Ven).
|
||||||
|
if (0 === $expectedMinutes) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
$prorata = (int) round($weeklyHours * 12);
|
$prorata = (int) round($weeklyHours * 12);
|
||||||
|
|
||||||
return ($expectedMinutes - $workedMinutes) - $prorata;
|
return ($expectedMinutes - $workedMinutes) - $prorata;
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ final readonly class WorkHourDayContextProvider implements ProviderInterface
|
|||||||
weeklyHours: $contract?->getWeeklyHours(),
|
weeklyHours: $contract?->getWeeklyHours(),
|
||||||
contractType: $contract?->getType()->value,
|
contractType: $contract?->getType()->value,
|
||||||
contractName: $contract?->getName(),
|
contractName: $contract?->getName(),
|
||||||
|
workDaysHours: $workDaysMinutes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -207,6 +207,20 @@ final class RttRecoveryComputationServiceTest extends TestCase
|
|||||||
self::assertSame(-168, $delta);
|
self::assertSame(-168, $delta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CUSTOM 4h NE travaillant PAS le jour de solidarité (lundi non planifié, ex. Nadia Mar+Ven) :
|
||||||
|
* workDaysHours[lundi] absent → expected = 0. Le jour de solidarité ne la concerne pas → delta 0,
|
||||||
|
* aucun déficit imputé. C'est la correction du bug : (0 − 0) − 48 ne doit PAS donner −48.
|
||||||
|
*/
|
||||||
|
public function testSolidarityAdjustmentCustomNotScheduledThatDayIsZero(): void
|
||||||
|
{
|
||||||
|
$service = new ReflectionClass(RttRecoveryComputationService::class)->newInstanceWithoutConstructor();
|
||||||
|
|
||||||
|
$delta = $this->invokePrivate($service, 'computeSolidarityDeficitAdjustment', self::customContract(4), 0, 0);
|
||||||
|
|
||||||
|
self::assertSame(0, $delta);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CUSTOM 28h : prorata = round(28×12) = 336 (5h36). worked 0, expected 336 → delta 0.
|
* CUSTOM 28h : prorata = round(28×12) = 336 (5h36). worked 0, expected 336 → delta 0.
|
||||||
* Le delta est nul ici par coïncidence du fallback uniforme (expected = prorata) ; avec un vrai
|
* Le delta est nul ici par coïncidence du fallback uniforme (expected = prorata) ; avec un vrai
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use App\Service\WorkHours\HolidayVirtualHoursResolver;
|
|||||||
use App\Service\WorkHours\WorkedHoursCreditPolicy;
|
use App\Service\WorkHours\WorkedHoursCreditPolicy;
|
||||||
use App\State\WorkHourDayContextProvider;
|
use App\State\WorkHourDayContextProvider;
|
||||||
use DateTime;
|
use DateTime;
|
||||||
|
use DateTimeImmutable;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use ReflectionObject;
|
use ReflectionObject;
|
||||||
use Symfony\Bundle\SecurityBundle\Security;
|
use Symfony\Bundle\SecurityBundle\Security;
|
||||||
@@ -150,8 +151,7 @@ final class WorkHourDayContextProviderTest extends TestCase
|
|||||||
// Resolver renvoie le contrat 39h avant 2026-03-01, le forfait à partir de cette date.
|
// Resolver renvoie le contrat 39h avant 2026-03-01, le forfait à partir de cette date.
|
||||||
$resolver = $this->createStub(EmployeeContractResolver::class);
|
$resolver = $this->createStub(EmployeeContractResolver::class);
|
||||||
$resolver->method('resolveForEmployeeAndDate')->willReturnCallback(
|
$resolver->method('resolveForEmployeeAndDate')->willReturnCallback(
|
||||||
static fn (Employee $e, \DateTimeImmutable $d): ?Contract =>
|
static fn (Employee $e, DateTimeImmutable $d): ?Contract => $d < new DateTimeImmutable('2026-03-01') ? $timeContract : $forfaitContract
|
||||||
$d < new \DateTimeImmutable('2026-03-01') ? $timeContract : $forfaitContract
|
|
||||||
);
|
);
|
||||||
$resolver->method('resolveNatureForEmployeeAndDate')->willReturn(ContractNature::CDI);
|
$resolver->method('resolveNatureForEmployeeAndDate')->willReturn(ContractNature::CDI);
|
||||||
|
|
||||||
@@ -180,6 +180,67 @@ final class WorkHourDayContextProviderTest extends TestCase
|
|||||||
self::assertSame('Contrat', $row['contractName']);
|
self::assertSame('Contrat', $row['contractName']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testRowCarriesWorkDaysHoursForCustomContract(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$employee = $this->buildEmployee(1, Contract::TRACKING_TIME, 4);
|
||||||
|
|
||||||
|
$resolver = $this->createStub(EmployeeContractResolver::class);
|
||||||
|
$resolver->method('resolveForEmployeeAndDate')->willReturn($employee->getContract());
|
||||||
|
$resolver->method('resolveNatureForEmployeeAndDate')->willReturn(ContractNature::CDI);
|
||||||
|
// Contrat 4h travaillé le lundi et le vendredi (120 min chacun).
|
||||||
|
$resolver->method('resolveWorkDaysMinutesForEmployeeAndDate')->willReturn([1 => 120, 5 => 120]);
|
||||||
|
|
||||||
|
$this->requestStack->push(new Request(query: ['workDate' => '2026-02-16']));
|
||||||
|
$this->security->method('getUser')->willReturn($user);
|
||||||
|
$this->employeeRepository->method('findScoped')->with($user)->willReturn([$employee]);
|
||||||
|
$this->absenceRepository->method('findByDateAndEmployees')->willReturn([]);
|
||||||
|
|
||||||
|
$provider = new WorkHourDayContextProvider(
|
||||||
|
$this->security,
|
||||||
|
$this->requestStack,
|
||||||
|
$this->employeeRepository,
|
||||||
|
$this->absenceRepository,
|
||||||
|
$this->formationRepository,
|
||||||
|
$resolver,
|
||||||
|
new AbsenceSegmentsResolver(),
|
||||||
|
new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
|
||||||
|
$this->buildHolidayResolver(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$row = $provider->provide(new Get())->rows[0];
|
||||||
|
|
||||||
|
self::assertSame([1 => 120, 5 => 120], $row['workDaysHours']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRowHasNullWorkDaysHoursForStandardContract(): void
|
||||||
|
{
|
||||||
|
$user = new User();
|
||||||
|
$employee = $this->buildEmployee(1, Contract::TRACKING_TIME, 35);
|
||||||
|
|
||||||
|
// buildResolverStub ne stube pas resolveWorkDaysMinutesForEmployeeAndDate → null (35h n'a pas de planning).
|
||||||
|
$this->requestStack->push(new Request(query: ['workDate' => '2026-02-16']));
|
||||||
|
$this->security->method('getUser')->willReturn($user);
|
||||||
|
$this->employeeRepository->method('findScoped')->with($user)->willReturn([$employee]);
|
||||||
|
$this->absenceRepository->method('findByDateAndEmployees')->willReturn([]);
|
||||||
|
|
||||||
|
$provider = new WorkHourDayContextProvider(
|
||||||
|
$this->security,
|
||||||
|
$this->requestStack,
|
||||||
|
$this->employeeRepository,
|
||||||
|
$this->absenceRepository,
|
||||||
|
$this->formationRepository,
|
||||||
|
$this->buildResolverStub(),
|
||||||
|
new AbsenceSegmentsResolver(),
|
||||||
|
new WorkedHoursCreditPolicy($this->buildResolverStub(), new DailyReferenceMinutesResolver()),
|
||||||
|
$this->buildHolidayResolver(),
|
||||||
|
);
|
||||||
|
|
||||||
|
$row = $provider->provide(new Get())->rows[0];
|
||||||
|
|
||||||
|
self::assertNull($row['workDaysHours']);
|
||||||
|
}
|
||||||
|
|
||||||
private function buildEmployee(int $id, string $trackingMode, ?int $weeklyHours): Employee
|
private function buildEmployee(int $id, string $trackingMode, ?int $weeklyHours): Employee
|
||||||
{
|
{
|
||||||
$contract = new Contract()
|
$contract = new Contract()
|
||||||
|
|||||||
Reference in New Issue
Block a user