feat(planner): implement cancellation for planning calculations
- Refactor leave-planner.worker.ts to support cancellation of ongoing calculations. - Introduce `calculatePlanningAsyncInternal` for asynchronous planning calculations. - Add cancellation handling in the worker message event. - Update planning-date.ts with new utility functions for date manipulation. - Extend translation-service.ts and translation files (en.json, de.json, es.json, fr.json) to include new strings related to planning horizon and cancellation. - Create documentation for the cancellation feature in planner-cancellation.md. - Add unit tests for cancellation functionality in leave-planner-cancellation.spec.ts and leave-planner.worker.spec.ts. #57 #58
This commit is contained in:
1 parent
5c35a59805
commit
2edf2004f5
31 files changed
+2275
-304
No files matched your search
@@ -13,7 +13,7 @@ import {
|
||||
PlanningReason,
|
||||
RemainingClassification,
|
||||
} from './leave-planner.service';
|
||||
import { addPlanningYears, planningDateInParis } from './planning-date';
|
||||
import { addPlanningDays, addPlanningYears, planningDateInParis } from './planning-date';
|
||||
|
||||
dayjs.extend(weekOfYear);
|
||||
dayjs.extend(isoWeek);
|
||||
@@ -39,10 +39,21 @@ export class CalendarService {
|
||||
}
|
||||
|
||||
selectedDatesForPlan(
|
||||
plan: Pick<PlanOutcome, 'placements'> & { horizon?: { start: string; end: string } },
|
||||
plan: Pick<PlanOutcome, 'placements'> & {
|
||||
horizon?: { start: string; end: string };
|
||||
timeOffPeriods?: PlanOutcome['timeOffPeriods'];
|
||||
},
|
||||
): SelectedDates {
|
||||
const selected = plan.horizon
|
||||
? this.getSelectedDatesForHorizon(plan.horizon)
|
||||
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);
|
||||
}
|
||||
}
|
||||
const selected = projectionHorizon
|
||||
? this.getSelectedDatesForHorizon(projectionHorizon)
|
||||
: this.getSelectedDatesForYear();
|
||||
for (const placement of plan.placements) {
|
||||
selected.push(
|
||||
@@ -58,9 +69,7 @@ export class CalendarService {
|
||||
return selected;
|
||||
}
|
||||
|
||||
// Cache for holiday timestamps
|
||||
private holidayTimestampsCache: Set<number> | null = null;
|
||||
private holidayCacheYear: number | null = null;
|
||||
private readonly holidaysByYear = new Map<number, number[]>();
|
||||
|
||||
// Get selected dates from current date to the next year
|
||||
getSelectedDatesFromNow(): SelectedDates {
|
||||
@@ -78,9 +87,10 @@ export class CalendarService {
|
||||
const start = new Date(`${horizon.start}T00:00:00`);
|
||||
const last = new Date(`${horizon.end}T00:00:00`);
|
||||
last.setDate(last.getDate() - 1);
|
||||
const holidays = this.getHolidays(start.getFullYear());
|
||||
const holidays = this.getHolidays(start.getFullYear(), last.getFullYear());
|
||||
const monthInHorizon: Year = [];
|
||||
const cursor = new Date(start.getFullYear(), start.getMonth(), 1);
|
||||
const cursor = new Date(start);
|
||||
cursor.setDate(1);
|
||||
while (cursor <= last) {
|
||||
monthInHorizon.push(
|
||||
CalendarService.weeksInMonth(cursor.getMonth(), cursor.getFullYear()),
|
||||
@@ -128,59 +138,47 @@ export class CalendarService {
|
||||
|
||||
setWeekendSelection(date: Date, selectedDates: SelectedDates) {
|
||||
if (date.getDay() === 6) {
|
||||
const end = new Date(date);
|
||||
end.setDate(end.getDate() + 1);
|
||||
selectedDates._datesSelected.push(
|
||||
new SelectedDate(
|
||||
DayType.WEEKEND,
|
||||
new DateRange<Date>(
|
||||
date,
|
||||
new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1),
|
||||
),
|
||||
),
|
||||
new SelectedDate(DayType.WEEKEND, new DateRange<Date>(date, end)),
|
||||
);
|
||||
}
|
||||
if (date.getDate() === 1 && date.getDay() === 0) {
|
||||
const start = new Date(date);
|
||||
start.setDate(start.getDate() - 1);
|
||||
selectedDates._datesSelected.push(
|
||||
new SelectedDate(
|
||||
DayType.WEEKEND,
|
||||
new DateRange<Date>(
|
||||
new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1),
|
||||
date,
|
||||
),
|
||||
),
|
||||
new SelectedDate(DayType.WEEKEND, new DateRange<Date>(start, date)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getHolidays(startYear = this.dateCache.getCurrentYear()): Date[] {
|
||||
// Cache holidays to avoid recalculation
|
||||
if (this.holidayCacheYear === startYear && this.holidayTimestampsCache) {
|
||||
// Return timestamps directly for efficient lookup
|
||||
return Array.from(this.holidayTimestampsCache).map((ts) => new Date(ts));
|
||||
getHolidays(startYear = this.dateCache.getCurrentYear(), lastYear = startYear + 1): Date[] {
|
||||
const dates: Date[] = [];
|
||||
for (let year = startYear; year <= lastYear; year++) {
|
||||
let timestamps = this.holidaysByYear.get(year);
|
||||
if (!timestamps) {
|
||||
const calendar = new Holidays('FR', allTypesOptions);
|
||||
timestamps = calendar
|
||||
.getHolidays(year, lang)
|
||||
.filter((holiday) => holiday.type === 'public')
|
||||
.map((holiday) => {
|
||||
const dateOnly = String(holiday.date).match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
||||
return (
|
||||
dateOnly ? new Date(`${dateOnly}T00:00:00`) : new Date(holiday.date)
|
||||
).getTime();
|
||||
});
|
||||
this.holidaysByYear.set(year, timestamps);
|
||||
}
|
||||
dates.push(...timestamps.map((timestamp) => new Date(timestamp)));
|
||||
}
|
||||
|
||||
// Logic to return holidays for the given year
|
||||
const hd = new Holidays('FR', allTypesOptions);
|
||||
const holidays = hd
|
||||
.getHolidays(startYear, lang)
|
||||
.filter((holiday) => holiday.type === 'public');
|
||||
const holidaysPlusOne = hd
|
||||
.getHolidays(startYear + 1, lang)
|
||||
.filter((holiday) => holiday.type === 'public');
|
||||
holidays.push(...holidaysPlusOne);
|
||||
const holidaysDate: Date[] = holidays.map((holiday) => {
|
||||
const dateOnly = String(holiday.date).match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
||||
return dateOnly ? new Date(`${dateOnly}T00:00:00`) : new Date(holiday.date);
|
||||
});
|
||||
|
||||
// Update cache
|
||||
this.holidayTimestampsCache = new Set(holidaysDate.map((d) => d.getTime()));
|
||||
this.holidayCacheYear = startYear;
|
||||
|
||||
return holidaysDate;
|
||||
return dates;
|
||||
}
|
||||
|
||||
static daysInWeek(weekIndex: number, monthIndex: number, year: number): Week {
|
||||
const firstDayOfMonth = new Date(year, monthIndex, 1);
|
||||
const firstDayOfMonth = new Date(0);
|
||||
firstDayOfMonth.setFullYear(year, monthIndex, 1);
|
||||
firstDayOfMonth.setHours(0, 0, 0, 0);
|
||||
const firstDayOfWeek = new Date(firstDayOfMonth);
|
||||
let offset = firstDayOfMonth.getDay() - 1; // Adjust to make Monday the first day of the week
|
||||
if (offset < 0) {
|
||||
@@ -357,8 +355,7 @@ export class SelectedDate implements SelectedDateInterface {
|
||||
|
||||
getLengthInDays(): number {
|
||||
if (this.range.start && this.range.end) {
|
||||
const timeDiff = this.range.end.getTime() - this.range.start.getTime();
|
||||
return Math.floor(timeDiff / (1000 * 3600 * 24)) + 1;
|
||||
return dayjs(this.range.end).diff(dayjs(this.range.start), 'day') + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -409,8 +406,6 @@ export class SelectedDates implements SelectedDateInterface {
|
||||
return;
|
||||
}
|
||||
|
||||
const oneDayInMs = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Sort dates by start date
|
||||
const sorted = [...this._datesSelected].sort((a, b) => {
|
||||
const aStart = a.range.start?.getTime() ?? 0;
|
||||
@@ -429,7 +424,9 @@ export class SelectedDates implements SelectedDateInterface {
|
||||
const currEnd = current.range.end?.getTime() ?? 0;
|
||||
|
||||
// Check if current date is adjacent or overlapping with the maximum end of the current group
|
||||
if (currStart - currentGroupMaxEnd <= oneDayInMs) {
|
||||
const nextDay = new Date(currentGroupMaxEnd);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
if (currStart <= nextDay.getTime()) {
|
||||
currentGroup.push(current);
|
||||
currentGroupMaxEnd = Math.max(currentGroupMaxEnd, currEnd);
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user