feat: implement rules engine and management panel for custom holiday optimization
This commit is contained in:
1 parent
c0549de4dd
commit
08ee84e5c9
20 files changed
+3951
-16847
No files matched your search
@@ -1,7 +1,15 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideZonelessChangeDetection } from '@angular/core';
|
||||
import { CalendarService, DayType, SelectedDate, SelectedDates } from './calendar-service';
|
||||
import {
|
||||
CalendarService,
|
||||
DayType,
|
||||
SelectedDate,
|
||||
SelectedDates,
|
||||
SelectableDayType,
|
||||
VacDay,
|
||||
} from './calendar-service';
|
||||
import { DateRange } from '@angular/material/datepicker';
|
||||
import { IfThenRule, TimeWindowRule, RuleType } from './rule-service';
|
||||
|
||||
describe('CalendarService', () => {
|
||||
let service: CalendarService;
|
||||
@@ -202,4 +210,254 @@ describe('SelectedDates', () => {
|
||||
const tooltip = selectedDates.getTooltip(date);
|
||||
expect(tooltip).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('optimizeVacations and rules engine', () => {
|
||||
let calendarService: CalendarService;
|
||||
let selectedDates: SelectedDates;
|
||||
|
||||
beforeEach(() => {
|
||||
calendarService = TestBed.inject(CalendarService);
|
||||
selectedDates = new SelectedDates();
|
||||
});
|
||||
|
||||
it('should execute rules sequentially in order', async () => {
|
||||
const vacationDays: VacDay[] = [];
|
||||
const rules: (TimeWindowRule | IfThenRule)[] = [
|
||||
{ id: '1', type: RuleType.IF_THEN, enabled: true } as IfThenRule,
|
||||
{ id: '2', type: RuleType.TIME_WINDOW, enabled: true } as TimeWindowRule,
|
||||
{ id: '3', type: RuleType.IF_THEN, enabled: true } as IfThenRule,
|
||||
];
|
||||
|
||||
const applyTimeWindowRulesSpy = spyOn(selectedDates, 'applyTimeWindowRules');
|
||||
const applyIfThenRulesSpy = spyOn(selectedDates, 'applyIfThenRules');
|
||||
|
||||
await selectedDates.optimizeVacations(vacationDays, rules, calendarService);
|
||||
|
||||
expect(applyIfThenRulesSpy.calls.count()).toBe(2);
|
||||
expect(applyTimeWindowRulesSpy.calls.count()).toBe(1);
|
||||
|
||||
// First rule is IF_THEN
|
||||
expect(applyIfThenRulesSpy.calls.argsFor(0)[1][0].id).toBe('1');
|
||||
// Second rule is TIME_WINDOW
|
||||
expect(applyTimeWindowRulesSpy.calls.argsFor(0)[1][0].id).toBe('2');
|
||||
// Third rule is IF_THEN
|
||||
expect(applyIfThenRulesSpy.calls.argsFor(1)[1][0].id).toBe('3');
|
||||
});
|
||||
|
||||
it('should apply time window rules', () => {
|
||||
const futureYear = new Date().getFullYear() + 2;
|
||||
const vacationDays: VacDay[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: SelectableDayType.CP,
|
||||
numberOfDays: 5,
|
||||
expiryDate: new Date(futureYear + 1, 11, 31),
|
||||
},
|
||||
];
|
||||
const rules: TimeWindowRule[] = [
|
||||
{
|
||||
id: 't1',
|
||||
name: 'Time Window',
|
||||
type: RuleType.TIME_WINDOW,
|
||||
enabled: true,
|
||||
startDate: new Date(futureYear, 11, 20),
|
||||
endDate: new Date(futureYear, 11, 25),
|
||||
placeDays: 3,
|
||||
placeType: SelectableDayType.CP,
|
||||
},
|
||||
];
|
||||
|
||||
// Mock monthsInAYearFromNow to return a controlled range of days
|
||||
spyOn(calendarService, 'monthsInAYearFromNow').and.returnValue([
|
||||
[
|
||||
[
|
||||
new Date(futureYear, 11, 20),
|
||||
new Date(futureYear, 11, 21),
|
||||
new Date(futureYear, 11, 22),
|
||||
new Date(futureYear, 11, 23),
|
||||
new Date(futureYear, 11, 24),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
selectedDates.applyTimeWindowRules(vacationDays, rules, calendarService);
|
||||
|
||||
// Should have placed 3 days
|
||||
const placedCount = selectedDates._datesSelected.filter(
|
||||
(d) => d.type === DayType.CP,
|
||||
).length;
|
||||
expect(placedCount).toBe(3);
|
||||
// Should have consumed 3 days from the pool
|
||||
expect(vacationDays[0].numberOfDays).toBe(2);
|
||||
});
|
||||
|
||||
it('should apply if-then rules', () => {
|
||||
const futureYear = new Date().getFullYear() + 2;
|
||||
const vacationDays: VacDay[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: SelectableDayType.CP,
|
||||
numberOfDays: 5,
|
||||
expiryDate: new Date(futureYear + 1, 11, 31),
|
||||
},
|
||||
];
|
||||
const rules: IfThenRule[] = [
|
||||
{
|
||||
id: 'i1',
|
||||
name: 'Samedi Malin',
|
||||
type: RuleType.IF_THEN,
|
||||
enabled: true,
|
||||
dayOfWeek: 6, // Saturday
|
||||
placeDays: 2,
|
||||
placeType: SelectableDayType.CP,
|
||||
placement: 'auto',
|
||||
rewardDays: 1,
|
||||
rewardType: SelectableDayType.OTHER,
|
||||
},
|
||||
];
|
||||
|
||||
// Setup a mock holiday on a Saturday
|
||||
// Use a date far in the future so it doesn't fail the "past date" check
|
||||
const saturdayHoliday = new Date(new Date().getFullYear() + 2, 11, 5); // Dec 5 is usually Saturday or close, let's just make sure we get a Saturday
|
||||
// To guarantee it's a Saturday for the test:
|
||||
while (saturdayHoliday.getDay() !== 6) {
|
||||
saturdayHoliday.setDate(saturdayHoliday.getDate() + 1);
|
||||
}
|
||||
selectedDates._datesSelected.push(
|
||||
new SelectedDate(
|
||||
DayType.CLOSED_DAY,
|
||||
new DateRange<Date>(saturdayHoliday, saturdayHoliday),
|
||||
),
|
||||
);
|
||||
|
||||
selectedDates.applyIfThenRules(vacationDays, rules);
|
||||
|
||||
// Should have placed 2 CP days (e.g. Thu/Fri, Mon/Tue, or Fri/Mon)
|
||||
const placedCP = selectedDates._datesSelected.filter(
|
||||
(d) => d.type === DayType.CP,
|
||||
).length;
|
||||
expect(placedCP).toBe(2);
|
||||
|
||||
// Should have consumed 2 CP days
|
||||
expect(vacationDays[0].numberOfDays).toBe(3);
|
||||
|
||||
// Should have added 1 OTHER reward day
|
||||
const rewardDay = vacationDays.find((vd) => vd.type === SelectableDayType.OTHER);
|
||||
expect(rewardDay).toBeDefined();
|
||||
expect(rewardDay?.numberOfDays).toBe(1);
|
||||
});
|
||||
|
||||
it('should apply time window rules with ANY type', () => {
|
||||
const futureYear = new Date().getFullYear() + 2;
|
||||
const vacationDays: VacDay[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: SelectableDayType.CP,
|
||||
numberOfDays: 2,
|
||||
expiryDate: new Date(futureYear, 11, 31),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: SelectableDayType.RTT,
|
||||
numberOfDays: 2,
|
||||
expiryDate: new Date(futureYear - 1, 11, 31), // Expires earlier
|
||||
},
|
||||
];
|
||||
const rules: TimeWindowRule[] = [
|
||||
{
|
||||
id: 'tw1',
|
||||
name: 'Vacances',
|
||||
type: RuleType.TIME_WINDOW,
|
||||
enabled: true,
|
||||
startDate: new Date(futureYear, 6, 1),
|
||||
endDate: new Date(futureYear, 6, 15),
|
||||
placeDays: 3,
|
||||
placeType: SelectableDayType.ANY,
|
||||
},
|
||||
];
|
||||
|
||||
spyOn(calendarService, 'monthsInAYearFromNow').and.returnValue([
|
||||
[
|
||||
[
|
||||
new Date(futureYear, 6, 1),
|
||||
new Date(futureYear, 6, 2),
|
||||
new Date(futureYear, 6, 3),
|
||||
new Date(futureYear, 6, 4),
|
||||
new Date(futureYear, 6, 5),
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
selectedDates.applyTimeWindowRules(vacationDays, rules, calendarService);
|
||||
|
||||
// Should have placed 3 days total: 2 RTT (earlier expiry) and 1 CP
|
||||
const placedRTT = selectedDates._datesSelected.filter(
|
||||
(d) => d.type === DayType.RTT,
|
||||
).length;
|
||||
const placedCP = selectedDates._datesSelected.filter(
|
||||
(d) => d.type === DayType.CP,
|
||||
).length;
|
||||
expect(placedRTT).toBe(2);
|
||||
expect(placedCP).toBe(1);
|
||||
|
||||
// Should have consumed 2 RTT and 1 CP
|
||||
expect(vacationDays[0].numberOfDays).toBe(1); // CP remaining
|
||||
expect(vacationDays[1].numberOfDays).toBe(0); // RTT remaining
|
||||
});
|
||||
|
||||
it('should apply if-then rules with ANY type', () => {
|
||||
const futureYear = new Date().getFullYear() + 2;
|
||||
const vacationDays: VacDay[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: SelectableDayType.CP,
|
||||
numberOfDays: 2,
|
||||
expiryDate: new Date(futureYear + 2, 11, 31),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: SelectableDayType.RTT,
|
||||
numberOfDays: 2,
|
||||
expiryDate: new Date(futureYear + 1, 11, 31), // Expires earlier
|
||||
},
|
||||
];
|
||||
const rules: IfThenRule[] = [
|
||||
{
|
||||
id: 'i1',
|
||||
name: 'Rule',
|
||||
type: RuleType.IF_THEN,
|
||||
enabled: true,
|
||||
dayOfWeek: 6, // Saturday
|
||||
placeDays: 3,
|
||||
placeType: SelectableDayType.ANY,
|
||||
placement: 'auto',
|
||||
rewardDays: 0,
|
||||
rewardType: SelectableDayType.CP,
|
||||
},
|
||||
];
|
||||
|
||||
const saturdayHoliday = new Date(new Date().getFullYear() + 2, 11, 5);
|
||||
while (saturdayHoliday.getDay() !== 6) {
|
||||
saturdayHoliday.setDate(saturdayHoliday.getDate() + 1);
|
||||
}
|
||||
selectedDates._datesSelected.push(
|
||||
new SelectedDate(
|
||||
DayType.CLOSED_DAY,
|
||||
new DateRange<Date>(saturdayHoliday, saturdayHoliday),
|
||||
),
|
||||
);
|
||||
|
||||
selectedDates.applyIfThenRules(vacationDays, rules);
|
||||
|
||||
// Should have placed 3 days total: 2 RTT (earlier expiry) and 1 CP
|
||||
const placedRTT = selectedDates._datesSelected.filter(
|
||||
(d) => d.type === DayType.RTT,
|
||||
).length;
|
||||
const placedCP = selectedDates._datesSelected.filter(
|
||||
(d) => d.type === DayType.CP,
|
||||
).length;
|
||||
expect(placedRTT).toBe(2);
|
||||
expect(placedCP).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user