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

@@ -5,16 +5,11 @@
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;
max-width: 65vw;
width: 100%;
max-width: 100%;
position: relative;
}
@media (max-width: 768px) {
.calendar-year {
max-width: 100vw;
}
}
.loading-overlay {
position: absolute;
top: 0;
@@ -5,8 +5,8 @@ import { CalendarMonth } from '../calendar-month/calendar-month';
import { CalendarService, SelectedDates } from '../../services/calendar-service';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { DateCacheService } from '../../services/date-cache.service';
import { RuleService } from '../../services/rule-service';
@Component({
selector: 'app-calendar-year',
@@ -18,7 +18,7 @@ export class CalendarYear {
private dateCache = inject(DateCacheService);
private userInput = inject(UserInputService);
private calendarService = inject(CalendarService);
private calendarSettingsService = inject(CalendarSettingsService);
private ruleService = inject(RuleService);
protected isLoading = signal(true);
protected loadingProgress = signal(0);
@@ -38,9 +38,11 @@ export class CalendarYear {
this.selectedDates = this.calendarService.getSelectedDatesForYear();
const vacationData = untracked(() => this.userInput.vacationNumberSignal());
const c = untracked(() => this.calendarSettingsService.get('samediMalin')());
console.log('Samedi Malin setting:', c);
const currentRules = untracked(() => this.ruleService.rules);
console.log('Optimizing vacations with data:', vacationData);
console.log('Using rules:', currentRules);
this.isLoading.set(true);
this.loadingProgress.set(0);
// Defer execution to allow UI to update
@@ -51,7 +53,7 @@ export class CalendarYear {
const vacationDaysCopy = vacationData.map((vd) => ({ ...vd }));
const remainingDays = await this.selectedDates.optimizeVacations(
vacationDaysCopy,
this.calendarSettingsService,
currentRules,
this.calendarService,
(progress: number) => {
this.loadingProgress.set(progress);
@@ -0,0 +1,376 @@
<mat-expansion-panel class="rules-panel-container mat-elevation-z2">
<mat-expansion-panel-header>
<mat-panel-title>
<mat-icon>rule</mat-icon>
Règles Personnalisées
</mat-panel-title>
<mat-panel-description> Gérer les règles d'optimisation </mat-panel-description>
</mat-expansion-panel-header>
<div class="priority-statement">
<mat-icon color="primary">info</mat-icon>
<span>L'ordre des règles définit leur priorité d'exécution (de haut en bas). Glissez-déposez pour réorganiser.</span>
</div>
<div class="rules-list" cdkDropList (cdkDropListDropped)="drop($event)">
@for (rule of rules; track rule.id; let i = $index) {
<div class="rule-item" cdkDrag [cdkDragDisabled]="editingRuleId() === rule.id" [class.editing]="editingRuleId() === rule.id">
@if (editingRuleId() !== rule.id) {
<div class="rule-drag-grip">
<mat-icon class="drag-handle-icon">drag_handle</mat-icon>
</div>
<div class="rule-display">
<div class="rule-header">
<mat-slide-toggle
[checked]="rule.enabled"
(change)="toggleRule(rule.id)"
color="primary"
>
<span class="rule-name"><span class="rule-number">{{ i + 1 }}.</span> {{ rule.name }}</span>
</mat-slide-toggle>
<div class="rule-actions">
<button mat-icon-button (click)="startEdit(rule, $event)">
<mat-icon>edit</mat-icon>
</button>
<button
mat-icon-button
color="warn"
(click)="deleteRule(rule.id, $event)"
>
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
<div class="rule-summary">
@if (rule.type === RuleType.IF_THEN) {
<span>
Si un jour férié tombe un
<strong>{{
daysOfWeek[rule.dayOfWeek > 0 ? rule.dayOfWeek - 1 : 6]
.label
}}</strong
>, placer <strong>{{ rule.placeDays }}</strong> jour(s)
<strong>{{ getDayTypeLabel(rule.placeType) }}</strong>
<strong>
{{
placementOptions.find((p) => p.value === rule.placement)
?.label | lowercase
}}</strong
>
et obtenir <strong>{{ rule.rewardDays }}</strong> jour(s)
<strong>{{ getDayTypeLabel(rule.rewardType) }}</strong> en
bonus.
</span>
} @else if (rule.type === RuleType.TIME_WINDOW) {
<span>
Placer <strong>{{ rule.placeDays }}</strong> jour(s)
<strong>{{ getDayTypeLabel(rule.placeType) }}</strong> entre
le <strong>{{ rule.startDate | date: 'dd/MM/yyyy' }}</strong> et
le <strong>{{ rule.endDate | date: 'dd/MM/yyyy' }}</strong
>.
</span>
}
</div>
</div>
} @else {
<div class="rule-editor">
<h4>Modifier la règle</h4>
<mat-form-field class="full-width">
<mat-label>Nom de la règle</mat-label>
<input matInput [(ngModel)]="$any(editBuffer).name" required />
</mat-form-field>
@if ($any(editBuffer).type === RuleType.IF_THEN) {
<div class="editor-row">
<mat-form-field>
<mat-label>Jour férié tombe un</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).dayOfWeek">
@for (day of daysOfWeek; track day.value) {
<mat-option [value]="day.value">{{
day.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">Placer</span>
<mat-form-field class="small-input">
<mat-label>Jours</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).placeDays"
min="1"
/>
</mat-form-field>
<mat-form-field>
<mat-label>Type</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placeType">
@for (type of placeDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Position</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placement">
@for (opt of placementOptions; track opt.value) {
<mat-option [value]="opt.value">{{
opt.label
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">Obtenir en bonus</span>
<mat-form-field class="small-input">
<mat-label>Jours</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).rewardDays"
min="0"
/>
</mat-form-field>
<mat-form-field>
<mat-label>Type</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).rewardType">
@for (type of rewardDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
} @else {
<div class="editor-row">
<mat-form-field>
<mat-label>Date de début</mat-label>
<input
matInput
[matDatepicker]="startPicker"
[(ngModel)]="$any(editBuffer).startDate"
/>
<mat-datepicker-toggle
matSuffix
[for]="startPicker"
></mat-datepicker-toggle>
<mat-datepicker #startPicker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>Date de fin</mat-label>
<input
matInput
[matDatepicker]="endPicker"
[(ngModel)]="$any(editBuffer).endDate"
/>
<mat-datepicker-toggle
matSuffix
[for]="endPicker"
></mat-datepicker-toggle>
<mat-datepicker #endPicker></mat-datepicker>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">Placer</span>
<mat-form-field class="small-input">
<mat-label>Jours</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).placeDays"
min="1"
/>
</mat-form-field>
<mat-form-field>
<mat-label>Type</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placeType">
@for (type of placeDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
<div class="editor-actions">
<button mat-button (click)="cancelEdit()">Annuler</button>
<button mat-flat-button color="primary" (click)="saveEdit()">
Enregistrer
</button>
</div>
</div>
}
</div>
}
@if (isAddingNew()) {
<div class="rule-item editing">
<div class="rule-editor">
<h4>Ajouter une règle</h4>
<mat-form-field class="full-width">
<mat-label>Nom de la règle</mat-label>
<input matInput [(ngModel)]="$any(editBuffer).name" required />
</mat-form-field>
@if (newRuleType() === RuleType.IF_THEN) {
<div class="editor-row">
<mat-form-field>
<mat-label>Jour férié tombe un</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).dayOfWeek">
@for (day of daysOfWeek; track day.value) {
<mat-option [value]="day.value">{{ day.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">Placer</span>
<mat-form-field class="small-input">
<mat-label>Jours</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).placeDays"
min="1"
/>
</mat-form-field>
<mat-form-field>
<mat-label>Type</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placeType">
@for (type of placeDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field>
<mat-label>Position</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placement">
@for (opt of placementOptions; track opt.value) {
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">Obtenir en bonus</span>
<mat-form-field class="small-input">
<mat-label>Jours</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).rewardDays"
min="0"
/>
</mat-form-field>
<mat-form-field>
<mat-label>Type</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).rewardType">
@for (type of rewardDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
} @else {
<div class="editor-row">
<mat-form-field>
<mat-label>Date de début</mat-label>
<input
matInput
[matDatepicker]="startPicker"
[(ngModel)]="$any(editBuffer).startDate"
/>
<mat-datepicker-toggle
matSuffix
[for]="startPicker"
></mat-datepicker-toggle>
<mat-datepicker #startPicker></mat-datepicker>
</mat-form-field>
<mat-form-field>
<mat-label>Date de fin</mat-label>
<input
matInput
[matDatepicker]="endPicker"
[(ngModel)]="$any(editBuffer).endDate"
/>
<mat-datepicker-toggle
matSuffix
[for]="endPicker"
></mat-datepicker-toggle>
<mat-datepicker #endPicker></mat-datepicker>
</mat-form-field>
</div>
<div class="editor-row">
<span class="action-text">Placer</span>
<mat-form-field class="small-input">
<mat-label>Jours</mat-label>
<input
matInput
type="number"
[(ngModel)]="$any(editBuffer).placeDays"
min="1"
/>
</mat-form-field>
<mat-form-field>
<mat-label>Type</mat-label>
<mat-select [(ngModel)]="$any(editBuffer).placeType">
@for (type of placeDayTypes; track type) {
<mat-option [value]="type">{{
getDayTypeLabel(type)
}}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
}
<div class="editor-actions">
<button mat-button (click)="cancelEdit()">Annuler</button>
<button mat-flat-button color="primary" (click)="saveEdit()">
Ajouter
</button>
</div>
</div>
</div>
}
<div class="add-buttons" [class.hidden]="isAddingNew() || editingRuleId()">
<button mat-stroked-button (click)="startAddNew(RuleType.IF_THEN)">
<mat-icon>add</mat-icon> Nouvelle Règle "Si ... Alors"
</button>
<button mat-stroked-button (click)="startAddNew(RuleType.TIME_WINDOW)">
<mat-icon>add</mat-icon> Nouvelle Période Imposée
</button>
</div>
</div>
</mat-expansion-panel>
@@ -0,0 +1,217 @@
.rules-panel-container {
margin-top: 1rem;
margin-bottom: 1rem;
border-radius: 12px;
overflow: hidden;
container-type: inline-size;
mat-panel-title {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
@container (max-width: 480px) {
mat-panel-description {
display: none !important;
}
}
}
.rules-list {
display: flex;
flex-direction: column;
gap: 12px;
padding-top: 8px;
}
.rule-item {
border: 1px solid var(--mat-sys-outline-variant);
border-radius: 8px;
transition: all 0.2s ease;
background: var(--mat-sys-surface-container);
&:not(.editing) {
cursor: grab;
&:active {
cursor: grabbing;
}
}
&:hover:not(.editing) {
border-color: var(--mat-sys-outline);
}
&.editing {
border: 1px solid var(--mat-sys-primary);
}
}
.cdk-drag-preview {
box-sizing: border-box;
border-radius: 8px;
box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2),
0 8px 10px 1px rgba(0, 0, 0, 0.14),
0 3px 14px 2px rgba(0, 0, 0, 0.12);
}
.cdk-drag-placeholder {
opacity: 0;
}
.cdk-drag-animating {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.rules-list.cdk-drop-list-dragging .rule-item:not(.cdk-drag-placeholder) {
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
}
.rule-display {
padding: 16px;
}
.priority-statement {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
background: var(--mat-sys-primary-container);
color: var(--mat-sys-on-primary-container);
border-radius: 8px;
font-size: 0.95rem;
margin-top: 8px;
margin-bottom: 8px;
mat-icon {
color: var(--mat-sys-primary);
flex-shrink: 0;
}
}
.rule-drag-grip {
display: flex;
justify-content: center;
align-items: center;
padding-top: 8px;
margin-bottom: -8px;
color: var(--mat-sys-outline);
opacity: 0.6;
cursor: grab;
transition: opacity 0.2s;
.rule-item:active & {
cursor: grabbing;
}
&:hover {
opacity: 1;
}
}
.rule-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.rule-name {
font-weight: 600;
font-size: 1.05rem;
display: flex;
align-items: center;
gap: 6px;
.rule-number {
color: var(--mat-sys-primary);
font-weight: 700;
}
}
.rule-actions {
display: flex;
gap: 4px;
opacity: 0.6;
transition: opacity 0.2s;
.rule-item:hover & {
opacity: 1;
}
}
}
.rule-summary {
font-size: 0.95rem;
line-height: 1.5;
padding-left: 48px; /* Align with toggle switch text */
strong {
font-weight: 600;
}
}
.rule-editor {
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
background: var(--mat-sys-surface-container-high);
border-radius: 8px;
h4 {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
}
.editor-row {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
.action-text {
font-weight: 500;
margin-right: 4px;
}
mat-form-field {
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none;
}
}
.small-input {
width: 100px;
}
}
.full-width {
width: 100%;
}
.editor-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 8px;
}
}
.add-buttons {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 8px;
&.hidden {
display: none;
}
button {
border-radius: 24px;
width: 100%;
}
}
@@ -0,0 +1,103 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { provideZonelessChangeDetection } from '@angular/core';
import { RulesPanel } from './rules-panel';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RuleService, RuleType } from '../../services/rule-service';
describe('RulesPanel', () => {
let component: RulesPanel;
let fixture: ComponentFixture<RulesPanel>;
beforeEach(async () => {
// Clear localStorage before testing
localStorage.clear();
await TestBed.configureTestingModule({
imports: [RulesPanel, BrowserAnimationsModule],
providers: [provideZonelessChangeDetection(), RuleService],
}).compileComponents();
fixture = TestBed.createComponent(RulesPanel);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should display initial rules from service', () => {
expect(component.rules.length).toBe(2);
});
it('should start adding new If-Then rule', () => {
component.startAddNew(RuleType.IF_THEN);
expect(component['isAddingNew']()).toBeTrue();
expect(component['newRuleType']()).toBe(RuleType.IF_THEN);
expect(component['editBuffer'].type).toBe(RuleType.IF_THEN);
expect(component['editBuffer'].name).toBe('Nouvelle Règle');
});
it('should start adding new Time Window rule', () => {
component.startAddNew(RuleType.TIME_WINDOW);
expect(component['isAddingNew']()).toBeTrue();
expect(component['newRuleType']()).toBe(RuleType.TIME_WINDOW);
expect(component['editBuffer'].type).toBe(RuleType.TIME_WINDOW);
expect(component['editBuffer'].name).toBe('Nouvelle Période');
});
it('should cancel edit', () => {
component.startAddNew(RuleType.IF_THEN);
component.cancelEdit();
expect(component['isAddingNew']()).toBeFalse();
expect(component['editingRuleId']()).toBeNull();
expect(Object.keys(component['editBuffer']).length).toBe(0);
});
it('should save a new rule', () => {
const initialCount = component.rules.length;
component.startAddNew(RuleType.IF_THEN);
component['editBuffer'].name = 'Custom Test Rule';
component.saveEdit();
expect(component.rules.length).toBe(initialCount + 1);
expect(component.rules.some((r) => r.name === 'Custom Test Rule')).toBeTrue();
});
it('should update an existing rule', () => {
const targetRule = component.rules[0];
// Create a mock event with stopPropagation
const mockEvent = { stopPropagation: () => {} } as Event;
component.startEdit(targetRule, mockEvent);
component['editBuffer'].name = 'Updated Name';
component.saveEdit();
expect(component.rules.find((r) => r.id === targetRule.id)?.name).toBe('Updated Name');
});
it('should delete a rule', () => {
const targetId = component.rules[0].id;
const initialCount = component.rules.length;
const mockEvent = { stopPropagation: () => {} } as Event;
component.deleteRule(targetId, mockEvent);
expect(component.rules.length).toBe(initialCount - 1);
expect(component.rules.some((r) => r.id === targetId)).toBeFalse();
});
it('should toggle a rule', () => {
const targetRule = component.rules[0];
const initialState = targetRule.enabled;
component.toggleRule(targetRule.id);
expect(component.rules.find((r) => r.id === targetRule.id)?.enabled).toBe(!initialState);
});
});
@@ -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);
}
}
@@ -24,6 +24,7 @@
</button>
<app-calendar-settings></app-calendar-settings>
</div>
<app-rules-panel></app-rules-panel>
@if (hasRemainingDays()) {
<mat-card class="mat-elevation-z2 warning-card">
<mat-card-content class="warning-content">
@@ -2,6 +2,7 @@ import { Component, computed, inject, ChangeDetectionStrategy } from '@angular/c
import { MatInputModule } from '@angular/material/input';
import { UserInputService } from '../../services/user-input-service';
import { CalendarSettings } from '../calendar-settings/calendar-settings';
import { RulesPanel } from '../rules-panel/rules-panel';
import { MatCard, MatCardContent } from '@angular/material/card';
import { MatIcon } from '@angular/material/icon';
import { DayInput } from '../userInput/day-input/day-input';
@@ -13,6 +14,7 @@ import { tooltipTypeMapping, DayType, VacDay } from '../../services/calendar-ser
imports: [
MatInputModule,
CalendarSettings,
RulesPanel,
MatCard,
MatCardContent,
MatIcon,
@@ -1,3 +1,9 @@
:host {
display: block;
container-type: inline-size;
width: 100%;
}
.card {
display: flex;
flex-direction: row;
@@ -18,3 +24,49 @@
::ng-deep .mat-mdc-form-field-subscript-wrapper {
display: none;
}
@container (max-width: 480px) {
.card {
flex-wrap: wrap;
gap: 12px;
padding: 12px;
align-items: center;
}
.numberInput {
width: 70px;
flex-grow: 0;
}
.dayTypeInput {
flex-grow: 1;
width: 0;
min-width: 120px;
}
.dateInput {
width: 100%;
flex-grow: 1;
flex-basis: 100%;
margin-top: 4px;
}
button[matFab] {
width: 40px;
height: 40px;
min-width: 40px;
line-height: 40px;
::ng-deep .mat-mdc-button-touch-target {
width: 40px;
height: 40px;
}
mat-icon {
margin: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
}
@@ -85,7 +85,9 @@ export class DayInput {
public dayChange = output<VacDay>();
public deleteEntry = output<void>();
protected readonly selectableDayTypes = Object.values(SelectableDayType);
protected readonly selectableDayTypes = Object.values(SelectableDayType).filter(
(t) => t !== SelectableDayType.ANY,
);
protected readonly tooltipTypeMapping = tooltipTypeMapping;
onInput(event: Event) {