From 0ad2f0c624b56b62e00509b349ab43e20cad68d1 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 21 May 2026 08:14:08 +0200 Subject: [PATCH] feat(leave) : show presence days in header for non-forfait, bound to contract start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/composables/useEmployeeDetailPage.ts | 19 ++++++++++++++----- frontend/pages/employees/[id].vue | 3 ++- src/State/EmployeeLeaveSummaryProvider.php | 14 ++++++++++---- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/frontend/composables/useEmployeeDetailPage.ts b/frontend/composables/useEmployeeDetailPage.ts index ffd1aa9..eeaae0b 100644 --- a/frontend/composables/useEmployeeDetailPage.ts +++ b/frontend/composables/useEmployeeDetailPage.ts @@ -66,8 +66,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. + } else if (showLeaveTab.value) { + // Eager load: the header shows présence (et jours à travailler/restant pour le forfait), + // qui proviennent du récap congés — nécessaire même quand on ouvre un autre onglet. await leave.loadLeaveData() } } finally { @@ -77,14 +78,21 @@ export const useEmployeeDetailPage = () => { const contract = useEmployeeContract(employee, loadEmployee) const leave = useEmployeeLeave(employee, loadEmployee, phase.selectedPhase) + const formatDays = (n: number) => (Number.isInteger(n) ? String(n) : (Math.round(n * 100) / 100).toFixed(2).replace('.', ',')) + // Forfait : « (présence · restant à travailler) ». restant = jours à travailler (prorata) − présence. const forfaitRemainingDaysLabel = computed(() => { if (!isForfait.value) return '' const presence = leave.leaveSummary.value?.presenceDaysToToday if (presence === undefined || presence === null) return '' - 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)` + return ` (${formatDays(presence)} présence · ${formatDays(remaining)} restants)` + }) + // Non-forfait : « (présence) » seul (pas de cible de jours à travailler). + const nonForfaitPresenceLabel = computed(() => { + if (isForfait.value) return '' + const presence = leave.leaveSummary.value?.presenceDaysToToday + if (presence === undefined || presence === null) return '' + return ` (${formatDays(presence)} présence)` }) const rtt = useEmployeeRtt(employee, loadEmployee, phase.selectedPhase) const mileage = useEmployeeMileage(employee, loadEmployee) @@ -135,6 +143,7 @@ export const useEmployeeDetailPage = () => { showRttTab, employeeContractWorkLabel, forfaitRemainingDaysLabel, + nonForfaitPresenceLabel, ...phase, ...contract, ...leave, diff --git a/frontend/pages/employees/[id].vue b/frontend/pages/employees/[id].vue index 30a31e1..6d6b993 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 }}{{ forfaitRemainingDaysLabel }}

+

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

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

@@ -299,6 +299,7 @@ const { contractHistory, employeeContractWorkLabel, forfaitRemainingDaysLabel, + nonForfaitPresenceLabel, contractForm, createContractForm, isContractDrawerOpen, diff --git a/src/State/EmployeeLeaveSummaryProvider.php b/src/State/EmployeeLeaveSummaryProvider.php index bed16d3..2d81e85 100644 --- a/src/State/EmployeeLeaveSummaryProvider.php +++ b/src/State/EmployeeLeaveSummaryProvider.php @@ -148,25 +148,31 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface ); } + // La présence est bornée au début de contrat de l'employé : on ne compte pas comme + // « présents » les jours ouvrés antérieurs à l'embauche (cas d'une entrée en cours + // d'exercice, ex. CDD). Sans effet pour un employé présent depuis avant l'exercice, + // ni pour le forfait (déjà capé au début de phase). + $presenceFrom = $this->resolveEarliestContractStartWithinRange($employee, $periodFrom, $periodTo) ?? $periodFrom; + // 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, + $presenceFrom, $periodTo, $n1AbsencesBudget ); // Same logic as presenceDaysByMonth but bounded at today: number of presence days - // accumulated from leave year start up to today (inclusive). + // accumulated from contract start up to today (inclusive). $today = new DateTimeImmutable('today'); $cappedTo = $today < $periodTo ? $today : $periodTo; - $summary->presenceDaysToToday = $today < $periodFrom + $summary->presenceDaysToToday = $today < $presenceFrom ? 0.0 : array_sum($this->computePresenceDaysByMonth( $employee, - $periodFrom, + $presenceFrom, $cappedTo, $n1AbsencesBudget ));