From e310f65d8163d164da474704f2135556ba800c48 Mon Sep 17 00:00:00 2001 From: tristan Date: Wed, 20 May 2026 17:51:48 +0200 Subject: [PATCH] feat(leave) : prorate forfait work-target days in employee header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/composables/useEmployeeDetailPage.ts | 14 +++++++++++--- frontend/services/dto/employee-leave-summary.ts | 1 + src/ApiResource/EmployeeLeaveSummary.php | 8 ++++++++ src/State/EmployeeLeaveSummaryProvider.php | 8 ++++++++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/frontend/composables/useEmployeeDetailPage.ts b/frontend/composables/useEmployeeDetailPage.ts index afd25b5..ffd1aa9 100644 --- a/frontend/composables/useEmployeeDetailPage.ts +++ b/frontend/composables/useEmployeeDetailPage.ts @@ -14,10 +14,16 @@ export const useEmployeeDetailPage = () => { const showLeaveTab = computed(() => employee.value?.currentContractNature !== 'INTERIM') const showRttTab = computed(() => phase.selectedPhase.value?.contractType !== CONTRACT_TYPES.FORFAIT) const isForfait = computed(() => phase.selectedPhase.value?.contractType === CONTRACT_TYPES.FORFAIT) + // Jours à travailler du forfait : prorata exposé par le backend (218 sur année pleine, + // moins sur une entrée en cours d'année). Fallback 218 tant que le récap n'est pas chargé. + const forfaitWorkTargetDays = computed(() => { + const target = leave.leaveSummary.value?.forfaitWorkTargetDays + return (target === null || target === undefined) ? 218 : Math.round(target) + }) const employeeContractWorkLabel = computed(() => { const contract = employee.value?.contract if (!contract) return '-' - if (contract.type === CONTRACT_TYPES.FORFAIT) return 'Forfait - 218 jours' + if (contract.type === CONTRACT_TYPES.FORFAIT) return `Forfait - ${forfaitWorkTargetDays.value} jours` if (contract.weeklyHours !== null && contract.weeklyHours !== undefined) return `${contract.weeklyHours} heures` return contract.name || '-' }) @@ -75,8 +81,10 @@ export const useEmployeeDetailPage = () => { if (!isForfait.value) return '' const presence = leave.leaveSummary.value?.presenceDaysToToday if (presence === undefined || presence === null) return '' - const remaining = 218 - presence - return ` (${remaining} restants)` + const fmt = (n: number) => (Number.isInteger(n) ? String(n) : (Math.round(n * 100) / 100).toFixed(2).replace('.', ',')) + // restant à travailler = jours à travailler (prorata) − jours de présence déjà effectués + const remaining = forfaitWorkTargetDays.value - presence + return ` (${fmt(presence)} présence · ${fmt(remaining)} restants)` }) const rtt = useEmployeeRtt(employee, loadEmployee, phase.selectedPhase) const mileage = useEmployeeMileage(employee, loadEmployee) diff --git a/frontend/services/dto/employee-leave-summary.ts b/frontend/services/dto/employee-leave-summary.ts index b5eb025..5da16b5 100644 --- a/frontend/services/dto/employee-leave-summary.ts +++ b/frontend/services/dto/employee-leave-summary.ts @@ -16,6 +16,7 @@ export type EmployeeLeaveSummary = { previousYearPaidDays: number presenceDaysByMonth: Record presenceDaysToToday: number + forfaitWorkTargetDays: number | null dataStartDate: string | null } diff --git a/src/ApiResource/EmployeeLeaveSummary.php b/src/ApiResource/EmployeeLeaveSummary.php index 14cda47..a3f6338 100644 --- a/src/ApiResource/EmployeeLeaveSummary.php +++ b/src/ApiResource/EmployeeLeaveSummary.php @@ -42,6 +42,14 @@ final class EmployeeLeaveSummary /** Cumul des jours de présence depuis le début de l'année de congé jusqu'à aujourd'hui (forfait). */ public float $presenceDaysToToday = 0.0; + /** + * FORFAIT uniquement : jours à travailler sur l'exercice = jours ouvrés de la période − congés acquis. + * Vaut 218 sur une année pleine (252 − 34) et le prorata sur une entrée en cours d'année + * (ex. Grégory : 168 − 13 ≈ 155). Null pour les non-forfait. Le « restant à travailler » + * affiché = forfaitWorkTargetDays − presenceDaysToToday. + */ + public ?float $forfaitWorkTargetDays = null; + /** Date de mise en service du logiciel (env RTT_START_DATE) — borne minimale pour les sélecteurs d'historique. */ public ?string $dataStartDate = null; } diff --git a/src/State/EmployeeLeaveSummaryProvider.php b/src/State/EmployeeLeaveSummaryProvider.php index 74c17a3..828363a 100644 --- a/src/State/EmployeeLeaveSummaryProvider.php +++ b/src/State/EmployeeLeaveSummaryProvider.php @@ -133,6 +133,14 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface $summary->previousYearPaidDays = $paidLeaveDays; [$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year, $phase); + + // Forfait : jours à travailler sur l'exercice = jours ouvrés de la période − congés acquis. + // Année pleine → 218 (252 − 34) ; entrée en cours d'année → prorata (ex. 168 − 13 ≈ 155). + if (LeaveRuleCode::FORFAIT_218->value === $summary->ruleCode) { + $businessDaysInPeriod = $this->countBusinessDays($periodFrom, $periodTo, $this->buildRawPublicHolidayMap($periodFrom, $periodTo)); + $summary->forfaitWorkTargetDays = $businessDaysInPeriod - $summary->acquiredDays; + } + // Forfait-only: leaves taken from N-1 stock do NOT decrement presence days. // For non-forfait, previousYearTakenDays is always 0, so the budget has no effect. $n1AbsencesBudget = $yearSummary['previousYearTakenDays'];