('[role="dialog"] button')!.click();
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
activateWithKeyboard(afterButton);
diff --git a/src/app/components/calendar-year/calendar-year.html b/src/app/components/calendar-year/calendar-year.html
index 31356fe..b77d7b1 100644
--- a/src/app/components/calendar-year/calendar-year.html
+++ b/src/app/components/calendar-year/calendar-year.html
@@ -1295,6 +1295,14 @@
{{ formatStatistic(plan.metrics.longestTimeOffPeriod) }}
+
+
+ {{ tr().planning.statistics.weekendConnectedHolidays }} ({{
+ tr().planning.statistics.calendarDays
+ }})
+
+ {{ formatStatistic(weekendConnectedHolidays()) }}
+
{{ tr().planning.statistics.earnedLeave }} ({{
diff --git a/src/app/components/calendar-year/calendar-year.ts b/src/app/components/calendar-year/calendar-year.ts
index c3b4dcc..384d5fb 100644
--- a/src/app/components/calendar-year/calendar-year.ts
+++ b/src/app/components/calendar-year/calendar-year.ts
@@ -202,6 +202,20 @@ export class CalendarYear {
}
protected readonly displayedPlan = this.userInput.displayedPlanSignal;
+ protected readonly weekendConnectedHolidays = computed(() => {
+ const isWeekend = (date: string) => {
+ const day = new Date(`${date}T00:00:00Z`).getUTCDay();
+ return day === 0 || day === 6;
+ };
+ // Weekday natural non-working dates are public holidays; declarations are separate.
+ return new Set(
+ (this.displayedPlan()?.timeOffPeriods ?? []).flatMap(({ nonWorkingDates }) =>
+ nonWorkingDates.some(isWeekend)
+ ? nonWorkingDates.filter((date) => !isWeekend(date))
+ : [],
+ ),
+ ).size;
+ });
protected readonly earnedLeave = computed(() => {
const byRule = new Map();
for (const contribution of this.displayedPlan()?.ifThenContributions ?? []) {
diff --git a/src/app/services/leave-planner.worker.spec.ts b/src/app/services/leave-planner.worker.spec.ts
index 7656952..7945af9 100644
--- a/src/app/services/leave-planner.worker.spec.ts
+++ b/src/app/services/leave-planner.worker.spec.ts
@@ -221,6 +221,13 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
root.querySelector('.statistics-button')!.click();
await fixture.whenStable();
const dialog = document.querySelector('[role="dialog"]')!;
+ const connectedHolidays = () =>
+ Array.from(document.querySelectorAll('[role="dialog"] .statistics-summary dt'))
+ .find((label) =>
+ label.textContent?.includes('Jours fériés reliés à un week-end'),
+ )
+ ?.nextElementSibling?.textContent?.trim();
+ expect(connectedHolidays()).toBe('1');
const earnedStatistics = () => [
dialog
.querySelector('.statistics-summary > div:last-child dt')
@@ -258,6 +265,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
const trace = await calculation.trace;
await fixture.whenStable();
fixture.detectChanges();
+ expect(connectedHolidays()).toBe('1');
expect(earnedStatistics()).toEqual([
'Congés acquis avant consommation (jours)',
'1',
@@ -292,6 +300,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
expect(plan.placements).toEqual(direct.placements);
expect(plan.ifThenContributions).toEqual(direct.ifThenContributions);
expect(plan.metrics).toEqual(direct.metrics);
+ expect(plan.timeOffPeriods).toEqual(direct.timeOffPeriods);
expect(root.textContent).toContain('Droit futur généré');
expect(root.textContent).toContain('CP autour du férié');
expect(root.textContent).not.toContain('Règle modifiée après calcul');
@@ -331,6 +340,9 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
calculationDate: '2024-05-01',
declaredDays: originalDeclarations,
});
+ root.querySelector('.statistics-button')!.click();
+ await fixture.whenStable();
+ expect(connectedHolidays()).toBe('1');
fixture.destroy();
},
);
diff --git a/src/app/services/translation-service.ts b/src/app/services/translation-service.ts
index ca6fbe1..92ff8a6 100644
--- a/src/app/services/translation-service.ts
+++ b/src/app/services/translation-service.ts
@@ -134,6 +134,7 @@ export interface AppTranslations {
averageWorkSequence: string;
averageTimeOffPeriod: string;
longestTimeOffPeriod: string;
+ weekendConnectedHolidays: string;
earnedLeave: string;
earnedLeaveByRule: string;
leaveDaysUnit: string;
diff --git a/src/i18n/de.json b/src/i18n/de.json
index ec28422..4f08b9f 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -143,6 +143,7 @@
"averageWorkSequence": "Durchschnittliche Arbeitsphase",
"averageTimeOffPeriod": "Durchschnittliche Ruhephase",
"longestTimeOffPeriod": "Längste Ruhephase",
+ "weekendConnectedHolidays": "Feiertage mit Wochenendanschluss",
"workingDays": "Arbeitstage",
"calendarDays": "Kalendertage",
"partialPlan": "Teilplan",
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 124872f..432d629 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -143,6 +143,7 @@
"averageWorkSequence": "Average work sequence",
"averageTimeOffPeriod": "Average time-off period",
"longestTimeOffPeriod": "Longest time-off period",
+ "weekendConnectedHolidays": "Public holidays connected to a weekend",
"workingDays": "working days",
"calendarDays": "calendar days",
"partialPlan": "Partial plan",
diff --git a/src/i18n/es.json b/src/i18n/es.json
index e54274a..04ad197 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -143,6 +143,7 @@
"averageWorkSequence": "Secuencia de trabajo media",
"averageTimeOffPeriod": "Período de descanso medio",
"longestTimeOffPeriod": "Período de descanso más largo",
+ "weekendConnectedHolidays": "Festivos conectados con un fin de semana",
"workingDays": "días laborables",
"calendarDays": "días naturales",
"partialPlan": "Plan parcial",
diff --git a/src/i18n/fr.json b/src/i18n/fr.json
index a8c7700..599a00e 100644
--- a/src/i18n/fr.json
+++ b/src/i18n/fr.json
@@ -143,6 +143,7 @@
"averageWorkSequence": "Séquence de travail moyenne",
"averageTimeOffPeriod": "Période de repos moyenne",
"longestTimeOffPeriod": "Plus longue période de repos",
+ "weekendConnectedHolidays": "Jours fériés reliés à un week-end",
"workingDays": "jours ouvrés",
"calendarDays": "jours calendaires",
"partialPlan": "Planning partiel",