The CP exercise (Juin N-1 → Mai N) is annual and continuous across contract-signature changes within the same leave rule (e.g. 35h → 39h, isDriver flip, weeklyHours bump). Capping `from` at the phase start truncated the accrual to just the months under the latest phase, producing wrong "en cours d'acquisition" values and dropping presence days from earlier months on the leave-tab calendar. For Damien GUILLOT (35h until 2025-10-31, then 39h), this gave 15 days acquired (6 months Nov→Apr) instead of the expected 27.5 days (11 months Jun→Apr at 2.5/month). After this fix, the H39 view shows the full annual accrual as expected. FORFAIT phases keep the from cap: the 218-day target is calendar-year scoped and only counts the FORFAIT portion of the year. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.3 KiB
Vue contrat — sélecteur de phase
Objectif
Permettre à la RH de consulter les onglets Congés et RTT d'un employé selon une phase de contrat passée (ex. un 39h CDI avant un switch FORFAIT, ou un CDD clôturé avec solde de tout compte) sans changer le comportement par défaut sur la phase courante.
Concept de "phase"
Une phase = groupe d'EmployeeContractPeriod consécutifs (triés par startDate) partageant la signature (contract.type, weeklyHours, isDriver). Le service EmployeeContractPhaseResolver (src/Service/Contracts/EmployeeContractPhaseResolver.php) calcule ces phases à la volée depuis Employee::getContractPeriods().
Une transition de signature (35h → 39h, 39h → FORFAIT, driver false→true, weeklyHours 28→30, etc.) ouvre une nouvelle phase. Un type différent entre deux périodes de même signature empêche leur fusion (39h → INTERIM → 39h = 3 phases).
Filtrage par RTT_START_DATE
Les phases dont la date de fin est strictement antérieure à RTT_START_DATE (env, date de mise en service du logiciel) sont masquées du picker. Aucune donnée logiciel (heures, absences) n'existe avant cette date, donc consulter une phase entièrement antérieure n'a pas de sens fonctionnel.
Exemple : un employé sous 35h CDI de 2014 à 2025-10-31 puis 39h CDI depuis 2025-11-01, avec RTT_START_DATE=2026-02-23 :
- Phase 35h (2014 → 2025-10-31) : entièrement avant → masquée
- Phase 39h (depuis 2025-11-01) : chevauche → visible
- Résultat : 1 seule phase visible → picker caché (seuil ≥ 2 phases)
Une phase dont la date de fin est égale à RTT_START_DATE est conservée (inégalité >=, non stricte).
EmployeeContractPhaseResolver reçoit RTT_START_DATE via DI (services.yaml). L'entité Employee::getContractPhases() lit la valeur depuis $_SERVER/$_ENV directement (l'entité n'a pas d'injection) et passe la chaîne au constructeur du resolver.
Picker UI
- Position : en haut de la fiche employé, sous le nom et au-dessus des onglets.
- Libellé :
Vue contrat. - Caché si l'employé n'a qu'une seule phase.
- Sélection non persistée — chaque ouverture de fiche démarre sur la phase courante.
Bandeau d'information
Affiché quand le picker est sur une phase passée. Indique que le mode lecture est partiel — les paiements de solde restent possibles, l'édition d'absences et des stocks de report est désactivée.
Effet sur les onglets
| Onglet | Effet |
|---|---|
| Congés | Recharge avec les règles de la phase. Période Juin→Mai pour non-forfait, Jan→Déc pour FORFAIT. Exercices bornés à la phase. |
| RTT | Visible ssi phase.contractType !== FORFAIT. Exercices bornés à la phase. |
| Heures, Frais, Formation, Contrat, Calendrier | Non impactés. |
Paiements de solde sur phase passée
- RTT :
+ Payer les RTTactivé sur le dernier exercice de la phase uniquement (l'exercice contenantphase.endDate). Les exercices antérieurs restent en lecture seule. - CP : utiliser le mécanisme existant
EmployeeContractPeriod.paidLeaveSettledClosureDatepour solder. Pas de nouveau channel.
Transition d'exercice
Quand un exercice chevauche deux phases, les bornes sont capées différemment selon le type de phase consultée :
Phase FORFAIT (passée ou courante)
Le cumul 218 jours est par année civile. Toute consultation FORFAIT cape :
fromàmax(phase.startDate, 1er janvier de l'année)toàmin(phase.endDate, 31 décembre de l'année)
Ex. switch 39h → FORFAIT au 01/05/2026, vue FORFAIT année 2026 → période = [01/05/2026, 31/12/2026].
Phase non-forfait (35h / 39h / CUSTOM / INTERIM)
L'exercice CP est annuel (Juin N-1 → Mai N) et continu à travers les changements d'heures contractuelles dans le même régime non-forfait. La cap n'applique pas sur from :
fromreste à 1er juin de l'année (le contrat-entry-date est géré parresolveEffectivePeriodStartpour les nouveaux embauchés)toest borné àphase.endDateuniquement quand on consulte une phase passée
Ex. employé 35h jusqu'au 31/10/2025 puis 39h depuis le 01/11/2025 :
- Vue 39h (courante) sur exercice 2026 → période = [01/06/2025, 31/05/2026]. Acquis CP = exercice complet (~27.5 jours à fin avril).
- Vue 35h (passée) sur exercice 2026 → période = [01/06/2025, 31/10/2025]. Acquis CP = 5 mois de l'exercice.
Important : c'est intentionnel que la vue courante 39h inclue les mois Juin-Octobre travaillés en 35h dans son cumul. Le stock CP est annuel, pas par phase ; un changement d'heures ne reset pas le compteur.
API
Les endpoints suivants acceptent ?phaseId=N :
GET /employees/{id}/leave-summaryGET /employees/{id}/rtt-summary
Quand absent, ils utilisent la phase courante (comportement inchangé).
Employee.contractPhases (lecture, groupe employee:read) liste les phases au format {id, contractType, weeklyHours, isDriver, contractNature, startDate, endDate, periodIds, isCurrent}.
Tests
tests/Service/Contracts/EmployeeContractPhaseResolverTest.php(unit)tests/State/EmployeeLeaveSummaryProviderTest.php(functional, phaseId)tests/State/EmployeeRttSummaryProviderTest.php(functional, phaseId)tests/State/EmployeeRttPaymentProcessorTest.php(functional, dernier exo phase clôturée)tests/Service/Exercise/ExerciseYearResolverTest.php(helper extrait pendant Task 5)