diff --git a/CLAUDE.md b/CLAUDE.md index 8daf08f..cd33261 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -61,6 +61,7 @@ - INTERIM: no overtime bonuses, no recovery time - Driver contracts: RTT uses `dayHoursMinutes + nightHoursMinutes + workshopHoursMinutes` instead of morning/afternoon/evening time ranges - FORFAIT weekend/holiday bonus: each weekend or public holiday day worked gives bonus leave (full day if morning+afternoon, 0.5 if only one). Added to acquired days, no cap. PRESENCE mode only. +- **FORFAIT — jours de présence et N-1** : les congés posés et imputés sur le stock N-1 ne décrémentent **pas** les jours de présence affichés (`presenceDaysByMonth` et `presenceDaysToToday`). Implémenté dans `EmployeeLeaveSummaryProvider::computePresenceDaysByMonth` via un budget N-1 (= `previousYearTakenDays`) consommé chronologiquement avant comptage des absences. Pour les non-forfait, ce budget vaut toujours 0 → comportement inchangé. ## Récap. congés (écran) - Accès via sidebar `Récap. congés`, conditionné au flag `User.hasLeaveRecapAccess` (défaut `false`) — activé au create/edit user. Le flag s'applique à tous les profils, y compris admin. diff --git a/doc/functional-rules.md b/doc/functional-rules.md index 49db4b4..3be0d1d 100644 --- a/doc/functional-rules.md +++ b/doc/functional-rules.md @@ -335,7 +335,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer. | Contrat | Contract.name | | CP N-1 restant | CDI/CDD: acquis N-1 − pris sur N-1. Forfait: report N-1 restant | | Samedi restant | CDI/CDD: samedis acquis N-1 − pris. Forfait: `-` | -| CP N | Forfait: jours acquis année civile. Non-forfait: en cours d'acquisition | +| CP N | Forfait: restant sur quota année civile (acquis − pris depuis N, sans toucher au stock N-1). Non-forfait: en cours d'acquisition | | RTT | Minutes disponibles (report N-1 + acquis N - payés). Format `X h Y m`. Forfait et INTERIM: `-` | ## 10bis) Écran Récap. congés (tableau) diff --git a/frontend/composables/useEmployeeDetailPage.ts b/frontend/composables/useEmployeeDetailPage.ts index 04e4309..bfdadce 100644 --- a/frontend/composables/useEmployeeDetailPage.ts +++ b/frontend/composables/useEmployeeDetailPage.ts @@ -10,10 +10,11 @@ export const useEmployeeDetailPage = () => { const showLeaveTab = computed(() => employee.value?.currentContractNature !== 'INTERIM') const showRttTab = computed(() => employee.value?.contract?.type !== CONTRACT_TYPES.FORFAIT) + const isForfait = computed(() => employee.value?.contract?.type === CONTRACT_TYPES.FORFAIT) const employeeContractWorkLabel = computed(() => { const contract = employee.value?.contract if (!contract) return '-' - if (contract.type === CONTRACT_TYPES.FORFAIT) return 'Forfait' + if (contract.type === CONTRACT_TYPES.FORFAIT) return 'Forfait - 218 jours' if (contract.weeklyHours !== null && contract.weeklyHours !== undefined) return `${contract.weeklyHours} heures` return contract.name || '-' }) @@ -55,6 +56,9 @@ export const useEmployeeDetailPage = () => { await bonus.loadBonusData() } else if (activeTab.value === 'observation') { await observation.loadObservationData() + } else if (isForfait.value && showLeaveTab.value) { + // Eager load: needed for the "X jours restants" header label on forfait employees. + await leave.loadLeaveData() } } finally { isLoading.value = false @@ -63,6 +67,13 @@ export const useEmployeeDetailPage = () => { const contract = useEmployeeContract(employee, loadEmployee) const leave = useEmployeeLeave(employee, loadEmployee) + const forfaitRemainingDaysLabel = computed(() => { + if (!isForfait.value) return '' + const presence = leave.leaveSummary.value?.presenceDaysToToday + if (presence === undefined || presence === null) return '' + const remaining = 218 - presence + return ` (${remaining} restants)` + }) const rtt = useEmployeeRtt(employee, loadEmployee) const mileage = useEmployeeMileage(employee, loadEmployee) const formation = useEmployeeFormation(employee, loadEmployee) @@ -97,6 +108,7 @@ export const useEmployeeDetailPage = () => { showLeaveTab, showRttTab, employeeContractWorkLabel, + forfaitRemainingDaysLabel, ...contract, ...leave, ...rtt, diff --git a/frontend/pages/employees/[id].vue b/frontend/pages/employees/[id].vue index 898f1a4..138cb2c 100644 --- a/frontend/pages/employees/[id].vue +++ b/frontend/pages/employees/[id].vue @@ -26,7 +26,7 @@

Date d'entrée : {{ employee.entryDate ? employee.entryDate.split('-').reverse().join('/') : '-' }}

-

{{ contractNatureLabel(employee.currentContractNature) }} {{ employeeContractWorkLabel }}

+

{{ contractNatureLabel(employee.currentContractNature) }} {{ employeeContractWorkLabel }}{{ forfaitRemainingDaysLabel }}

{{ employee.site?.name ?? '-' }}

@@ -257,6 +257,7 @@ const { showRttTab, contractHistory, employeeContractWorkLabel, + forfaitRemainingDaysLabel, contractForm, createContractForm, isContractDrawerOpen, diff --git a/frontend/services/dto/employee-leave-summary.ts b/frontend/services/dto/employee-leave-summary.ts index 20a85e0..7150778 100644 --- a/frontend/services/dto/employee-leave-summary.ts +++ b/frontend/services/dto/employee-leave-summary.ts @@ -15,5 +15,6 @@ export type EmployeeLeaveSummary = { previousYearRemainingDays: number previousYearPaidDays: number presenceDaysByMonth: Record + presenceDaysToToday: number } diff --git a/src/ApiResource/EmployeeLeaveSummary.php b/src/ApiResource/EmployeeLeaveSummary.php index 0b275bd..de27fac 100644 --- a/src/ApiResource/EmployeeLeaveSummary.php +++ b/src/ApiResource/EmployeeLeaveSummary.php @@ -38,4 +38,7 @@ final class EmployeeLeaveSummary /** @var array YYYY-MM => count (0.5 for half-days) */ public array $presenceDaysByMonth = []; + + /** 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; } diff --git a/src/Service/Leave/LeaveRecapRowBuilder.php b/src/Service/Leave/LeaveRecapRowBuilder.php index 633a357..46a0d4e 100644 --- a/src/Service/Leave/LeaveRecapRowBuilder.php +++ b/src/Service/Leave/LeaveRecapRowBuilder.php @@ -73,7 +73,7 @@ final readonly class LeaveRecapRowBuilder } } $cpN1Remaining = round($yearSummary['previousYearRemainingDays'], 2); - $cpN = (string) round($yearSummary['acquiredDays'], 2); + $cpN = (string) round($yearSummary['remainingDays'], 2); $acquiredSaturdays = '-'; } else { $cpN1Remaining = round($yearSummary['remainingDays'], 2); diff --git a/src/State/EmployeeLeaveSummaryProvider.php b/src/State/EmployeeLeaveSummaryProvider.php index e1b7c28..24ed690 100644 --- a/src/State/EmployeeLeaveSummaryProvider.php +++ b/src/State/EmployeeLeaveSummaryProvider.php @@ -119,8 +119,29 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface $summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays']; $summary->previousYearPaidDays = $paidLeaveDays; - [$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year); - $summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo); + [$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year); + // 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']; + $summary->presenceDaysByMonth = $this->computePresenceDaysByMonth( + $employee, + $periodFrom, + $periodTo, + $n1AbsencesBudget + ); + + // Same logic as presenceDaysByMonth but bounded at today: number of presence days + // accumulated from leave year start up to today (inclusive). + $today = new DateTimeImmutable('today'); + $cappedTo = $today < $periodTo ? $today : $periodTo; + $summary->presenceDaysToToday = $today < $periodFrom + ? 0.0 + : array_sum($this->computePresenceDaysByMonth( + $employee, + $periodFrom, + $cappedTo, + $n1AbsencesBudget + )); return $summary; } @@ -686,8 +707,12 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface * * @return array YYYY-MM => presence day count */ - private function computePresenceDaysByMonth(Employee $employee, DateTimeImmutable $from, DateTimeImmutable $to): array - { + private function computePresenceDaysByMonth( + Employee $employee, + DateTimeImmutable $from, + DateTimeImmutable $to, + float $n1AbsencesBudget = 0.0 + ): array { $publicHolidays = $this->buildPublicHolidayMap($from, $to); $weekendWorkedDays = $this->workHourRepository->countWeekendWorkedDaysByMonth($employee, $from, $to); $absences = $this->absenceRepository->findByEmployeeAndOverlappingDateRange($employee, $from, $to); @@ -697,10 +722,20 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface ? $this->workHourRepository->findWorkedDatesAmong($employee, array_keys($publicHolidays)) : []; + // Sort absences chronologically so N-1 budget (forfait only) is consumed in date order: + // earliest absences attribute to N-1 first, later ones overflow to N and reduce presence. + $sortedAbsences = $absences; + usort( + $sortedAbsences, + static fn ($a, $b): int => $a->getStartDate() <=> $b->getStartDate() + ); + + $remainingN1Budget = $n1AbsencesBudget; + // Count absence days per month, iterating day by day to handle multi-day absences // and properly distribute across months. $absenceDaysByMonth = []; - foreach ($absences as $absence) { + foreach ($sortedAbsences as $absence) { $start = DateTimeImmutable::createFromInterface($absence->getStartDate())->setTime(0, 0); $end = DateTimeImmutable::createFromInterface($absence->getEndDate())->setTime(0, 0); @@ -718,6 +753,17 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface continue; } + // Forfait: leaves taken from N-1 stock do NOT decrement presence days. + // We chronologically consume the N-1 budget before counting any absence. + if ($remainingN1Budget > 0.0) { + $consumed = min($remainingN1Budget, $dayAmount); + $remainingN1Budget -= $consumed; + $dayAmount -= $consumed; + if ($dayAmount <= 0.0) { + continue; + } + } + $absenceDaysByMonth[$monthKey] = ($absenceDaysByMonth[$monthKey] ?? 0.0) + $dayAmount; } }