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 @@
+