-
-
+
+
| Report |
- |
{{ formatMinutes(summary!.carryBase25Minutes) }} |
- {{ formatMinutes(summary!.carryBonus25Minutes) }} |
+ {{ formatMinutes(summary!.carryBonus25Minutes) }} |
+ {{ formatMinutes(summary!.carryBase25Minutes + summary!.carryBonus25Minutes) }} |
{{ formatMinutes(summary!.carryBase50Minutes) }} |
- {{ formatMinutes(summary!.carryBonus50Minutes) }} |
+ {{ formatMinutes(summary!.carryBonus50Minutes) }} |
+ {{ formatMinutes(summary!.carryBase50Minutes + summary!.carryBonus50Minutes) }} |
{{ formatMinutes(summary!.carryFromPreviousYearMinutes) }} |
+
+
+ | Report |
+ - |
+ {{ formatMinutes(monthReport.base25) }} |
+ {{ formatMinutes(monthReport.bonus25) }} |
+ {{ formatMinutes(monthReport.total25) }} |
+ {{ formatMinutes(monthReport.base50) }} |
+ {{ formatMinutes(monthReport.bonus50) }} |
+ {{ formatMinutes(monthReport.total50) }} |
+ {{ formatMinutes(monthReport.total) }} |
+
+
{{ formatMinutes(week.base25Minutes) }}
0 h
- |
+ |
{{ formatMinutes(week.bonus25Minutes) }}
0 h
|
+
+ {{ formatMinutes(week.base25Minutes + week.bonus25Minutes) }}
+ 0 h
+ |
{{ formatMinutes(week.base50Minutes) }}
0 h
|
-
+ |
{{ formatMinutes(week.bonus50Minutes) }}
0 h
|
+
+ {{ formatMinutes(week.base50Minutes + week.bonus50Minutes) }}
+ 0 h
+ |
{{ formatMinutes(week.totalMinutes) }}
0 h
@@ -110,9 +137,11 @@
| Total |
{{ formatMinutes(totals.overtime) }} |
{{ formatMinutes(totals.base25) }} |
- {{ formatMinutes(totals.bonus25) }} |
+ {{ formatMinutes(totals.bonus25) }} |
+ {{ formatMinutes(totals.total25) }} |
{{ formatMinutes(totals.base50) }} |
- {{ formatMinutes(totals.bonus50) }} |
+ {{ formatMinutes(totals.bonus50) }} |
+ {{ formatMinutes(totals.total50) }} |
{{ formatMinutes(totals.total) }} |
@@ -121,9 +150,11 @@
Payé |
- |
{{ currentPayment ? formatMinutes(-currentPayment.paidBase25Minutes) : '0 h' }} |
- {{ currentPayment ? formatMinutes(-currentPayment.paidBonus25Minutes) : '0 h' }} |
+ {{ currentPayment ? formatMinutes(-currentPayment.paidBonus25Minutes) : '0 h' }} |
+ {{ currentPayment ? formatMinutes(-(currentPayment.paidBase25Minutes + currentPayment.paidBonus25Minutes)) : '0 h' }} |
{{ currentPayment ? formatMinutes(-currentPayment.paidBase50Minutes) : '0 h' }} |
- {{ currentPayment ? formatMinutes(-currentPayment.paidBonus50Minutes) : '0 h' }} |
+ {{ currentPayment ? formatMinutes(-currentPayment.paidBonus50Minutes) : '0 h' }} |
+ {{ currentPayment ? formatMinutes(-(currentPayment.paidBase50Minutes + currentPayment.paidBonus50Minutes)) : '0 h' }} |
{{ formatMinutes(paidTotal) }} |
@@ -131,10 +162,12 @@
| Reste |
- |
- {{ formatMinutes(totals.base25 - (currentPayment?.paidBase25Minutes ?? 0)) }} |
- {{ formatMinutes(totals.bonus25 - (currentPayment?.paidBonus25Minutes ?? 0)) }} |
- {{ formatMinutes(totals.base50 - (currentPayment?.paidBase50Minutes ?? 0)) }} |
- {{ formatMinutes(totals.bonus50 - (currentPayment?.paidBonus50Minutes ?? 0)) }} |
+ {{ formatMinutes(monthReport.base25 + totals.base25 - (currentPayment?.paidBase25Minutes ?? 0)) }} |
+ {{ formatMinutes(monthReport.bonus25 + totals.bonus25 - (currentPayment?.paidBonus25Minutes ?? 0)) }} |
+ {{ formatMinutes(monthReport.total25 + totals.total25 - (currentPayment?.paidBase25Minutes ?? 0) - (currentPayment?.paidBonus25Minutes ?? 0)) }} |
+ {{ formatMinutes(monthReport.base50 + totals.base50 - (currentPayment?.paidBase50Minutes ?? 0)) }} |
+ {{ formatMinutes(monthReport.bonus50 + totals.bonus50 - (currentPayment?.paidBonus50Minutes ?? 0)) }} |
+ {{ formatMinutes(monthReport.total50 + totals.total50 - (currentPayment?.paidBase50Minutes ?? 0) - (currentPayment?.paidBonus50Minutes ?? 0)) }} |
{{ formatMinutes(resteTotal) }} |
@@ -290,44 +323,91 @@ const paddedWeeks = computed((): (EmployeeRttWeekSummary | null)[] => {
return padded
})
-// --- Report row ---
+// --- Carry row (RTT rollover from previous year, June only) ---
-const reportMonth = computed(() => {
+const carryMonth = computed(() => {
if (!props.summary) return 6
- const carryMonth = props.summary.carryMonth
- // Report appears in the month AFTER carryMonth (wrapping 12 -> 1)
- return carryMonth >= 12 ? 1 : carryMonth + 1
+ const cm = props.summary.carryMonth
+ return cm >= 12 ? 1 : cm + 1
})
-const showReportRow = computed(() => {
+const showCarryRow = computed(() => {
return (
- currentMonth.value === reportMonth.value &&
+ currentMonth.value === carryMonth.value &&
(props.summary?.carryFromPreviousYearMinutes ?? 0) > 0
)
})
-// --- Totals ---
+// --- Month report row (cumulated balance from previous months) ---
+
+// Months of the exercise in order, starting from the carry month
+const exerciseMonths = computed((): number[] => {
+ const start = carryMonth.value
+ const startIdx = orderedMonths.indexOf(start as (typeof orderedMonths)[number])
+ if (startIdx === -1) return [...orderedMonths]
+ return [...orderedMonths.slice(startIdx), ...orderedMonths.slice(0, startIdx)]
+})
+
+const monthReport = computed(() => {
+ if (!props.summary) return { base25: 0, bonus25: 0, total25: 0, base50: 0, bonus50: 0, total50: 0, total: 0 }
+
+ const cm = currentMonth.value
+ const cmIdx = exerciseMonths.value.indexOf(cm)
+ const previousMonths = exerciseMonths.value.slice(0, cmIdx)
+
+ // Start from carry (included in the cumulation)
+ let base25 = props.summary.carryBase25Minutes
+ let bonus25 = props.summary.carryBonus25Minutes
+ let base50 = props.summary.carryBase50Minutes
+ let bonus50 = props.summary.carryBonus50Minutes
+ let total = props.summary.carryFromPreviousYearMinutes
+
+ // Add weeks from previous months
+ for (const w of props.summary.weeks) {
+ if (previousMonths.includes(w.month)) {
+ base25 += w.base25Minutes
+ bonus25 += w.bonus25Minutes
+ base50 += w.base50Minutes
+ bonus50 += w.bonus50Minutes
+ total += w.totalMinutes
+ }
+ }
+
+ // Subtract payments from previous months
+ for (const p of props.summary.monthPayments) {
+ if (previousMonths.includes(p.month)) {
+ base25 -= p.paidBase25Minutes
+ bonus25 -= p.paidBonus25Minutes
+ base50 -= p.paidBase50Minutes
+ bonus50 -= p.paidBonus50Minutes
+ total -= (p.paidBase25Minutes + p.paidBonus25Minutes + p.paidBase50Minutes + p.paidBonus50Minutes)
+ }
+ }
+
+ return { base25, bonus25, total25: base25 + bonus25, base50, bonus50, total50: base50 + bonus50, total }
+})
+
+const showMonthReportRow = computed(() => {
+ // Not on the carry month — carry row handles that
+ if (currentMonth.value === carryMonth.value) return false
+ const r = monthReport.value
+ return r.total !== 0
+})
+
+// --- Totals (current month weeks only) ---
const totals = computed(() => {
const weeks = weeksForCurrentMonth.value
- const base = {
+ return {
overtime: weeks.reduce((s, w) => s + w.overtimeMinutes, 0),
base25: weeks.reduce((s, w) => s + w.base25Minutes, 0),
bonus25: weeks.reduce((s, w) => s + w.bonus25Minutes, 0),
+ total25: weeks.reduce((s, w) => s + w.base25Minutes + w.bonus25Minutes, 0),
base50: weeks.reduce((s, w) => s + w.base50Minutes, 0),
bonus50: weeks.reduce((s, w) => s + w.bonus50Minutes, 0),
+ total50: weeks.reduce((s, w) => s + w.base50Minutes + w.bonus50Minutes, 0),
total: weeks.reduce((s, w) => s + w.totalMinutes, 0),
}
-
- if (showReportRow.value && props.summary) {
- base.base25 += props.summary.carryBase25Minutes
- base.bonus25 += props.summary.carryBonus25Minutes
- base.base50 += props.summary.carryBase50Minutes
- base.bonus50 += props.summary.carryBonus50Minutes
- base.total += props.summary.carryFromPreviousYearMinutes
- }
-
- return base
})
const currentPayment = computed(() => {
@@ -342,7 +422,7 @@ const paidTotal = computed(() => {
})
const resteTotal = computed(() => {
- return totals.value.total + paidTotal.value
+ return monthReport.value.total + totals.value.total + paidTotal.value
})
// --- Format ---
diff --git a/src/Service/Rtt/RttRecoveryComputationService.php b/src/Service/Rtt/RttRecoveryComputationService.php
index 772cacc..3883ef1 100644
--- a/src/Service/Rtt/RttRecoveryComputationService.php
+++ b/src/Service/Rtt/RttRecoveryComputationService.php
@@ -21,13 +21,18 @@ use DateTimeImmutable;
final readonly class RttRecoveryComputationService
{
+ private ?DateTimeImmutable $rttStartDate;
+
public function __construct(
private WorkHourRepository $workHourRepository,
private AbsenceRepository $absenceRepository,
private AbsenceSegmentsResolver $absenceSegmentsResolver,
private WorkedHoursCreditPolicy $workedHoursCreditPolicy,
private EmployeeContractResolver $contractResolver,
- ) {}
+ string $rttStartDate = '',
+ ) {
+ $this->rttStartDate = '' !== $rttStartDate ? new DateTimeImmutable($rttStartDate) : null;
+ }
/**
* @return array{DateTimeImmutable, DateTimeImmutable}
@@ -71,7 +76,7 @@ final readonly class RttRecoveryComputationService
return $weeks;
}
- public function computeTotalRecoveryForExercise(Employee $employee, int $exerciseYear): WeekRecoveryDetail
+ public function computeTotalRecoveryForExercise(Employee $employee, int $exerciseYear, ?DateTimeImmutable $limitDate = null): WeekRecoveryDetail
{
[$from, $to] = $this->resolveExerciseBounds($exerciseYear);
$weeks = $this->buildWeeksForExercise($from, $to);
@@ -85,7 +90,7 @@ final readonly class RttRecoveryComputationService
$weeks
);
- $byWeek = $this->computeRecoveryByWeek($employee, $weekRanges, $from, $to, null);
+ $byWeek = $this->computeRecoveryByWeek($employee, $weekRanges, $from, $to, $limitDate);
$total = new WeekRecoveryDetail();
foreach ($byWeek as $detail) {
@@ -172,6 +177,12 @@ final readonly class RttRecoveryComputationService
continue;
}
+ if ($this->rttStartDate instanceof DateTimeImmutable && $effectiveEnd < $this->rttStartDate) {
+ $results[$weekKey] = new WeekRecoveryDetail();
+
+ continue;
+ }
+
$weekDays = [];
for ($cursor = $effectiveStart; $cursor <= $effectiveEnd; $cursor = $cursor->modify('+1 day')) {
$weekDays[] = $cursor->format('Y-m-d');
@@ -203,7 +214,7 @@ final readonly class RttRecoveryComputationService
$overtime25StartMinutes = $this->computeWeeklyOvertime25StartMinutes($weekDays, $employeeContractsByDate);
$weeklyOvertimeTotalMinutes = $isWeekPresenceTracking
? 0
- : max(0, $weeklyTotalMinutes - $overtimeReferenceMinutes);
+ : $weeklyTotalMinutes - $overtimeReferenceMinutes;
$base25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : max(0, min($weeklyTotalMinutes, 43 * 60) - $overtime25StartMinutes);
$bonus25 = ($isWeekPresenceTracking || $disableOvertimeBonuses) ? 0 : (int) round($base25 * 0.25);
diff --git a/src/State/EmployeeLeaveSummaryProvider.php b/src/State/EmployeeLeaveSummaryProvider.php
index 7f54afb..fd30908 100644
--- a/src/State/EmployeeLeaveSummaryProvider.php
+++ b/src/State/EmployeeLeaveSummaryProvider.php
@@ -128,7 +128,7 @@ final readonly class EmployeeLeaveSummaryProvider implements ProviderInterface
*/
public function computeYearSummary(Employee $employee, int $targetYear): ?array
{
- $firstYear = $this->resolveFirstComputationYear($employee);
+ $firstYear = max($this->resolveFirstComputationYear($employee), $targetYear - 1);
if ($targetYear < $firstYear) {
$targetYear = $firstYear;
}
diff --git a/src/State/EmployeeRttSummaryProvider.php b/src/State/EmployeeRttSummaryProvider.php
index 3deda78..ecc9731 100644
--- a/src/State/EmployeeRttSummaryProvider.php
+++ b/src/State/EmployeeRttSummaryProvider.php
@@ -72,9 +72,12 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
$weeks
);
- $limitDate = null;
if ($year > $currentExerciseYear) {
$limitDate = $periodFrom->modify('-1 day');
+ } else {
+ // Exclude the current (incomplete) week: limit to last Sunday
+ $isoDay = (int) $today->format('N'); // 1=Monday .. 7=Sunday
+ $limitDate = 7 === $isoDay ? $today : $today->modify('last sunday');
}
$currentByWeekStart = $this->rttRecoveryService->computeRecoveryByWeek($employee, $weekRanges, $periodFrom, $periodTo, $limitDate);
@@ -110,6 +113,37 @@ final readonly class EmployeeRttSummaryProvider implements ProviderInterface
$weekRanges
);
+ // Post-process: distribute deficit weeks across cumulative balance (50% first, then 25%)
+ $cumulative50 = $carry->base50Minutes + $carry->bonus50Minutes;
+ $cumulative25 = $carry->base25Minutes + $carry->bonus25Minutes;
+
+ foreach ($summary->weeks as $i => $week) {
+ if ($week->totalMinutes >= 0) {
+ $cumulative50 += $week->base50Minutes + $week->bonus50Minutes;
+ $cumulative25 += $week->base25Minutes + $week->bonus25Minutes;
+ } else {
+ $deficit = -$week->totalMinutes;
+ $from50 = min($deficit, max(0, $cumulative50));
+ $from25 = $deficit - $from50;
+
+ $cumulative50 -= $from50;
+ $cumulative25 -= $from25;
+
+ $summary->weeks[$i] = new EmployeeRttWeekSummary(
+ month: $week->month,
+ weekNumber: $week->weekNumber,
+ weekStart: $week->weekStart,
+ weekEnd: $week->weekEnd,
+ overtimeMinutes: $week->overtimeMinutes,
+ base25Minutes: $from25 > 0 ? -$from25 : 0,
+ bonus25Minutes: 0,
+ base50Minutes: $from50 > 0 ? -$from50 : 0,
+ bonus50Minutes: 0,
+ totalMinutes: $week->totalMinutes,
+ );
+ }
+ }
+
$payments = $this->rttPaymentRepository->findByEmployeeAndYear($employee, $year);
$monthBuckets = [];
diff --git a/src/State/LeaveRecapPrintProvider.php b/src/State/LeaveRecapPrintProvider.php
index 9326563..05c2edc 100644
--- a/src/State/LeaveRecapPrintProvider.php
+++ b/src/State/LeaveRecapPrintProvider.php
@@ -9,13 +9,10 @@ use ApiPlatform\State\ProviderInterface;
use App\Entity\Employee;
use App\Enum\ContractNature;
use App\Enum\ContractType;
-use App\Enum\LeaveRuleCode;
use App\Enum\TrackingMode;
-use App\Repository\EmployeeLeaveBalanceRepository;
use App\Repository\EmployeeRepository;
use App\Repository\EmployeeRttBalanceRepository;
use App\Repository\EmployeeRttPaymentRepository;
-use App\Service\PublicHolidayServiceInterface;
use App\Service\Rtt\RttRecoveryComputationService;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
@@ -30,8 +27,7 @@ class LeaveRecapPrintProvider implements ProviderInterface
public function __construct(
private Environment $twig,
private EmployeeRepository $employeeRepository,
- private EmployeeLeaveBalanceRepository $leaveBalanceRepository,
- private PublicHolidayServiceInterface $publicHolidayService,
+ private EmployeeLeaveSummaryProvider $leaveSummaryProvider,
private RttRecoveryComputationService $rttRecoveryService,
private EmployeeRttBalanceRepository $rttBalanceRepository,
private EmployeeRttPaymentRepository $rttPaymentRepository,
@@ -95,26 +91,24 @@ class LeaveRecapPrintProvider implements ProviderInterface
$nature = ContractNature::tryFrom($employee->getCurrentContractNature());
$isInterim = ContractNature::INTERIM === $nature;
- $acquiredDays = 0.0;
+ $cpN1Remaining = 0.0;
$cpN = '-';
$acquiredSaturdays = '-';
$rtt = '-';
if (!$isInterim) {
- $leaveYear = $this->resolveLeaveYear($employee, $today);
- $ruleCode = $isForfait ? LeaveRuleCode::FORFAIT_218 : LeaveRuleCode::CDI_CDD_NON_FORFAIT;
- $balance = $this->leaveBalanceRepository->findOneByEmployeeRuleAndYear($employee, $ruleCode, $leaveYear);
+ $leaveYear = $this->leaveSummaryProvider->resolveLeaveYearForToday($employee);
+ $yearSummary = $this->leaveSummaryProvider->computeYearSummary($employee, $leaveYear);
- if (null !== $balance) {
- $acquiredDays = $balance->getOpeningDays();
- $acquiredSaturdays = $isForfait ? '-' : (string) $balance->getOpeningSaturdays();
- }
-
- if ($isForfait) {
- try {
- $cpN = (string) $this->computeForfaitAcquiredDays($employee, $today);
- } catch (Throwable) {
- $cpN = '-';
+ if (null !== $yearSummary) {
+ if ($isForfait) {
+ $cpN1Remaining = $yearSummary['previousYearRemainingDays'];
+ $cpN = (string) $yearSummary['acquiredDays'];
+ $acquiredSaturdays = '-';
+ } else {
+ $cpN1Remaining = $yearSummary['remainingDays'];
+ $cpN = (string) $yearSummary['accruingDays'];
+ $acquiredSaturdays = (string) $yearSummary['remainingSaturdays'];
}
}
@@ -131,31 +125,23 @@ class LeaveRecapPrintProvider implements ProviderInterface
'lastName' => $employee->getLastName(),
'firstName' => $employee->getFirstName(),
'contractName' => $contractName,
- 'acquiredDays' => $acquiredDays,
+ 'cpN1Remaining' => $cpN1Remaining,
'cpN' => $cpN,
'acquiredSaturdays' => $acquiredSaturdays,
'rtt' => $rtt,
];
}
- private function resolveLeaveYear(Employee $employee, DateTimeImmutable $today): int
- {
- if (ContractType::FORFAIT === $employee->getContract()?->getType()) {
- return (int) $today->format('Y');
- }
-
- $month = (int) $today->format('n');
- $year = (int) $today->format('Y');
-
- return $month >= 6 ? $year + 1 : $year;
- }
-
private function computeAvailableRttMinutes(Employee $employee, DateTimeImmutable $today): int
{
$month = (int) $today->format('n');
$year = (int) $today->format('Y');
$exerciseYear = $month >= 6 ? $year + 1 : $year;
+ // Exclude incomplete current week: limit to last Sunday
+ $isoDay = (int) $today->format('N');
+ $limitDate = 7 === $isoDay ? $today : $today->modify('last sunday');
+
// Carry from previous exercise
$carry = 0;
$balance = $this->rttBalanceRepository->findOneByEmployeeAndYear($employee, $exerciseYear);
@@ -166,8 +152,8 @@ class LeaveRecapPrintProvider implements ProviderInterface
$carry = $previousTotal->totalMinutes;
}
- // Current exercise
- $current = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $exerciseYear);
+ // Current exercise (limited to completed weeks)
+ $current = $this->rttRecoveryService->computeTotalRecoveryForExercise($employee, $exerciseYear, $limitDate);
// Paid RTT
$paid = 0;
@@ -179,41 +165,6 @@ class LeaveRecapPrintProvider implements ProviderInterface
return $carry + $current->totalMinutes - $paid;
}
- private function computeForfaitAcquiredDays(Employee $employee, DateTimeImmutable $today): float
- {
- $year = (int) $today->format('Y');
- $from = new DateTimeImmutable(sprintf('%d-01-01', $year));
- $to = new DateTimeImmutable(sprintf('%d-12-31', $year));
-
- $contractStartRaw = $employee->getCurrentContractStartDate();
- if (null !== $contractStartRaw && '' !== trim($contractStartRaw)) {
- $contractStart = DateTimeImmutable::createFromFormat('!Y-m-d', trim($contractStartRaw));
- if ($contractStart instanceof DateTimeImmutable && $contractStart > $from) {
- $from = $contractStart;
- }
- }
-
- $contractEndRaw = $employee->getCurrentContractEndDate();
- if (null !== $contractEndRaw && '' !== trim($contractEndRaw)) {
- $contractEnd = DateTimeImmutable::createFromFormat('!Y-m-d', trim($contractEndRaw));
- if ($contractEnd instanceof DateTimeImmutable && $contractEnd < $to) {
- $to = $contractEnd;
- }
- }
-
- $holidays = $this->publicHolidayService->getHolidaysDayByYears('metropole', (string) $year);
- $businessDays = 0;
-
- for ($cursor = $from; $cursor <= $to; $cursor = $cursor->modify('+1 day')) {
- $weekDay = (int) $cursor->format('N');
- if ($weekDay <= 5 && !isset($holidays[$cursor->format('Y-m-d')])) {
- ++$businessDays;
- }
- }
-
- return (float) max(0, $businessDays - 218);
- }
-
private function formatMinutes(int $minutes): string
{
if (0 === $minutes) {
diff --git a/templates/leave-recap/print.html.twig b/templates/leave-recap/print.html.twig
index e37ee42..1880bd9 100644
--- a/templates/leave-recap/print.html.twig
+++ b/templates/leave-recap/print.html.twig
@@ -86,9 +86,9 @@