Review follow-ups: (1) createFromFormat('Y-m-d') keeps the current time, so a raw
DateTime comparison wrongly excluded an employee ending on the from-day (and dropped
first-day absences); normalize from/to to day bounds and compare contract periods on
date only (Y-m-d), mirroring the calendar view. (2) eager-load contractPeriods in
findForPrintBySiteIds to avoid an N+1 during filtering. Added a boundary test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The calendar view hides employees whose contract doesn't intersect the displayed
month, but the absence PDF print still listed them. Apply the same intersection
filter (hasContractInRange over [from, to]) in AbsencePrintProvider, and reject
invalid from/to dates. A employee who left in April no longer appears on a May print.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Non-forfait header now shows '{weeklyHours} heures ({presence} présence)'.
Presence (presenceDaysByMonth/presenceDaysToToday) is bounded to the employee's
contract start so business days before hire are not counted (Dylan CDD: 43.5,
was 246). No change for employees present before the exercise or for forfait
(already capped at phase start). Leave summary now eager-loaded for any employee
with a leave tab to feed the header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The header target subtracted summary.acquiredDays, which includes fractionedDays
(and bonusDays via the full-year acquired), so a full-year forfait with weekend
work or HR fractioned days showed <218. Full year = contractual 218 (capped at
period business days); entry year = businessDays − entry acquired (repos + carried
CP, excluding bonus/fractioned). Extracted computeForfaitWorkTargetDays + 3 tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The header hardcoded '218 jours' and '218 - presence restants', wrong for a
forfait entered mid-year. Expose forfaitWorkTargetDays = businessDays(period) -
acquiredDays (218 full year, prorated otherwise) and show
'Forfait - {target} jours ({presence} présence · {target-presence} restants)'.
Grégory: 155 jours (11 présence · 144 restants).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mid-week hire weeks (e.g. CDD starting Thursday) had their first day with no
contract, so the week was classified CUSTOM and the 25%/50% overtime bonuses
were disabled. Anchor the week's contract type/nature on the first contracted
day instead, so a 35h/39h hire week keeps its overtime tiers.
Dylan CHABOISSON week 12: 7h → 8h45 (7h × 1.25).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
computeWeeklyOvertime25StartMinutes used a ternary that fell back to 35h
when a day had no active contract. Once periodFrom was uncapped (so the
RTT week iterates the full exercise), pre-hire days were silently
contributing 7h each to the weekly threshold, erasing the 25% overtime
for the employee's first partial week.
Dylan CHABOISSON week 12 (Mar 19 hire, worked Thu 9h + Fri 9h30 +
Sat 3h30 = 22h): threshold was 36h (incorrect), now back to 15h (Thu 8h
+ Fri 7h) so the 7h above threshold + 1h45 bonus are correctly credited.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The RTT week table was hiding all weeks before the phase's startDate,
so for an employee hired mid-exercise (e.g. Dylan CHABOISSON, CDD
starting 2026-03-19) March displayed only from week 12 instead of
week 9, with no visible padding for the pre-hire weeks of the exercise.
Mirrors the same fix applied to the leave provider: the exercise unit
for RTT is annual (Juin→Mai). Capping periodFrom artificially clipped
the displayed weeks. Days without a contract naturally contribute 0
minutes (no reference, no worked hours), so the cumul is correct
without the cap.
periodTo and limitDate caps at phase.endDate are preserved for closed
phases so the table doesn't extend past the phase end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the inline <label> with the MalioSelect's native `label` prop
to match the visual style of other selects on the employee detail page.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
EmployeeContractPhaseResolver now accepts the data-start date and filters
out phases whose endDate is strictly before it. No work-hour or absence
data exists before the application launch date, so legacy contract
periods that ended before that date would surface meaningless RTT/CP
figures in the phase picker.
For employees who joined long before the software (typical legacy 35h
contracts, in production since 2014), only the current phase remains
visible — which also collapses the picker (threshold ≥ 2 phases).
The Employee entity reads RTT_START_DATE from $_SERVER/$_ENV directly
since it has no DI. The resolver service is wired via services.yaml.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The annual calendar in LeaveTab.vue was showing absences from outside
the selected phase's lifespan. For an employee who switched contract
type mid-year, this leaked the old phase's absences into the new
phase's calendar view (and vice versa via the phase picker).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pull the "date -> leave/RTT exercise year" formula out of
EmployeeRttPaymentProcessor, EmployeeRttSummaryProvider and
EmployeeLeaveSummaryProvider into a single
App\Service\Exercise\ExerciseYearResolver. Forfait flag is parameterised
so the leave (calendar year) and RTT (Juin N-1 -> Mai N) variants share
the same implementation. Pure refactor, no behavioural change.
Mirror Task 3 (leave provider) on the RTT side: accept an optional `?phaseId`
query parameter and cap the exercise window to the phase boundaries when set.
- Inject EmployeeContractPhaseResolver.
- New helpers: resolveTargetPhase, clampYearToPhase, exerciseYearForDate.
- resolveYear now takes the phase: default year falls back to the phase end
date when phaseId is provided; explicit year is silently clamped to the
phase range.
- provide() narrows periodFrom / periodTo / limitDate to the phase end date
for past phases.
- Default behavior (no phaseId) unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Extract private helper `exerciseYearForDate(date, isForfait)` to dedupe
the date->leave-exercise-year expression duplicated across `clampYearToPhase`
and `resolveFirstComputationYear` (4 copies collapsed into 1 helper + 4
call sites).
- Remove the unused `ContractPhase $phase` parameter from
`resolveLeavePeriodBounds`: the body never reads it (the phase cap is
applied later by `resolvePeriodBounds`).
- Add `ContractNature $contractNature` to `ContractPhase` DTO, populated
from the first period of the group by `EmployeeContractPhaseResolver`.
Drop the `resolveNatureForPhase` lookup in `EmployeeLeaveSummaryProvider`
in favor of `$phase->contractNature`. Expose `contractNature` in
`Employee::getContractPhases()` array shape for frontend use.
- Fix regression for terminated employees calling `computeYearSummary`
without an explicit phase (LeaveRecapRowBuilder,
DumpVerificationSnapshotCommand). Before the refactor the period bounds,
accrual end and taken end were NOT capped at the contract end for
terminated employees, because `Employee::getCurrentContractEndDate()`
returns null when no period covers "today". The new fallback phase
(`isCurrent=false`, real `endDate`) was silently capping `to`. Add an
internal `applyPhaseEndCap` flag, true when phase is explicit, false
for legacy callers, threaded through `resolvePeriodBounds`,
`resolveAccrualCalculationEndDate` and `resolveTakenCalculationEndDate`.
- Add regression test
`testTerminatedEmployeeWithoutExplicitPhaseSkipsPhaseEndCap` proving
that legacy callers keep the natural exercise upper bound while explicit
phase callers get the cap.
- Add `contractNature` assertion in `EmployeeContractPhaseResolverTest`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The provider now resolves a target ContractPhase from a ?phaseId query
parameter and propagates it through resolveYear, resolvePeriodBounds,
resolveLeavePolicy, resolveAccrualCalculationEndDate,
resolveTakenCalculationEndDate, and resolveFirstComputationYear.
- phaseId missing → current phase (legacy behavior preserved)
- phaseId valid → past/current phase used for rule code, weekly hours
and period bounds (capped at phase end)
- phaseId invalid (non-numeric or unknown) → 422
- year missing + phaseId → year derived from phase end date
- year out of phase range + phaseId → silent clamp to phase boundaries
Public methods computeYearSummary/resolvePaidLeaveDays/resolveLeaveYearForToday
remain backward-compatible for external callers (LeaveRecapRowBuilder,
DumpVerificationSnapshotCommand).