('input[name="lastPlanningDate"]')!;
- start.value = '2024-02-29';
+ start.value = '29/02/2024';
start.dispatchEvent(new Event('input'));
- last.value = '2024-03-08';
+ last.value = '08/03/2024';
last.dispatchEvent(new Event('input'));
await fixture.whenStable();
expect(calculate).toHaveBeenCalledOnce();
@@ -379,10 +379,10 @@ describe('Application Settings', () => {
await revisitApp(true);
expect(fixture.nativeElement.querySelector('input[name="planningStart"]').value).toBe(
- '2024-02-29',
+ '29/02/2024',
);
expect(fixture.nativeElement.querySelector('input[name="lastPlanningDate"]').value).toBe(
- '2024-03-08',
+ '08/03/2024',
);
});
diff --git a/src/app/components/user-input/user-input.html b/src/app/components/user-input/user-input.html
index 82af8c7..4e4de88 100644
--- a/src/app/components/user-input/user-input.html
+++ b/src/app/components/user-input/user-input.html
@@ -14,41 +14,45 @@
{{ tr().settings.planningStart }}
+
+
{{ tr().settings.lastPlanningDate }}
+
+
@if (calendarSettings.planningHorizonError(); as error) {
diff --git a/src/app/components/user-input/user-input.spec.ts b/src/app/components/user-input/user-input.spec.ts
index 0f82465..51cc6e6 100644
--- a/src/app/components/user-input/user-input.spec.ts
+++ b/src/app/components/user-input/user-input.spec.ts
@@ -43,6 +43,7 @@ describe('UserInput', () => {
component = fixture.componentInstance;
userInputService = TestBed.inject(UserInputService);
fixture.detectChanges();
+ await fixture.whenStable();
}
async function revisit(): Promise {
@@ -200,10 +201,44 @@ describe('UserInput', () => {
expect(userInputService.computeTriggerSignal()).toBe(0);
});
- it('prevents calculation while planning dates are invalid and re-enables it after correction', () => {
+ it('saves both calendar selections as inclusive planning dates without recalculating', async () => {
+ const settings = TestBed.inject(CalendarSettingsService);
+ settings.setPlanningDates('2024-02-29', '2024-03-08');
+ TestBed.inject(TranslationService).setLanguage('fr');
+ await fixture.whenStable();
+ const toggles = fixture.nativeElement.querySelectorAll(
+ '.planning-dates mat-datepicker-toggle button',
+ );
+ expect(toggles).toHaveLength(2);
+ for (const [index, day] of ['27', '9'].entries()) {
+ toggles[index].click();
+ await fixture.whenStable();
+ const calendar = document.querySelector('mat-calendar')!;
+ if (index === 0)
+ expect(
+ calendar
+ .querySelector('.mat-calendar-period-button')
+ ?.textContent?.toLowerCase(),
+ ).toContain('févr');
+ Array.from(calendar.querySelectorAll('.mat-calendar-body-cell'))
+ .find((cell) => cell.textContent?.trim() === day)!
+ .click();
+ await fixture.whenStable();
+ await expect.poll(() => document.querySelector('mat-calendar')).toBeNull();
+ }
+ expect(settings.planningHorizon()).toEqual({ start: '2024-02-27', end: '2024-03-10' });
+ expect(JSON.parse(localStorage.getItem('calendarSettings.planningDates')!)).toEqual({
+ start: '2024-02-27',
+ lastPlanningDate: '2024-03-09',
+ });
+ expect(userInputService.computeTriggerSignal()).toBe(0);
+ });
+
+ it('prevents calculation while planning dates are invalid and re-enables it after correction', async () => {
const settings = TestBed.inject(CalendarSettingsService);
settings.setPlanningDates('2024-03-01', '2024-03-01');
- editInput('input[name="lastPlanningDate"]', '2024-02-29');
+ await fixture.whenStable();
+ editInput('input[name="lastPlanningDate"]', '29/02/2024');
const root = fixture.nativeElement as HTMLElement;
const calculate = root.querySelector('.calculate-button')!;
expect(calculate.disabled).toBe(true);
@@ -212,18 +247,23 @@ describe('UserInput', () => {
calculate.click();
component.triggerComputation();
expect(userInputService.computeTriggerSignal()).toBe(0);
- editInput('input[name="planningStart"]', '2024-02-29');
+ editInput('input[name="planningStart"]', '29/02/2024');
expect(calculate.disabled).toBe(false);
calculate.click();
expect(userInputService.computeTriggerSignal()).toBe(1);
});
- it.each(['', '2024-02-30', 'not-a-date'])(
+ it.each(['', '30/02/2024', 'not-a-date', '2'])(
'reports an invalid planning date without saving it: %s',
- (value) => {
+ async (value) => {
const settings = TestBed.inject(CalendarSettingsService);
settings.setPlanningDates('2024-02-29', '2027-02-28');
+ await fixture.whenStable();
editInput('input[name="planningStart"]', value);
+ await fixture.whenStable();
+ expect(fixture.nativeElement.querySelector('input[name="planningStart"]').value).toBe(
+ value,
+ );
expect(
fixture.nativeElement.querySelector('#planning-dates-error')?.textContent,
).toContain('two valid calendar dates');
@@ -235,22 +275,22 @@ describe('UserInput', () => {
},
);
- it('edits inclusive dates with inline validation while preserving the saved planning horizon', () => {
+ it('edits inclusive dates with inline validation while preserving the saved planning horizon', async () => {
const settings = TestBed.inject(CalendarSettingsService);
settings.setPlanningDates('2024-02-29', '2027-02-28');
- fixture.detectChanges();
+ await fixture.whenStable();
const root = fixture.nativeElement as HTMLElement;
const start = root.querySelector('input[name="planningStart"]')!;
const last = root.querySelector('input[name="lastPlanningDate"]')!;
- expect(start.value).toBe('2024-02-29');
- expect(last.value).toBe('2027-02-28');
+ expect(start.value).toBe('29/02/2024');
+ expect(last.value).toBe('28/02/2027');
expect(root.textContent).toContain('Last planning date (inclusive)');
- editInput('input[name="lastPlanningDate"]', '2024-02-28');
+ editInput('input[name="lastPlanningDate"]', '28/02/2024');
expect(root.querySelector('#planning-dates-error')?.textContent).toContain('on or after');
expect(last.getAttribute('aria-invalid')).toBe('true');
expect(last.getAttribute('aria-describedby')).toContain('planning-dates-error');
expect(settings.planningHorizon()).toBeNull();
- editInput('input[name="lastPlanningDate"]', '2024-02-29');
+ editInput('input[name="lastPlanningDate"]', '29/02/2024');
expect(root.querySelector('#planning-dates-error')).toBeNull();
expect(last.getAttribute('aria-invalid')).toBe('false');
expect(last.getAttribute('aria-describedby')).not.toContain('planning-dates-error');
diff --git a/src/app/components/user-input/user-input.ts b/src/app/components/user-input/user-input.ts
index cc99140..4c18dde 100644
--- a/src/app/components/user-input/user-input.ts
+++ b/src/app/components/user-input/user-input.ts
@@ -8,6 +8,10 @@ import {
} from '@angular/core';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatDatepickerModule } from '@angular/material/datepicker';
+import { DateAdapter } from '@angular/material/core';
+import { FormsModule } from '@angular/forms';
+import dayjs from 'dayjs';
import { UserInputService } from '../../services/user-input-service';
import { RulesPanel } from '../rules-panel/rules-panel';
import { MatCard, MatCardContent } from '@angular/material/card';
@@ -19,13 +23,16 @@ import type { LeaveType } from '../../services/leave-planner.service';
import { TranslationService } from '../../services/translation-service';
import { CalendarSettingsService } from '../../services/calendar-settings-service';
import { PlanningDurationService } from '../../services/planning-duration.service';
-import { addPlanningDays } from '../../services/planning-date';
+import { addPlanningDays, isPlanningDate } from '../../services/planning-date';
+import { DAY_FIRST_DATE_PROVIDERS } from '../../shared/day-first-date-adapter';
@Component({
selector: 'app-user-input',
imports: [
MatInputModule,
MatFormFieldModule,
+ MatDatepickerModule,
+ FormsModule,
RulesPanel,
MatCard,
MatCardContent,
@@ -36,6 +43,7 @@ import { addPlanningDays } from '../../services/planning-date';
templateUrl: './user-input.html',
styleUrl: './user-input.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: DAY_FIRST_DATE_PROVIDERS,
})
export class UserInput {
private userInputService: UserInputService = inject(UserInputService);
@@ -45,6 +53,16 @@ export class UserInput {
protected readonly calendarSettings = inject(CalendarSettingsService);
protected readonly duration = inject(PlanningDurationService);
protected readonly translationService = inject(TranslationService);
+ private readonly dateAdapter = inject>(DateAdapter);
+ protected readonly planningDates = computed(() => {
+ const { start, lastPlanningDate } = this.calendarSettings.planningDates();
+ return {
+ start: isPlanningDate(start) ? new Date(`${start}T00:00:00`) : null,
+ lastPlanningDate: isPlanningDate(lastPlanningDate)
+ ? new Date(`${lastPlanningDate}T00:00:00`)
+ : null,
+ };
+ });
protected readonly durationLabel = computed(() => {
const estimate = this.duration.estimate();
if (!estimate) return '';
@@ -171,6 +189,7 @@ export class UserInput {
);
constructor() {
+ effect(() => this.dateAdapter.setLocale(this.translationService.locale()));
effect(() => {
this.userInputService.planningOutcomeSignal();
this.historyBalanceId.set(null);
@@ -178,6 +197,14 @@ export class UserInput {
});
}
+ protected setPlanningDate(field: 'start' | 'lastPlanningDate', value: Date | null): void {
+ const dates = {
+ ...this.calendarSettings.planningDates(),
+ [field]: value ? dayjs(value).format('YYYY-MM-DD') : '',
+ };
+ this.calendarSettings.setPlanningDates(dates.start, dates.lastPlanningDate);
+ }
+
private ruleName(id: string): string {
return (
this.userInputService.planningSnapshotSignal()?.rules.find((rule) => rule.id === id)