From 380c72c2429dcc5a5d1921d58718c2efa9fdcd4c Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 2 Mar 2026 10:33:42 +0100 Subject: [PATCH 1/4] =?UTF-8?q?fix=20:=20r=C3=A8gle=20de=20calcule=20des?= =?UTF-8?q?=20heures=20travaill=C3=A9es=20sur=20les=20contrats=20Forfait?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/composables/useHoursPage.ts | 7 +++++-- src/Service/WorkHours/WorkedHoursCreditPolicy.php | 14 +++++++------- src/State/WorkHourWeeklySummaryProvider.php | 12 +++++++++--- tests/State/WorkHourWeeklySummaryProviderTest.php | 2 +- 4 files changed, 22 insertions(+), 13 deletions(-) diff --git a/frontend/composables/useHoursPage.ts b/frontend/composables/useHoursPage.ts index b8379ad..36ffa21 100644 --- a/frontend/composables/useHoursPage.ts +++ b/frontend/composables/useHoursPage.ts @@ -427,8 +427,11 @@ export const useHoursPage = () => { const getPresenceDayValue = (employeeId: number) => { const row = rows.value[employeeId] - const basePresence = (row?.isPresentMorning ? 0.5 : 0) + (row?.isPresentAfternoon ? 0.5 : 0) - const creditedPresence = dayContextByEmployeeId.value.get(employeeId)?.creditedPresenceUnits ?? 0 + const dayRow = dayContextByEmployeeId.value.get(employeeId) + const absentMorning = dayRow?.absentMorning ?? false + const absentAfternoon = dayRow?.absentAfternoon ?? false + const basePresence = ((row?.isPresentMorning && !absentMorning) ? 0.5 : 0) + ((row?.isPresentAfternoon && !absentAfternoon) ? 0.5 : 0) + const creditedPresence = dayRow?.creditedPresenceUnits ?? 0 const total = Math.min(1, basePresence + creditedPresence) return Number.isInteger(total) ? String(total) : total.toFixed(1) } diff --git a/src/Service/WorkHours/WorkedHoursCreditPolicy.php b/src/Service/WorkHours/WorkedHoursCreditPolicy.php index 9454e86..7a4f72f 100644 --- a/src/Service/WorkHours/WorkedHoursCreditPolicy.php +++ b/src/Service/WorkHours/WorkedHoursCreditPolicy.php @@ -60,11 +60,6 @@ final readonly class WorkedHoursCreditPolicy bool $absentMorning, bool $absentAfternoon ): float { - $type = $absence->getType(); - if (!$type?->getCountAsWorkedHours()) { - return 0.0; - } - $employee = $absence->getEmployee(); if (null === $employee) { return 0.0; @@ -74,9 +69,14 @@ final readonly class WorkedHoursCreditPolicy return 0.0; } - $halfUnits = ($absentMorning ? 1 : 0) + ($absentAfternoon ? 1 : 0); + // Règle forfait: + // - demi-journée d'absence => 0.5 travaillé + // - journée complète d'absence => 0 travaillé + if ($absentMorning xor $absentAfternoon) { + return 0.5; + } - return $halfUnits * 0.5; + return 0.0; } public function resolveContractDayMinutes(?int $weeklyHours, int $isoWeekDay): int diff --git a/src/State/WorkHourWeeklySummaryProvider.php b/src/State/WorkHourWeeklySummaryProvider.php index b11922b..526e2fe 100644 --- a/src/State/WorkHourWeeklySummaryProvider.php +++ b/src/State/WorkHourWeeklySummaryProvider.php @@ -135,6 +135,8 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface $creditedByEmployeeDate = []; $creditedPresenceByEmployeeDate = []; $absenceByEmployeeDate = []; + $absentMorningByEmployeeDate = []; + $absentAfternoonByEmployeeDate = []; $absenceLabelByEmployeeDate = []; $absenceColorByEmployeeDate = []; foreach ($absences as $absence) { @@ -153,7 +155,9 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface [$absentMorning, $absentAfternoon] = $this->absenceSegmentsResolver->resolveForDate($absence, $date); if ($absentMorning || $absentAfternoon) { - $absenceByEmployeeDate[$employeeId][$date] = true; + $absenceByEmployeeDate[$employeeId][$date] = true; + $absentMorningByEmployeeDate[$employeeId][$date] = ($absentMorningByEmployeeDate[$employeeId][$date] ?? false) || $absentMorning; + $absentAfternoonByEmployeeDate[$employeeId][$date] = ($absentAfternoonByEmployeeDate[$employeeId][$date] ?? false) || $absentAfternoon; if (!isset($absenceLabelByEmployeeDate[$employeeId][$date])) { $absenceLabelByEmployeeDate[$employeeId][$date] = $absence->getType()?->getLabel(); } @@ -202,8 +206,10 @@ final readonly class WorkHourWeeklySummaryProvider implements ProviderInterface $metrics->addCreditedMinutes($creditedMinutes); $present = null; if ($isPresenceTracking) { - $morning = ($entry['isPresentMorning'] ?? false) ? 0.5 : 0.0; - $afternoon = ($entry['isPresentAfternoon'] ?? false) ? 0.5 : 0.0; + $absentMorning = $absentMorningByEmployeeDate[$employeeId][$date] ?? false; + $absentAfternoon = $absentAfternoonByEmployeeDate[$employeeId][$date] ?? false; + $morning = (($entry['isPresentMorning'] ?? false) && !$absentMorning) ? 0.5 : 0.0; + $afternoon = (($entry['isPresentAfternoon'] ?? false) && !$absentAfternoon) ? 0.5 : 0.0; $creditedPresence = $creditedPresenceByEmployeeDate[$employeeId][$date] ?? 0.0; $present = min(1.0, $morning + $afternoon + $creditedPresence); } diff --git a/tests/State/WorkHourWeeklySummaryProviderTest.php b/tests/State/WorkHourWeeklySummaryProviderTest.php index 24ac5ab..a3a441d 100644 --- a/tests/State/WorkHourWeeklySummaryProviderTest.php +++ b/tests/State/WorkHourWeeklySummaryProviderTest.php @@ -135,7 +135,7 @@ final class WorkHourWeeklySummaryProviderTest extends TestCase self::assertSame(210, $result->rows[0]->weeklyOvertime50Minutes); self::assertSame(1230, $result->rows[0]->weeklyRecoveryMinutes); - self::assertSame(1.0, $result->rows[1]->weeklyPresenceCount); + self::assertSame(0.0, $result->rows[1]->weeklyPresenceCount); self::assertTrue($result->rows[1]->daily[0]->hasAbsence); self::assertSame('Congé', $result->rows[1]->daily[0]->absenceLabel); self::assertSame('#000', $result->rows[1]->daily[0]->absenceColor); From b5e7395760e9930b22dc3330378bae552580bc72 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Mon, 2 Mar 2026 09:34:21 +0000 Subject: [PATCH 2/4] chore: bump version to v0.1.16 --- config/version.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/version.yaml b/config/version.yaml index 58ebf50..998931d 100644 --- a/config/version.yaml +++ b/config/version.yaml @@ -1,2 +1,2 @@ parameters: - app.version: '0.1.15' + app.version: '0.1.16' From 6395ffbe1c35fdf1d734297c1baa9d9de3dc4208 Mon Sep 17 00:00:00 2001 From: tristan Date: Mon, 2 Mar 2026 10:50:02 +0100 Subject: [PATCH 3/4] =?UTF-8?q?feat=20:=20modification=20des=20s=C3=A9lect?= =?UTF-8?q?eurs=20de=20date=20sur=20le=20calendrier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/PeriodStepperPicker.vue | 77 +++++++++++++++++++++ frontend/components/hours/HoursToolbar.vue | 62 ++++------------- frontend/pages/calendar.vue | 48 ++++++++----- 3 files changed, 120 insertions(+), 67 deletions(-) create mode 100644 frontend/components/PeriodStepperPicker.vue diff --git a/frontend/components/PeriodStepperPicker.vue b/frontend/components/PeriodStepperPicker.vue new file mode 100644 index 0000000..c88b66d --- /dev/null +++ b/frontend/components/PeriodStepperPicker.vue @@ -0,0 +1,77 @@ + + + diff --git a/frontend/components/hours/HoursToolbar.vue b/frontend/components/hours/HoursToolbar.vue index 991ccfa..f833eb6 100644 --- a/frontend/components/hours/HoursToolbar.vue +++ b/frontend/components/hours/HoursToolbar.vue @@ -64,41 +64,17 @@ -
- - - - -
+
@@ -145,6 +121,7 @@ import type { Site } from '~/services/dto/site' import type { AbsenceType } from '~/services/dto/absence-type' import EmployeeNameFilterInput from '~/components/EmployeeNameFilterInput.vue' import SiteFilterSelector from '~/components/SiteFilterSelector.vue' +import PeriodStepperPicker from '~/components/PeriodStepperPicker.vue' import { weekInputValueToYmd, ymdToWeekInputValue } from '~/utils/date' const selectedDate = defineModel('selectedDate', { required: true }) @@ -172,7 +149,6 @@ const emit = defineEmits<{ (e: 'shift-date', value: number): void }>() -const nativeDateInput = ref(null) const pickerValue = computed(() => { if (viewMode.value === 'week') return ymdToWeekInputValue(selectedDate.value) return selectedDate.value @@ -186,19 +162,7 @@ const viewModeButtonClass = (mode: 'day' | 'week') => { return 'bg-white text-primary-500 hover:bg-tertiary-500' } -const openDatePicker = () => { - const input = nativeDateInput.value - if (!input) return - if (typeof input.showPicker === 'function') { - input.showPicker() - return - } - input.focus() - input.click() -} - -const onPickerInput = (event: Event) => { - const value = (event.target as HTMLInputElement).value +const onPickerValue = (value: string) => { if (!value) return if (viewMode.value === 'week') { diff --git a/frontend/pages/calendar.vue b/frontend/pages/calendar.vue index 88c183a..77d17f6 100644 --- a/frontend/pages/calendar.vue +++ b/frontend/pages/calendar.vue @@ -30,22 +30,17 @@
- - +
@@ -111,6 +106,7 @@ import CalendarGrid from '~/components/CalendarGrid.vue' import AbsenceFormDrawer from '~/components/AbsenceFormDrawer.vue' import AbsencePrintDrawer from '~/components/AbsencePrintDrawer.vue' import EmployeeNameFilterInput from '~/components/EmployeeNameFilterInput.vue' +import PeriodStepperPicker from '~/components/PeriodStepperPicker.vue' import SiteFilterSelector from '~/components/SiteFilterSelector.vue' useHead({ @@ -195,8 +191,8 @@ const months = [ {value: 11, label: 'Décembre'} ] -const years = Array.from({length: 5}, (unusedValue, index) => now.getFullYear() - 2 + index) - +const selectedMonthLabel = computed(() => `${months[selectedMonth.value]?.label ?? ''}`) +const monthPickerValue = computed(() => `${selectedYear.value}-${String(selectedMonth.value + 1).padStart(2, '0')}`) // Infos de calendrier calculées. const daysInMonth = computed(() => getDaysInMonth(selectedYear.value, selectedMonth.value)) @@ -316,6 +312,22 @@ const addMonths = (date: Date, months: number) => { return next } +const shiftMonth = (delta: number) => { + const next = new Date(selectedYear.value, selectedMonth.value + delta, 1) + selectedYear.value = next.getFullYear() + selectedMonth.value = next.getMonth() +} + +const onMonthPickerValue = (value: string) => { + if (!value) return + const [yearStr, monthStr] = value.split('-') + const year = Number(yearStr) + const month = Number(monthStr) + if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12) return + selectedYear.value = year + selectedMonth.value = month - 1 +} + // Limite l'intervalle d'impression à 2 mois max. const enforcePrintRange = () => { if (!printForm.from) return From 36fe9ae54cc860c2268283790506a7743ddc7e32 Mon Sep 17 00:00:00 2001 From: gitea-actions Date: Mon, 2 Mar 2026 09:50:14 +0000 Subject: [PATCH 4/4] chore: bump version to v0.1.17 --- config/version.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/version.yaml b/config/version.yaml index 998931d..bb12e2d 100644 --- a/config/version.yaml +++ b/config/version.yaml @@ -1,2 +1,2 @@ parameters: - app.version: '0.1.16' + app.version: '0.1.17'