feat(planning): #62 #63 #64 enhance leave planner with calculation date and entitlement details

- Introduced calculationDate in various service interfaces and snapshots to track the date of planning calculations.
- Added futureEntitlement and includedEntitlement fields to the translation service and respective language files for better user feedback.
- Updated leave-planner.worker to handle calculationDate and declaredDays in requests.
- Enhanced tests to ensure proper handling of calculation dates and artifact retention during planning processes.
- Improved snapshot and explanation outputs to include calculation date for clarity in planning results.
This commit is contained in:
nessar committed 2026-10-07 19:22:46 +02:00
1 parent 036a8c0e1e
commit 9851edf906
18 files changed
+3347 -229

No files matched your search

+41 -17
View File
@@ -1,6 +1,6 @@
import { Injectable, inject } from '@angular/core';
import { DateRange } from '@angular/material/datepicker';
import { frenchPublicHolidaysForYear } from './declared-non-working-days';
import { frenchPublicHolidaysForYear, isWorkingPlanningDate } from './declared-non-working-days';
import dayjs from 'dayjs';
import weekOfYear from 'dayjs/plugin/weekOfYear';
import isoWeek from 'dayjs/plugin/isoWeek';
@@ -11,6 +11,7 @@ import {
LeaveType,
PlanOutcome,
PlanningReason,
PlanningTraceState,
RemainingClassification,
} from './leave-planner.service';
import { addPlanningDays, addPlanningYears, planningDateInParis } from './planning-date';
@@ -18,6 +19,41 @@ import { addPlanningDays, addPlanningYears, planningDateInParis } from './planni
dayjs.extend(weekOfYear);
dayjs.extend(isoWeek);
type CalendarPlan = Pick<PlanningTraceState, 'placements' | 'declaredDays'> & {
horizon?: PlanningTraceState['horizon'];
timeOffPeriods?: PlanOutcome['timeOffPeriods'];
};
export function calendarProjectionHorizon(plan: CalendarPlan) {
if (!plan.horizon) return undefined;
const horizon = { ...plan.horizon };
if (plan.timeOffPeriods) {
for (const period of plan.timeOffPeriods) {
if (period.start < horizon.start) horizon.start = period.start;
if (period.end >= horizon.end) horizon.end = addPlanningDays(period.end, 1);
}
return horizon;
}
const declaredDates = new Set(plan.declaredDays?.map(({ date }) => date));
const anchors = [...plan.placements, ...(plan.declaredDays ?? [])]
.map(({ date }) => date)
.filter((date) => date >= horizon.start && date < horizon.end)
.sort();
if (!anchors.length) return horizon;
for (const [anchor, direction] of [
[anchors[0], -1],
[anchors[anchors.length - 1], 1],
] as const) {
let date = addPlanningDays(anchor, direction);
while (declaredDates.has(date) || !isWorkingPlanningDate(date)) {
if (date < horizon.start) horizon.start = date;
if (date >= horizon.end) horizon.end = addPlanningDays(date, 1);
date = addPlanningDays(date, direction);
}
}
return horizon;
}
@Injectable({
providedIn: 'root',
})
@@ -32,20 +68,8 @@ export class CalendarService {
return { start, end: addPlanningYears(start, 1) };
}
selectedDatesForPlan(
plan: Pick<PlanOutcome, 'placements' | 'declaredDays'> & {
horizon?: { start: string; end: string };
timeOffPeriods?: PlanOutcome['timeOffPeriods'];
},
): SelectedDates {
const projectionHorizon = plan.horizon ? { ...plan.horizon } : undefined;
if (projectionHorizon) {
for (const period of plan.timeOffPeriods ?? []) {
if (period.start < projectionHorizon.start) projectionHorizon.start = period.start;
if (period.end >= projectionHorizon.end)
projectionHorizon.end = addPlanningDays(period.end, 1);
}
}
selectedDatesForPlan(plan: CalendarPlan): SelectedDates {
const projectionHorizon = calendarProjectionHorizon(plan);
const selected = projectionHorizon
? this.getSelectedDatesForHorizon(projectionHorizon)
: this.getSelectedDatesForYear();
@@ -53,8 +77,8 @@ export class CalendarService {
...plan.placements,
...(plan.declaredDays ?? []).filter(
(day) =>
!plan.horizon ||
(day.date >= plan.horizon.start && day.date < plan.horizon.end),
!projectionHorizon ||
(day.date >= projectionHorizon.start && day.date < projectionHorizon.end),
),
]) {
selected.push(