feat: implement rules engine and management panel for custom holiday optimization

This commit is contained in:
nessar committed 2026-05-30 15:00:07 +02:00
1 parent c0549de4dd
commit 08ee84e5c9
20 files changed
+3951 -16847

No files matched your search

@@ -0,0 +1,188 @@
import { Component, ChangeDetectionStrategy, inject, signal } from '@angular/core';
import {
RuleService,
RuleType,
IfThenRule,
TimeWindowRule,
VacationRule,
PlacementStrategy,
} from '../../services/rule-service';
import { SelectableDayType, tooltipTypeMapping, DayType } from '../../services/calendar-service';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { FormsModule } from '@angular/forms';
import { DatePipe, LowerCasePipe } from '@angular/common';
import {
DateAdapter,
MAT_DATE_FORMATS,
MAT_DATE_LOCALE,
NativeDateAdapter,
} from '@angular/material/core';
import { CdkDragDrop, CdkDropList, CdkDrag } from '@angular/cdk/drag-drop';
class CustomDateAdapter extends NativeDateAdapter {
override format(date: Date, displayFormat: unknown): string {
if (displayFormat === 'input') {
const day = date.getDate().toString().padStart(2, '0');
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
return super.format(date, displayFormat as object);
}
}
const MY_DATE_FORMATS = {
parse: { dateInput: 'input' },
display: {
dateInput: 'input',
monthYearLabel: { year: 'numeric', month: 'short' },
dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' },
monthYearA11yLabel: { year: 'numeric', month: 'long' },
},
};
@Component({
selector: 'app-rules-panel',
imports: [
MatExpansionModule,
MatIconModule,
MatButtonModule,
MatSlideToggleModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatDatepickerModule,
FormsModule,
DatePipe,
LowerCasePipe,
CdkDropList,
CdkDrag,
],
templateUrl: './rules-panel.html',
styleUrl: './rules-panel.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{ provide: DateAdapter, useClass: CustomDateAdapter },
{ provide: MAT_DATE_LOCALE, useValue: 'en-GB' },
{ provide: MAT_DATE_FORMATS, useValue: MY_DATE_FORMATS },
],
})
export class RulesPanel {
public ruleService = inject(RuleService);
protected readonly RuleType = RuleType;
protected readonly placeDayTypes = Object.values(SelectableDayType);
protected readonly rewardDayTypes = Object.values(SelectableDayType).filter(
(t) => t !== SelectableDayType.ANY,
);
protected readonly tooltipTypeMapping = tooltipTypeMapping;
protected readonly daysOfWeek = [
{ value: 1, label: 'Lundi' },
{ value: 2, label: 'Mardi' },
{ value: 3, label: 'Mercredi' },
{ value: 4, label: 'Jeudi' },
{ value: 5, label: 'Vendredi' },
{ value: 6, label: 'Samedi' },
{ value: 0, label: 'Dimanche' },
];
protected readonly placementOptions: { value: PlacementStrategy; label: string }[] = [
{ value: 'before', label: ' Avant' },
{ value: 'after', label: ' Après' },
{ value: 'around', label: ' Autour' },
{ value: 'auto', label: ' Auto (Meilleur choix)' },
];
protected editingRuleId = signal<string | null>(null);
protected isAddingNew = signal(false);
protected newRuleType = signal<RuleType>(RuleType.IF_THEN);
protected editBuffer: Partial<VacationRule> = {};
public get rules() {
return this.ruleService.rules;
}
public drop(event: CdkDragDrop<VacationRule[]>) {
if (event.previousIndex !== event.currentIndex) {
this.ruleService.reorderRules(event.previousIndex, event.currentIndex);
}
}
public toggleRule(id: string) {
this.ruleService.toggleRule(id);
}
public startEdit(rule: VacationRule, event: Event) {
event.stopPropagation();
this.editingRuleId.set(rule.id);
this.isAddingNew.set(false);
this.editBuffer = { ...rule };
}
public cancelEdit() {
this.editingRuleId.set(null);
this.isAddingNew.set(false);
this.editBuffer = {};
}
public saveEdit() {
if (this.editBuffer.id) {
this.ruleService.updateRule(this.editBuffer as VacationRule);
} else {
this.ruleService.addRule({
...this.editBuffer,
id: crypto.randomUUID(),
enabled: true,
} as VacationRule);
}
this.cancelEdit();
}
public deleteRule(id: string, event: Event) {
event.stopPropagation();
this.ruleService.deleteRule(id);
}
public startAddNew(type: RuleType) {
this.isAddingNew.set(true);
this.editingRuleId.set(null);
this.newRuleType.set(type);
if (type === RuleType.IF_THEN) {
this.editBuffer = {
name: 'Nouvelle Règle',
type: RuleType.IF_THEN,
dayOfWeek: 6,
placeDays: 2,
placeType: SelectableDayType.CP,
placement: 'auto',
rewardDays: 1,
rewardType: SelectableDayType.OTHER,
} as Partial<IfThenRule>;
} else {
this.editBuffer = {
name: 'Nouvelle Période',
type: RuleType.TIME_WINDOW,
startDate: new Date(),
endDate: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
placeDays: 5,
placeType: SelectableDayType.CP,
} as Partial<TimeWindowRule>;
}
}
protected getDayTypeLabel(type: string | undefined | null): string {
if (!type) return '';
return tooltipTypeMapping[type as unknown as DayType] || String(type);
}
}