From 8084198b728bc734f225a7f931184f2039eb8bbd Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 2 Apr 2026 08:56:24 +0200 Subject: [PATCH] =?UTF-8?q?feat=20:=20ajouter=20pour=20les=20forfaits=20le?= =?UTF-8?q?=20paiement=20de=20cong=C3=A9s=20N-1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/functional-rules.md | 1 + frontend/components/employees/LeaveTab.vue | 61 +++++++++++ frontend/composables/useEmployeeLeave.ts | 12 ++- frontend/pages/employees/[id].vue | 2 + .../services/dto/employee-leave-summary.ts | 1 + frontend/services/employee-leave-summary.ts | 8 ++ migrations/Version20260402064647.php | 27 +++++ src/ApiResource/EmployeeLeaveSummary.php | 1 + .../EmployeePaidLeaveDaysInput.php | 27 +++++ src/Entity/EmployeeLeaveBalance.php | 15 +++ src/State/EmployeeLeaveSummaryProvider.php | 11 +- src/State/EmployeePaidLeaveDaysProcessor.php | 101 ++++++++++++++++++ src/State/EmployeePaidLeaveDaysProvider.php | 17 +++ 13 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 migrations/Version20260402064647.php create mode 100644 src/ApiResource/EmployeePaidLeaveDaysInput.php create mode 100644 src/State/EmployeePaidLeaveDaysProcessor.php create mode 100644 src/State/EmployeePaidLeaveDaysProvider.php diff --git a/doc/functional-rules.md b/doc/functional-rules.md index cd9fa5a..a15f719 100644 --- a/doc/functional-rules.md +++ b/doc/functional-rules.md @@ -245,6 +245,7 @@ Tous les filtres checkbox sont cochés par défaut à l'ouverture du drawer. - pour `FORFAIT`: - pris: basé sur toutes les absences (demi-journées incluses) - restants = acquis - pris (borné à 0) + - paiement congés N-1: saisie RH via `PATCH /employees/{id}/paid-leave-days` (body: `paidLeaveDays`, `year`). Stocké dans `employee_leave_balances.paid_leave_days`. Les jours payés sont soustraits du reste à prendre N-1 (`previousYearRemainingDays = max(0, acquis_N-1 - pris_N-1 - payés)`). Uniquement pour les contrats forfait. - report annuel: - le reliquat (`restants`) de l'exercice précédent est reporté dans les acquis de l'exercice courant - pour `CDI`/`CDD` non forfait: report séparé jours + samedis diff --git a/frontend/components/employees/LeaveTab.vue b/frontend/components/employees/LeaveTab.vue index 35cf17c..b61e5d5 100644 --- a/frontend/components/employees/LeaveTab.vue +++ b/frontend/components/employees/LeaveTab.vue @@ -32,6 +32,18 @@

Reste à prendre : {{ formatCount(summary?.previousYearRemainingDays) }} Jours

+
+
+ Année n-1 payés : + {{ formatCount(summary?.previousYearPaidDays) }} Jours +
+ +
Fractionné acquis : @@ -112,6 +124,39 @@
+ +
+
+ + +
+ +
+ + +
+
+
@@ -136,11 +181,15 @@ const props = defineProps<{ const emit = defineEmits<{ (event: 'update-fractioned-days', days: number): void + (event: 'update-paid-leave-days', days: number): void }>() const isFractionedDrawerOpen = ref(false) const fractionedForm = reactive({days: 0}) +const isPaidLeaveDrawerOpen = ref(false) +const paidLeaveForm = reactive({days: 0}) + const openFractionedDrawer = () => { fractionedForm.days = props.summary?.fractionedDays ?? 0 isFractionedDrawerOpen.value = true @@ -153,6 +202,18 @@ const handleSubmitFractioned = () => { isFractionedDrawerOpen.value = false } +const openPaidLeaveDrawer = () => { + paidLeaveForm.days = props.summary?.previousYearPaidDays ?? 0 + isPaidLeaveDrawerOpen.value = true +} + +const handleSubmitPaidLeave = () => { + const value = Number(paidLeaveForm.days) + if (Number.isNaN(value) || value < 0) return + emit('update-paid-leave-days', value) + isPaidLeaveDrawerOpen.value = false +} + const monthLabels = [ 'Janvier', 'Fevrier', diff --git a/frontend/composables/useEmployeeLeave.ts b/frontend/composables/useEmployeeLeave.ts index 4b250f2..cf6e694 100644 --- a/frontend/composables/useEmployeeLeave.ts +++ b/frontend/composables/useEmployeeLeave.ts @@ -4,7 +4,7 @@ import type { EmployeeLeaveSummary } from '~/services/dto/employee-leave-summary import type { Employee } from '~/services/dto/employee' import { CONTRACT_TYPES } from '~/services/dto/contract' import { listAbsences } from '~/services/absences' -import { getEmployeeLeaveSummary, updateFractionedDays } from '~/services/employee-leave-summary' +import { getEmployeeLeaveSummary, updateFractionedDays, updatePaidLeaveDays } from '~/services/employee-leave-summary' import { listPublicHolidays } from '~/services/public-holidays' export const useEmployeeLeave = (employee: Ref, reloadEmployee: () => Promise) => { @@ -57,6 +57,13 @@ export const useEmployeeLeave = (employee: Ref, reloadEmployee: await reloadEmployee() } + const submitPaidLeaveDays = async (days: number) => { + if (!employee.value) return + const year = leaveSummary.value?.year ?? undefined + await updatePaidLeaveDays(employee.value.id, days, year) + await reloadEmployee() + } + return { employeeAbsences, leaveSummary, @@ -65,6 +72,7 @@ export const useEmployeeLeave = (employee: Ref, reloadEmployee: leaveDataLoaded, loadLeaveData, resetLoaded, - submitFractionedDays + submitFractionedDays, + submitPaidLeaveDays } } diff --git a/frontend/pages/employees/[id].vue b/frontend/pages/employees/[id].vue index 39d03b0..688c062 100644 --- a/frontend/pages/employees/[id].vue +++ b/frontend/pages/employees/[id].vue @@ -148,6 +148,7 @@ :summary="leaveSummary" :public-holidays="publicHolidays" @update-fractioned-days="submitFractionedDays" + @update-paid-leave-days="submitPaidLeaveDays" />
@@ -259,6 +260,7 @@ const { submitContractUpdate, submitCreateContract, submitFractionedDays, + submitPaidLeaveDays, submitRttPayment, suspensionForms, isSuspensionSubmitting, diff --git a/frontend/services/dto/employee-leave-summary.ts b/frontend/services/dto/employee-leave-summary.ts index fe9bfd2..20a85e0 100644 --- a/frontend/services/dto/employee-leave-summary.ts +++ b/frontend/services/dto/employee-leave-summary.ts @@ -13,6 +13,7 @@ export type EmployeeLeaveSummary = { previousYearAcquiredDays: number previousYearTakenDays: number previousYearRemainingDays: number + previousYearPaidDays: number presenceDaysByMonth: Record } diff --git a/frontend/services/employee-leave-summary.ts b/frontend/services/employee-leave-summary.ts index b419abc..ab73825 100644 --- a/frontend/services/employee-leave-summary.ts +++ b/frontend/services/employee-leave-summary.ts @@ -16,3 +16,11 @@ export const updateFractionedDays = async (employeeId: number, fractionedDays: n return api.patch(`/employees/${employeeId}/fractioned-days`, body) } +export const updatePaidLeaveDays = async (employeeId: number, paidLeaveDays: number, year?: number) => { + const api = useApi() + const body: Record = { paidLeaveDays } + if (year) body.year = year + + return api.patch(`/employees/${employeeId}/paid-leave-days`, body) +} + diff --git a/migrations/Version20260402064647.php b/migrations/Version20260402064647.php new file mode 100644 index 0000000..7eb3865 --- /dev/null +++ b/migrations/Version20260402064647.php @@ -0,0 +1,27 @@ +addSql('ALTER TABLE employee_leave_balances ADD paid_leave_days DOUBLE PRECISION DEFAULT 0 NOT NULL'); + $this->addSql("COMMENT ON COLUMN employee_leave_balances.paid_leave_days IS 'Jours de conges N-1 payes par la RH (forfait uniquement).'"); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE employee_leave_balances DROP paid_leave_days'); + } +} diff --git a/src/ApiResource/EmployeeLeaveSummary.php b/src/ApiResource/EmployeeLeaveSummary.php index 49f2832..0b275bd 100644 --- a/src/ApiResource/EmployeeLeaveSummary.php +++ b/src/ApiResource/EmployeeLeaveSummary.php @@ -34,6 +34,7 @@ final class EmployeeLeaveSummary public float $previousYearAcquiredDays = 0.0; public float $previousYearTakenDays = 0.0; public float $previousYearRemainingDays = 0.0; + public float $previousYearPaidDays = 0.0; /** @var array YYYY-MM => count (0.5 for half-days) */ public array $presenceDaysByMonth = []; diff --git a/src/ApiResource/EmployeePaidLeaveDaysInput.php b/src/ApiResource/EmployeePaidLeaveDaysInput.php new file mode 100644 index 0000000..8efc2c9 --- /dev/null +++ b/src/ApiResource/EmployeePaidLeaveDaysInput.php @@ -0,0 +1,27 @@ + 0, 'comment' => 'Jours de fractionnement saisis par la RH.'])] private float $fractionedDays = 0.0; + #[ORM\Column(type: 'float', options: ['default' => 0, 'comment' => 'Jours de conges N-1 payes par la RH (forfait uniquement).'])] + private float $paidLeaveDays = 0.0; + #[ORM\Column(type: 'boolean', options: ['default' => false, 'comment' => 'Indique si le solde de l exercice est fige (verrouille RH).'])] private bool $isLocked = false; @@ -222,6 +225,18 @@ class EmployeeLeaveBalance return $this; } + public function getPaidLeaveDays(): float + { + return $this->paidLeaveDays; + } + + public function setPaidLeaveDays(float $paidLeaveDays): self + { + $this->paidLeaveDays = $paidLeaveDays; + + return $this; + } + public function isLocked(): bool { return $this->isLocked; diff --git a/src/State/EmployeeLeaveSummaryProvider.php b/src/State/EmployeeLeaveSummaryProvider.php index 6223902..3453ed7 100644 --- a/src/State/EmployeeLeaveSummaryProvider.php +++ b/src/State/EmployeeLeaveSummaryProvider.php @@ -93,6 +93,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface } $fractionedDays = $this->resolveFractionedDays($employee, $yearSummary['ruleCode'], $year); + $paidLeaveDays = $this->resolvePaidLeaveDays($employee, $yearSummary['ruleCode'], $year); $summary->isSupported = true; $summary->ruleCode = $yearSummary['ruleCode']; @@ -106,7 +107,8 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface $summary->remainingSaturdays = $yearSummary['remainingSaturdays']; $summary->previousYearAcquiredDays = $yearSummary['previousYearAcquiredDays']; $summary->previousYearTakenDays = $yearSummary['previousYearTakenDays']; - $summary->previousYearRemainingDays = $yearSummary['previousYearRemainingDays']; + $summary->previousYearRemainingDays = max(0.0, $yearSummary['previousYearRemainingDays'] - $paidLeaveDays); + $summary->previousYearPaidDays = $paidLeaveDays; [$periodFrom, $periodTo] = $this->resolvePeriodBounds($employee, $year); $summary->presenceDaysByMonth = $this->computePresenceDaysByMonth($employee, $periodFrom, $periodTo); @@ -765,6 +767,13 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface return null !== $balance ? $balance->getFractionedDays() : 0.0; } + private function resolvePaidLeaveDays(Employee $employee, string $ruleCode, int $year): float + { + $balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year); + + return null !== $balance ? $balance->getPaidLeaveDays() : 0.0; + } + private function resolveCurrentLeaveYear(DateTimeImmutable $today): int { $year = (int) $today->format('Y'); diff --git a/src/State/EmployeePaidLeaveDaysProcessor.php b/src/State/EmployeePaidLeaveDaysProcessor.php new file mode 100644 index 0000000..8ba00c6 --- /dev/null +++ b/src/State/EmployeePaidLeaveDaysProcessor.php @@ -0,0 +1,101 @@ +employeeRepository->find($employeeId); + if (!$employee instanceof Employee) { + throw new NotFoundHttpException('Employee not found.'); + } + + $year = $data->year ?? $this->resolveCurrentYear($employee); + $ruleCode = $this->resolveRuleCode($employee); + + $balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $year); + + if (null === $balance) { + $balance = new EmployeeLeaveBalance(); + $balance->setEmployee($employee); + $balance->setRuleCode($ruleCode); + $balance->setYear($year); + $this->entityManager->persist($balance); + } + + $balance->setPaidLeaveDays($data->paidLeaveDays); + $balance->touch(); + + $empName = trim(($employee->getLastName() ?? '').' '.($employee->getFirstName() ?? '')); + $this->auditLogger->log( + $employee, + 'update', + 'paid_leave_days', + $balance->getId(), + sprintf('Congés N-1 payés modifiés pour %s (année %d) : %s', $empName, $year, (string) $data->paidLeaveDays), + ['new' => ['paidLeaveDays' => $data->paidLeaveDays, 'year' => $year]], + ); + + $this->entityManager->flush(); + + $data->year = $year; + + return $data; + } + + private function resolveRuleCode(Employee $employee): LeaveRuleCode + { + if (ContractType::FORFAIT === $employee->getContract()?->getType()) { + return LeaveRuleCode::FORFAIT_218; + } + + return LeaveRuleCode::CDI_CDD_NON_FORFAIT; + } + + private function resolveCurrentYear(Employee $employee): int + { + $today = new DateTimeImmutable('today'); + + if (ContractType::FORFAIT === $employee->getContract()?->getType()) { + return (int) $today->format('Y'); + } + + $month = (int) $today->format('n'); + + return $month >= 6 ? (int) $today->format('Y') + 1 : (int) $today->format('Y'); + } +} diff --git a/src/State/EmployeePaidLeaveDaysProvider.php b/src/State/EmployeePaidLeaveDaysProvider.php new file mode 100644 index 0000000..bf696cd --- /dev/null +++ b/src/State/EmployeePaidLeaveDaysProvider.php @@ -0,0 +1,17 @@ +