Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b21c9b34bd | ||
|
|
2a39dd480b |
No files matched your search
+2
-1
@@ -42,5 +42,6 @@ __screenshots__/
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Auto-generated i18n files
|
# Auto-generated application data
|
||||||
/src/app/i18n-registry.ts
|
/src/app/i18n-registry.ts
|
||||||
|
/src/app/services/french-holidays-data.json
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"@angular/platform-server": "22.2.1",
|
"@angular/platform-server": "22.2.1",
|
||||||
"@angular/router": "22.2.1",
|
"@angular/router": "22.2.1",
|
||||||
"@angular/ssr": "22.2.1",
|
"@angular/ssr": "22.2.1",
|
||||||
"date-holidays": "^3.30.2",
|
"date-holidays-parser": "^3.4.7",
|
||||||
"dayjs": "^1.11.21",
|
"dayjs": "^1.11.21",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"rxjs": "~7.8.2",
|
"rxjs": "~7.8.2",
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"@vitest/coverage-istanbul": "^5.0.0",
|
"@vitest/coverage-istanbul": "^5.0.0",
|
||||||
"@vitest/coverage-v8": "^5.0.0",
|
"@vitest/coverage-v8": "^5.0.0",
|
||||||
"@vitest/ui": "^5.0.0",
|
"@vitest/ui": "^5.0.0",
|
||||||
|
"date-holidays": "^3.30.2",
|
||||||
"eslint": "^10.4.1",
|
"eslint": "^10.4.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
|
|||||||
+10
-8
@@ -3,15 +3,16 @@
|
|||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "bun scripts/generate-langs.js && ng serve",
|
"generate": "bun scripts/generate-langs.js && bun scripts/generate-holidays.js",
|
||||||
"build": "bun scripts/generate-langs.js && ng build",
|
"start": "bun run generate && ng serve",
|
||||||
"watch": "bun scripts/generate-langs.js && ng build --watch --configuration development",
|
"build": "bun run generate && ng build",
|
||||||
"test": "bun scripts/generate-langs.js && ng test",
|
"watch": "bun run generate && ng build --watch --configuration development",
|
||||||
|
"test": "bun run generate && ng test",
|
||||||
"serve:ssr:vac-optimizer": "bun dist/vac-optimizer/server/server.mjs",
|
"serve:ssr:vac-optimizer": "bun dist/vac-optimizer/server/server.mjs",
|
||||||
"lint": "bun scripts/generate-langs.js && eslint \"src/**/*.ts\"",
|
"lint": "bun run generate && eslint \"src/**/*.ts\"",
|
||||||
"lint:fix": "bun scripts/generate-langs.js && eslint \"src/**/*.ts\" --fix",
|
"lint:fix": "bun run generate && eslint \"src/**/*.ts\" --fix",
|
||||||
"format": "bun scripts/generate-langs.js && prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
|
"format": "bun scripts/generate-langs.js && prettier --write \"src/**/*.{ts,html,scss,css,json}\"",
|
||||||
"format:check": "bun scripts/generate-langs.js && prettier --check \"src/**/*.{ts,html,scss,css,json}\""
|
"format:check": "bun run generate && prettier --check \"src/**/*.{ts,html,scss,css,json}\""
|
||||||
},
|
},
|
||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
@@ -39,7 +40,7 @@
|
|||||||
"@angular/platform-server": "22.2.1",
|
"@angular/platform-server": "22.2.1",
|
||||||
"@angular/router": "22.2.1",
|
"@angular/router": "22.2.1",
|
||||||
"@angular/ssr": "22.2.1",
|
"@angular/ssr": "22.2.1",
|
||||||
"date-holidays": "^3.30.2",
|
"date-holidays-parser": "^3.4.7",
|
||||||
"dayjs": "^1.11.21",
|
"dayjs": "^1.11.21",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"rxjs": "~7.8.2",
|
"rxjs": "~7.8.2",
|
||||||
@@ -61,6 +62,7 @@
|
|||||||
"@vitest/coverage-istanbul": "^5.0.0",
|
"@vitest/coverage-istanbul": "^5.0.0",
|
||||||
"@vitest/coverage-v8": "^5.0.0",
|
"@vitest/coverage-v8": "^5.0.0",
|
||||||
"@vitest/ui": "^5.0.0",
|
"@vitest/ui": "^5.0.0",
|
||||||
|
"date-holidays": "^3.30.2",
|
||||||
"eslint": "^10.4.1",
|
"eslint": "^10.4.1",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { data } = require('date-holidays/data');
|
||||||
|
|
||||||
|
// Use the upstream rules and translations, while bundling only the supported country.
|
||||||
|
const france = { ...data, holidays: { FR: data.holidays.FR } };
|
||||||
|
const outputFile = path.join(__dirname, '../src/app/services/french-holidays-data.json');
|
||||||
|
fs.writeFileSync(outputFile, JSON.stringify(france, null, '\t') + '\n');
|
||||||
|
console.log(`Successfully generated French holiday data at ${outputFile}.`);
|
||||||
@@ -414,6 +414,172 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
label: 'four declared days with both weekend dates outside the horizon',
|
||||||
|
start: '2024-06-10',
|
||||||
|
end: '2024-06-15',
|
||||||
|
declared: ['2024-06-10', '2024-06-11', '2024-06-12', '2024-06-13'],
|
||||||
|
entries: ['10 Jun 2024 – 16 Jun 2024 Only workday: 14 Jun 2024'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'four placed days',
|
||||||
|
start: '2024-06-10',
|
||||||
|
end: '2024-06-17',
|
||||||
|
placed: ['2024-06-10', '2024-06-11', '2024-06-12', '2024-06-13'],
|
||||||
|
entries: ['10 Jun 2024 – 16 Jun 2024 Only workday: 14 Jun 2024'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'zero remaining workdays',
|
||||||
|
start: '2024-06-10',
|
||||||
|
end: '2024-06-17',
|
||||||
|
declared: ['2024-06-10', '2024-06-11', '2024-06-12', '2024-06-13', '2024-06-14'],
|
||||||
|
entries: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'two consecutive remaining workdays',
|
||||||
|
start: '2024-06-10',
|
||||||
|
end: '2024-06-17',
|
||||||
|
declared: ['2024-06-10', '2024-06-11', '2024-06-12'],
|
||||||
|
entries: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'two separate one-day work sequences',
|
||||||
|
start: '2024-06-10',
|
||||||
|
end: '2024-06-17',
|
||||||
|
declared: ['2024-06-10', '2024-06-12', '2024-06-14'],
|
||||||
|
entries: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'two French holidays and two declared weekdays',
|
||||||
|
start: '2024-05-06',
|
||||||
|
end: '2024-05-11',
|
||||||
|
declared: ['2024-05-06', '2024-05-07'],
|
||||||
|
entries: ['6 May 2024 – 12 May 2024 Only workday: 10 May 2024'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'a holiday week with no leave and three remaining workdays',
|
||||||
|
start: '2024-05-06',
|
||||||
|
end: '2024-05-11',
|
||||||
|
entries: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'a clipped starting workweek with only Friday left to work',
|
||||||
|
start: '2024-06-13',
|
||||||
|
end: '2024-06-17',
|
||||||
|
declared: ['2024-06-13'],
|
||||||
|
entries: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'an exclusive Friday end with only Thursday left to work',
|
||||||
|
start: '2024-06-10',
|
||||||
|
end: '2024-06-14',
|
||||||
|
declared: ['2024-06-10', '2024-06-11', '2024-06-12'],
|
||||||
|
entries: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'a complete week after the clipped starting week',
|
||||||
|
start: '2024-06-13',
|
||||||
|
end: '2024-06-22',
|
||||||
|
declared: ['2024-06-13', '2024-06-17', '2024-06-18', '2024-06-19', '2024-06-20'],
|
||||||
|
entries: ['17 Jun 2024 – 23 Jun 2024 Only workday: 21 Jun 2024'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'a week across month boundaries with a Wednesday holiday',
|
||||||
|
start: '2024-04-29',
|
||||||
|
end: '2024-05-04',
|
||||||
|
declared: ['2024-04-29', '2024-04-30', '2024-05-02'],
|
||||||
|
entries: ['29 Apr 2024 – 5 May 2024 Only workday: 3 May 2024'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'a week across year boundaries with New Year excluded',
|
||||||
|
start: '2024-12-30',
|
||||||
|
end: '2025-01-04',
|
||||||
|
declared: ['2024-12-30', '2024-12-31', '2025-01-02'],
|
||||||
|
entries: ['30 Dec 2024 – 5 Jan 2025 Only workday: 3 Jan 2025'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'chronological matching weeks in multiple years without saved external weeks',
|
||||||
|
start: '2024-12-30',
|
||||||
|
end: '2026-01-03',
|
||||||
|
declared: [
|
||||||
|
'2025-12-31',
|
||||||
|
'2024-12-23',
|
||||||
|
'2025-01-02',
|
||||||
|
'2025-12-29',
|
||||||
|
'2024-12-24',
|
||||||
|
'2024-12-30',
|
||||||
|
'2026-01-05',
|
||||||
|
'2024-12-26',
|
||||||
|
'2025-12-30',
|
||||||
|
'2024-12-31',
|
||||||
|
'2026-01-06',
|
||||||
|
'2026-01-07',
|
||||||
|
'2026-01-08',
|
||||||
|
],
|
||||||
|
entries: [
|
||||||
|
'30 Dec 2024 – 5 Jan 2025 Only workday: 3 Jan 2025',
|
||||||
|
'29 Dec 2025 – 4 Jan 2026 Only workday: 2 Jan 2026',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
'reports weeks with one workday: $label',
|
||||||
|
async ({ start, end, declared = [], placed = [], entries }) => {
|
||||||
|
const core = await import('../../services/leave-planner.core');
|
||||||
|
const outcome = core.calculatePlanningInternal({
|
||||||
|
calculationDate: '2020-01-01',
|
||||||
|
horizon: { start, end },
|
||||||
|
balances: placed.length
|
||||||
|
? [{ id: 'cp', type: 'cp', quantity: placed.length, expiryDate: '2026-12-31' }]
|
||||||
|
: [],
|
||||||
|
rules: placed.map((date) => ({
|
||||||
|
id: date,
|
||||||
|
type: 'time_window',
|
||||||
|
force: 'obligation',
|
||||||
|
startDate: date,
|
||||||
|
endDate: date,
|
||||||
|
placeDays: 1,
|
||||||
|
placeType: 'cp',
|
||||||
|
})),
|
||||||
|
declaredDays: declared.map((date) => ({ date, type: 'other' })),
|
||||||
|
});
|
||||||
|
if (outcome.kind !== 'Plan') throw new Error('Expected a valid plan.');
|
||||||
|
expect(outcome.placements.map(({ date }) => date)).toEqual(placed);
|
||||||
|
const { calendar } = calendarFor(outcome);
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CalendarYear],
|
||||||
|
providers: [
|
||||||
|
provideZonelessChangeDetection(),
|
||||||
|
{ provide: CalendarService, useValue: calendar },
|
||||||
|
],
|
||||||
|
}).compileComponents();
|
||||||
|
TestBed.inject(TranslationService).setLanguage('en');
|
||||||
|
const fixture = TestBed.createComponent(CalendarYear);
|
||||||
|
fixture.detectChanges();
|
||||||
|
await fixture.whenStable();
|
||||||
|
fixture.detectChanges();
|
||||||
|
fixture.nativeElement.querySelector('.statistics-button').click();
|
||||||
|
await fixture.whenStable();
|
||||||
|
const dialog = document.querySelector('[role="dialog"]')!;
|
||||||
|
const label = Array.from(dialog.querySelectorAll('dt')).find((entry) =>
|
||||||
|
entry.textContent?.includes('Weeks with one workday'),
|
||||||
|
);
|
||||||
|
expect(label?.nextElementSibling?.textContent?.trim()).toBe(String(entries.length));
|
||||||
|
const warning = dialog.querySelector('[aria-labelledby="one-workday-weeks-title"]');
|
||||||
|
expect(
|
||||||
|
Array.from(warning?.querySelectorAll('li') ?? [], (entry) =>
|
||||||
|
entry.textContent?.replace(/\s+/g, ' ').trim(),
|
||||||
|
),
|
||||||
|
).toEqual(entries);
|
||||||
|
if (entries.length) {
|
||||||
|
expect(warning?.textContent).toContain('Warning: weeks with one workday');
|
||||||
|
expect(warning?.textContent).toContain(
|
||||||
|
'Working just one day in a full week is an undesirable pattern.',
|
||||||
|
);
|
||||||
|
} else expect(warning).toBeNull();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{
|
{
|
||||||
label: 'a Monday holiday directly beside a weekend',
|
label: 'a Monday holiday directly beside a weekend',
|
||||||
@@ -678,10 +844,19 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
['Average time-off period (calendar days)', '2.5'],
|
['Average time-off period (calendar days)', '2.5'],
|
||||||
['Longest time-off period (calendar days)', '4'],
|
['Longest time-off period (calendar days)', '4'],
|
||||||
['Public holidays connected to a weekend (calendar days)', '2'],
|
['Public holidays connected to a weekend (calendar days)', '2'],
|
||||||
|
['Weeks with one workday', placements ? '0' : '1'],
|
||||||
['Earned leave before use (days)', '3'],
|
['Earned leave before use (days)', '3'],
|
||||||
['Original generating rule', '3'],
|
['Original generating rule', '3'],
|
||||||
];
|
];
|
||||||
expect(statistics()).toEqual(expectedStatistics);
|
expect(statistics()).toEqual(expectedStatistics);
|
||||||
|
const matchingWeeks = () =>
|
||||||
|
Array.from(dialog.querySelectorAll('.one-workday-weeks li'), (entry) =>
|
||||||
|
entry.textContent?.replace(/\s+/g, ' ').trim(),
|
||||||
|
);
|
||||||
|
const expectedWeeks = placements
|
||||||
|
? []
|
||||||
|
: ['6 May 2024 – 12 May 2024 Only workday: 6 May 2024'];
|
||||||
|
expect(matchingWeeks()).toEqual(expectedWeeks);
|
||||||
expect(dialog.textContent?.includes('Partial plan')).toBe(partial);
|
expect(dialog.textContent?.includes('Partial plan')).toBe(partial);
|
||||||
expect(input.planningOutcomeSignal()).toBe(outcome);
|
expect(input.planningOutcomeSignal()).toBe(outcome);
|
||||||
expect(input.vacationNumberSignal()).toEqual(before.balances);
|
expect(input.vacationNumberSignal()).toEqual(before.balances);
|
||||||
@@ -694,6 +869,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(statistics()).toEqual(expectedStatistics);
|
expect(statistics()).toEqual(expectedStatistics);
|
||||||
|
expect(matchingWeeks()).toEqual(expectedWeeks);
|
||||||
input.updateBalances(() => []);
|
input.updateBalances(() => []);
|
||||||
input.toggleDeclaredDay('2024-05-10', 'other');
|
input.toggleDeclaredDay('2024-05-10', 'other');
|
||||||
rules.deleteRule(rules.rules[0].id);
|
rules.deleteRule(rules.rules[0].id);
|
||||||
@@ -703,6 +879,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(statistics()).toEqual(expectedStatistics);
|
expect(statistics()).toEqual(expectedStatistics);
|
||||||
|
expect(matchingWeeks()).toEqual(expectedWeeks);
|
||||||
expect(input.planningOutcomeSignal()).toBe(outcome);
|
expect(input.planningOutcomeSignal()).toBe(outcome);
|
||||||
expect(calendar.createPlanningCalculation).toHaveBeenCalledOnce();
|
expect(calendar.createPlanningCalculation).toHaveBeenCalledOnce();
|
||||||
},
|
},
|
||||||
@@ -1108,6 +1285,18 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
const dialog = document.querySelector('[role="dialog"]')!;
|
const dialog = document.querySelector('[role="dialog"]')!;
|
||||||
expect(dialog.textContent).toContain('Partial plan');
|
expect(dialog.textContent).toContain('Partial plan');
|
||||||
|
const oneWorkdayLabel = Array.from(dialog.querySelectorAll('dt')).find((label) =>
|
||||||
|
label.textContent?.includes('Weeks with one workday'),
|
||||||
|
);
|
||||||
|
expect(oneWorkdayLabel?.nextElementSibling?.textContent?.trim()).toBe('2');
|
||||||
|
expect(
|
||||||
|
Array.from(dialog.querySelectorAll('.one-workday-weeks li'), (entry) =>
|
||||||
|
entry.textContent?.replace(/\s+/g, ' ').trim(),
|
||||||
|
),
|
||||||
|
).toEqual([
|
||||||
|
'29 Apr 2024 – 5 May 2024 Only workday: 30 Apr 2024',
|
||||||
|
'6 May 2024 – 12 May 2024 Only workday: 7 May 2024',
|
||||||
|
]);
|
||||||
expect(
|
expect(
|
||||||
dialog
|
dialog
|
||||||
.querySelector('.statistics-summary > div:last-child dd')
|
.querySelector('.statistics-summary > div:last-child dd')
|
||||||
@@ -1162,8 +1351,8 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
),
|
),
|
||||||
).toEqual(
|
).toEqual(
|
||||||
allDaysOff
|
allDaysOff
|
||||||
? ['0', '4', '—', '0', '9', '9', '0', '0']
|
? ['0', '4', '—', '0', '9', '9', '0', '0', '0']
|
||||||
: ['0', '0', '5', '5', '—', '—', '0', '0'],
|
: ['0', '0', '5', '5', '—', '—', '0', '0', '0'],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -1171,6 +1360,10 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
it('localizes statistics and supports keyboard dismissal with focus restored to its opener', async () => {
|
it('localizes statistics and supports keyboard dismissal with focus restored to its opener', async () => {
|
||||||
const { calendar } = calendarFor({
|
const { calendar } = calendarFor({
|
||||||
...plan,
|
...plan,
|
||||||
|
declaredDays: ['2024-06-10', '2024-06-11', '2024-06-12', '2024-06-13'].map((date) => ({
|
||||||
|
date,
|
||||||
|
type: 'other',
|
||||||
|
})),
|
||||||
metrics: {
|
metrics: {
|
||||||
...plan.metrics,
|
...plan.metrics,
|
||||||
averageWorkSequence: 10 / 3,
|
averageWorkSequence: 10 / 3,
|
||||||
@@ -1201,19 +1394,50 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
expect(dialog.getAttribute('aria-labelledby')).toBe('plan-statistics-title');
|
expect(dialog.getAttribute('aria-labelledby')).toBe('plan-statistics-title');
|
||||||
expect(dialog.getAttribute('aria-describedby')).toBe('plan-statistics-help');
|
expect(dialog.getAttribute('aria-describedby')).toBe('plan-statistics-help');
|
||||||
await vi.waitFor(() => expect(document.activeElement).toBe(dialog.querySelector('h2')));
|
await vi.waitFor(() => expect(document.activeElement).toBe(dialog.querySelector('h2')));
|
||||||
for (const [language, workUnit, calendarUnit, holidayLabel] of [
|
expect(dialog.querySelector('.one-workday-weeks li')?.textContent).toContain('14 Jun 2024');
|
||||||
['de', 'Arbeitstage', 'Kalendertage', 'Feiertage mit Wochenendanschluss'],
|
for (const [language, workUnit, calendarUnit, holidayLabel, warningTitle, entry] of [
|
||||||
['es', 'días laborables', 'días naturales', 'Festivos conectados con un fin de semana'],
|
[
|
||||||
['fr', 'jours ouvrés', 'jours calendaires', 'Jours fériés reliés à un week-end'],
|
'de',
|
||||||
|
'Arbeitstage',
|
||||||
|
'Kalendertage',
|
||||||
|
'Feiertage mit Wochenendanschluss',
|
||||||
|
'Achtung: Wochen mit nur einem Arbeitstag',
|
||||||
|
'10.06.2024 – 16.06.2024 Einziger Arbeitstag: 14.06.2024',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'es',
|
||||||
|
'días laborables',
|
||||||
|
'días naturales',
|
||||||
|
'Festivos conectados con un fin de semana',
|
||||||
|
'Atención: semanas con un solo día de trabajo',
|
||||||
|
'10 jun 2024 – 16 jun 2024 Único día de trabajo: 14 jun 2024',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'fr',
|
||||||
|
'jours ouvrés',
|
||||||
|
'jours calendaires',
|
||||||
|
'Jours fériés reliés à un week-end',
|
||||||
|
'Attention : semaines avec un seul jour travaillé',
|
||||||
|
'10 juin 2024 – 16 juin 2024 Seul jour travaillé: 14 juin 2024',
|
||||||
|
],
|
||||||
]) {
|
]) {
|
||||||
translation.setLanguage(language);
|
translation.setLanguage(language);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(dialog.textContent).toContain(workUnit);
|
expect(dialog.textContent).toContain(workUnit);
|
||||||
expect(dialog.textContent).toContain(calendarUnit);
|
expect(dialog.textContent).toContain(calendarUnit);
|
||||||
expect(dialog.textContent).toContain(holidayLabel);
|
expect(dialog.textContent).toContain(holidayLabel);
|
||||||
|
expect(dialog.querySelector('#one-workday-weeks-title')?.textContent?.trim()).toBe(
|
||||||
|
warningTitle,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
dialog
|
||||||
|
.querySelector('.one-workday-weeks li')
|
||||||
|
?.textContent?.replace(/\s+/g, ' ')
|
||||||
|
.trim(),
|
||||||
|
).toBe(entry);
|
||||||
expect(
|
expect(
|
||||||
Array.from(dialog.querySelectorAll('dd'), (value) => value.textContent?.trim()),
|
Array.from(dialog.querySelectorAll('dd'), (value) => value.textContent?.trim()),
|
||||||
).toEqual(['2', '4', '3,333', '6', '2,5', '4', '0', '0']);
|
).toEqual(['2', '4', '3,333', '6', '2,5', '4', '0', '1', '0']);
|
||||||
}
|
}
|
||||||
expect(dialog.textContent).toContain('Statistiques du planning');
|
expect(dialog.textContent).toContain('Statistiques du planning');
|
||||||
expect(dialog.querySelector('dt')?.textContent).toContain('Nouveaux jours de congé posés');
|
expect(dialog.querySelector('dt')?.textContent).toContain('Nouveaux jours de congé posés');
|
||||||
@@ -1238,13 +1462,22 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
] satisfies [string, PlanningOutcome, boolean][])(
|
] satisfies [string, PlanningOutcome, boolean][])(
|
||||||
'replaces open statistics after %s and ignores an older valid outcome',
|
'replaces open statistics after %s and ignores an older valid outcome',
|
||||||
async (_, replacement: PlanningOutcome, focusMoved) => {
|
async (_, replacement: PlanningOutcome, focusMoved) => {
|
||||||
|
const previousPlan: PlanOutcome = {
|
||||||
|
...plan,
|
||||||
|
declaredDays: ['2024-06-10', '2024-06-11', '2024-06-12', '2024-06-13'].map(
|
||||||
|
(date) => ({
|
||||||
|
date,
|
||||||
|
type: 'other',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
};
|
||||||
const oldOutcome = deferred<PlanningOutcome>();
|
const oldOutcome = deferred<PlanningOutcome>();
|
||||||
const oldTrace = deferred<PlanningTrace | undefined>();
|
const oldTrace = deferred<PlanningTrace | undefined>();
|
||||||
const { calendar } = calendarFor(plan);
|
const { calendar } = calendarFor(previousPlan);
|
||||||
calendar.createPlanningCalculation
|
calendar.createPlanningCalculation
|
||||||
.mockReturnValueOnce(calculationFor(plan))
|
.mockReturnValueOnce(calculationFor(previousPlan))
|
||||||
.mockReturnValueOnce({
|
.mockReturnValueOnce({
|
||||||
...calculationFor(plan, undefined, undefined, oldTrace.promise),
|
...calculationFor(previousPlan, undefined, undefined, oldTrace.promise),
|
||||||
outcome: oldOutcome.promise,
|
outcome: oldOutcome.promise,
|
||||||
})
|
})
|
||||||
.mockReturnValueOnce(calculationFor(replacement));
|
.mockReturnValueOnce(calculationFor(replacement));
|
||||||
@@ -1255,6 +1488,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
{ provide: CalendarService, useValue: calendar },
|
{ provide: CalendarService, useValue: calendar },
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
TestBed.inject(TranslationService).setLanguage('en');
|
||||||
const input = TestBed.inject(UserInputService);
|
const input = TestBed.inject(UserInputService);
|
||||||
const fixture = TestBed.createComponent(CalendarYear);
|
const fixture = TestBed.createComponent(CalendarYear);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -1264,6 +1498,9 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
activateWithKeyboard(root.querySelector<HTMLButtonElement>('.statistics-button')!);
|
activateWithKeyboard(root.querySelector<HTMLButtonElement>('.statistics-button')!);
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe('2');
|
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe('2');
|
||||||
|
expect(
|
||||||
|
document.querySelector('[role="dialog"] .one-workday-weeks li')?.textContent,
|
||||||
|
).toContain('14 Jun 2024');
|
||||||
input.requestComputation();
|
input.requestComputation();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe('2');
|
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe('2');
|
||||||
@@ -1276,7 +1513,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
|
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
|
||||||
oldOutcome.resolve(plan);
|
oldOutcome.resolve(previousPlan);
|
||||||
oldTrace.resolve(jointTrace());
|
oldTrace.resolve(jointTrace());
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -1287,6 +1524,11 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
opener!.click();
|
opener!.click();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe('1');
|
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe('1');
|
||||||
|
expect(document.querySelector('[role="dialog"] .one-workday-weeks')).toBeNull();
|
||||||
|
const label = Array.from(document.querySelectorAll('[role="dialog"] dt')).find(
|
||||||
|
(entry) => entry.textContent?.includes('Weeks with one workday'),
|
||||||
|
);
|
||||||
|
expect(label?.nextElementSibling?.textContent?.trim()).toBe('0');
|
||||||
} else {
|
} else {
|
||||||
expect(opener).toBeNull();
|
expect(opener).toBeNull();
|
||||||
expect(document.activeElement).toBe(
|
expect(document.activeElement).toBe(
|
||||||
@@ -3509,7 +3751,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
|||||||
Array.from(document.querySelectorAll('[role="dialog"] dd'), (value) =>
|
Array.from(document.querySelectorAll('[role="dialog"] dd'), (value) =>
|
||||||
value.textContent?.trim(),
|
value.textContent?.trim(),
|
||||||
),
|
),
|
||||||
).toEqual(['2', '4', '6', '6', '—', '—', '0', '0']);
|
).toEqual(['2', '4', '6', '6', '—', '—', '0', '0', '0']);
|
||||||
document.querySelector<HTMLButtonElement>('[role="dialog"] button')!.click();
|
document.querySelector<HTMLButtonElement>('[role="dialog"] button')!.click();
|
||||||
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
|
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
|
||||||
activateWithKeyboard(afterButton);
|
activateWithKeyboard(afterButton);
|
||||||
|
|||||||
@@ -1303,6 +1303,10 @@
|
|||||||
</dt>
|
</dt>
|
||||||
<dd>{{ formatStatistic(weekendConnectedHolidays()) }}</dd>
|
<dd>{{ formatStatistic(weekendConnectedHolidays()) }}</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>{{ tr().planning.statistics.weeksWithOneWorkday }}</dt>
|
||||||
|
<dd>{{ formatStatistic(weeksWithOneWorkday().length) }}</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>
|
<dt>
|
||||||
{{ tr().planning.statistics.earnedLeave }} ({{
|
{{ tr().planning.statistics.earnedLeave }} ({{
|
||||||
@@ -1332,6 +1336,24 @@
|
|||||||
</dl>
|
</dl>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
@if (weeksWithOneWorkday().length) {
|
||||||
|
<section class="one-workday-weeks" aria-labelledby="one-workday-weeks-title">
|
||||||
|
<h3 id="one-workday-weeks-title">
|
||||||
|
{{ tr().planning.statistics.oneWorkdayWarning }}
|
||||||
|
</h3>
|
||||||
|
<p>{{ tr().planning.statistics.oneWorkdayWarningHelp }}</p>
|
||||||
|
<ol>
|
||||||
|
@for (week of weeksWithOneWorkday(); track week.start) {
|
||||||
|
<li>
|
||||||
|
{{ displayDate(week.start) }} – {{ displayDate(week.end) }}
|
||||||
|
<br />
|
||||||
|
{{ tr().planning.statistics.onlyWorkday }}:
|
||||||
|
{{ displayDate(week.workday) }}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</mat-dialog-content>
|
</mat-dialog-content>
|
||||||
<mat-dialog-actions align="end">
|
<mat-dialog-actions align="end">
|
||||||
|
|||||||
@@ -248,167 +248,6 @@ summary:focus-visible {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.placement-explanation,
|
|
||||||
.outcome-explanation,
|
|
||||||
.trace-decisions {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.placement-explanation,
|
|
||||||
.trace-layout {
|
|
||||||
padding: 1.25rem;
|
|
||||||
}
|
|
||||||
.placement-explanation ul {
|
|
||||||
margin: 0.5rem 0 0;
|
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
|
||||||
.break-story,
|
|
||||||
.outcome-explanation,
|
|
||||||
.replay-notice {
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: var(--ui-radius-sm);
|
|
||||||
background: var(--ui-panel-bg);
|
|
||||||
}
|
|
||||||
.technical-details {
|
|
||||||
border: 1px solid var(--ui-border);
|
|
||||||
border-radius: var(--ui-radius-sm);
|
|
||||||
> dl,
|
|
||||||
> div {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.break-story,
|
|
||||||
.day-impact,
|
|
||||||
.leave-story,
|
|
||||||
.counterfactual-result {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
.break-story > p,
|
|
||||||
.day-impact > p {
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.break-equation {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr auto 1fr auto 1fr;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
strong {
|
|
||||||
display: block;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.impact-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
table-layout: fixed;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
th,
|
|
||||||
td {
|
|
||||||
padding: 0.75rem 0.5rem;
|
|
||||||
border-bottom: 1px solid var(--ui-border);
|
|
||||||
text-align: center;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
th:first-child {
|
|
||||||
width: 52%;
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
td {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
}
|
|
||||||
small {
|
|
||||||
display: block;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 400;
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.compare-date-field {
|
|
||||||
width: 100%;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.compare-dates-button {
|
|
||||||
justify-self: start;
|
|
||||||
}
|
|
||||||
.placement-stepper,
|
|
||||||
.trace-toolbar,
|
|
||||||
.trace-decisions nav,
|
|
||||||
.trace-phase-stepper {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.placement-stepper button,
|
|
||||||
.trace-phase-stepper button {
|
|
||||||
padding: 0.375rem;
|
|
||||||
}
|
|
||||||
output {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-variant-numeric: tabular-nums;
|
|
||||||
padding: 0.375rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compact-facts {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
|
|
||||||
gap: 0.75rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.compact-facts > div {
|
|
||||||
min-width: 0;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background: var(--ui-panel-bg);
|
|
||||||
border-radius: var(--ui-radius-sm);
|
|
||||||
}
|
|
||||||
.compact-facts dt {
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
}
|
|
||||||
.compact-facts dd {
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
.reason-list {
|
|
||||||
padding: 1rem 1rem 1rem 2rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.inline-error,
|
|
||||||
.counterfactual-inline-error,
|
|
||||||
.conflict-explanation {
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-inline-start: 4px solid var(--ui-error-text);
|
|
||||||
background: var(--ui-error-bg);
|
|
||||||
color: var(--ui-error-text);
|
|
||||||
}
|
|
||||||
.counterfactual-inline-error {
|
|
||||||
display: grid;
|
|
||||||
justify-items: start;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
.decisive-summary {
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-inline-start: 3px solid var(--mat-sys-primary);
|
|
||||||
background: var(--ui-panel-bg);
|
|
||||||
}
|
|
||||||
.incomplete-explanation,
|
|
||||||
.incomplete-incumbent {
|
|
||||||
padding: 1rem;
|
|
||||||
border-inline-start: 4px solid var(--ui-warning-text);
|
|
||||||
background: var(--ui-warning-bg);
|
|
||||||
color: var(--ui-warning-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.calendar-help {
|
.calendar-help {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -617,248 +456,3 @@ output {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.calculation-content,
|
|
||||||
.criteria-detail,
|
|
||||||
.balance-detail {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.75rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
.calculation-content {
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
.calculation-intro,
|
|
||||||
.balance-detail > p {
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.balance-ledger {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1rem 2rem;
|
|
||||||
margin: 0;
|
|
||||||
dt {
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
dd {
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.criterion-help {
|
|
||||||
summary {
|
|
||||||
padding: 0;
|
|
||||||
align-content: center;
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
font-weight: 400;
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
padding-block: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.decisive-row {
|
|
||||||
background: var(--ui-panel-bg);
|
|
||||||
}
|
|
||||||
.decisive-label {
|
|
||||||
display: inline-block;
|
|
||||||
margin-top: 0.375rem;
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
border: 1px solid var(--mat-sys-primary);
|
|
||||||
border-radius: var(--ui-radius-sm);
|
|
||||||
color: var(--mat-sys-primary);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.tie-breaks {
|
|
||||||
padding: 0.75rem;
|
|
||||||
border-inline-start: 2px solid var(--ui-border);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
ol {
|
|
||||||
margin: 0.5rem 0 0;
|
|
||||||
padding-inline-start: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.audit-data {
|
|
||||||
border-top: 1px solid var(--ui-border);
|
|
||||||
summary {
|
|
||||||
padding-inline: 0;
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
max-height: 24rem;
|
|
||||||
overflow: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background: var(--ui-panel-bg);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.counterfactual-panel {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: calc(100dvh - 2rem);
|
|
||||||
background: var(--ui-card-bg);
|
|
||||||
color: var(--mat-sys-on-surface);
|
|
||||||
}
|
|
||||||
.comparison-heading,
|
|
||||||
.comparison-footer,
|
|
||||||
.comparison-result-heading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
.comparison-heading {
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 1.5rem;
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.comparison-close {
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 0;
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
.counterfactual-panel .comparison-content {
|
|
||||||
display: grid;
|
|
||||||
gap: 1rem;
|
|
||||||
min-height: 0;
|
|
||||||
max-height: none;
|
|
||||||
padding: 0 1.5rem 1.5rem;
|
|
||||||
color: inherit;
|
|
||||||
font: inherit;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.comparison-date-pair {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.75rem;
|
|
||||||
> mat-icon {
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
}
|
|
||||||
> .original-date {
|
|
||||||
padding: 1rem;
|
|
||||||
border: 1px solid var(--ui-border);
|
|
||||||
border-radius: var(--ui-radius-sm);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.original-date {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.75rem;
|
|
||||||
background: var(--ui-panel-bg);
|
|
||||||
span {
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
}
|
|
||||||
strong {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.comparison-range {
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
.nearby-dates {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
> span {
|
|
||||||
flex-basis: 100%;
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.comparison-result-heading {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding-top: 0.5rem;
|
|
||||||
}
|
|
||||||
.available-date,
|
|
||||||
.unavailable-date,
|
|
||||||
.comparison-loading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
mat-icon {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.available-date {
|
|
||||||
color: var(--ui-success-text);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
}
|
|
||||||
.unavailable-date {
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: var(--ui-radius-sm);
|
|
||||||
background: var(--ui-warning-bg);
|
|
||||||
color: var(--ui-warning-text);
|
|
||||||
p {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.comparison-empty {
|
|
||||||
padding: 1rem;
|
|
||||||
background: var(--ui-panel-bg);
|
|
||||||
border-radius: var(--ui-radius-sm);
|
|
||||||
}
|
|
||||||
.comparison-loading {
|
|
||||||
padding-block: 2rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.comparison-footer {
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
border-top: 1px solid var(--ui-border);
|
|
||||||
font-size: 0.8125rem;
|
|
||||||
color: var(--ui-text-secondary);
|
|
||||||
.comparison-done {
|
|
||||||
background: var(--mat-sys-primary);
|
|
||||||
color: var(--mat-sys-on-primary);
|
|
||||||
&:hover {
|
|
||||||
background: var(--mat-sys-primary);
|
|
||||||
opacity: 0.9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 560px) {
|
|
||||||
.comparison-heading {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
.counterfactual-panel .comparison-content {
|
|
||||||
padding: 0 1rem 1rem;
|
|
||||||
}
|
|
||||||
.comparison-date-pair {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
> mat-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
> div {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.comparison-footer {
|
|
||||||
padding: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
button {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nearby-dates button[aria-pressed='true'] {
|
|
||||||
border-color: var(--mat-sys-primary);
|
|
||||||
}
|
|
||||||
@@ -94,7 +94,7 @@ type TraceTransition = Readonly<{ before: PlanningTraceState; after: PlanningTra
|
|||||||
MatIcon,
|
MatIcon,
|
||||||
],
|
],
|
||||||
templateUrl: './calendar-year.html',
|
templateUrl: './calendar-year.html',
|
||||||
styleUrl: './calendar-year.scss',
|
styleUrls: ['./planning-details.scss', './calendar-year.scss'],
|
||||||
providers: DAY_FIRST_DATE_PROVIDERS,
|
providers: DAY_FIRST_DATE_PROVIDERS,
|
||||||
})
|
})
|
||||||
export class CalendarYear {
|
export class CalendarYear {
|
||||||
@@ -202,6 +202,32 @@ export class CalendarYear {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected readonly displayedPlan = this.userInput.displayedPlanSignal;
|
protected readonly displayedPlan = this.userInput.displayedPlanSignal;
|
||||||
|
protected readonly weeksWithOneWorkday = computed(() => {
|
||||||
|
const plan = this.displayedPlan();
|
||||||
|
if (!plan) return [];
|
||||||
|
const timeOff = new Set([
|
||||||
|
...plan.placements.map(({ date }) => date),
|
||||||
|
...(plan.declaredDays ?? []).map(({ date }) => date),
|
||||||
|
]);
|
||||||
|
const startDay = new Date(`${plan.horizon.start}T00:00:00Z`).getUTCDay();
|
||||||
|
const weeks: { start: string; end: string; workday: string }[] = [];
|
||||||
|
for (
|
||||||
|
let monday = addPlanningDays(plan.horizon.start, (8 - startDay) % 7);
|
||||||
|
addPlanningDays(monday, 4) < plan.horizon.end;
|
||||||
|
monday = addPlanningDays(monday, 7)
|
||||||
|
) {
|
||||||
|
const workdays = Array.from({ length: 5 }, (_, day) =>
|
||||||
|
addPlanningDays(monday, day),
|
||||||
|
).filter((date) => isWorkingPlanningDate(date) && !timeOff.has(date));
|
||||||
|
if (workdays.length === 1)
|
||||||
|
weeks.push({
|
||||||
|
start: monday,
|
||||||
|
end: addPlanningDays(monday, 6),
|
||||||
|
workday: workdays[0],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return weeks;
|
||||||
|
});
|
||||||
protected readonly weekendConnectedHolidays = computed(() => {
|
protected readonly weekendConnectedHolidays = computed(() => {
|
||||||
const isWeekend = (date: string) => {
|
const isWeekend = (date: string) => {
|
||||||
const day = new Date(`${date}T00:00:00Z`).getUTCDay();
|
const day = new Date(`${date}T00:00:00Z`).getUTCDay();
|
||||||
|
|||||||
@@ -0,0 +1,406 @@
|
|||||||
|
.placement-explanation,
|
||||||
|
.outcome-explanation,
|
||||||
|
.trace-decisions {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.placement-explanation,
|
||||||
|
.trace-layout {
|
||||||
|
padding: 1.25rem;
|
||||||
|
}
|
||||||
|
.placement-explanation ul {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding-inline-start: 1.25rem;
|
||||||
|
}
|
||||||
|
.break-story,
|
||||||
|
.outcome-explanation,
|
||||||
|
.replay-notice {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--ui-radius-sm);
|
||||||
|
background: var(--ui-panel-bg);
|
||||||
|
}
|
||||||
|
.technical-details {
|
||||||
|
border: 1px solid var(--ui-border);
|
||||||
|
border-radius: var(--ui-radius-sm);
|
||||||
|
> dl,
|
||||||
|
> div {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.break-story,
|
||||||
|
.day-impact,
|
||||||
|
.leave-story,
|
||||||
|
.counterfactual-result {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.break-story > p,
|
||||||
|
.day-impact > p {
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.break-equation {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto 1fr auto 1fr;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.impact-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
th,
|
||||||
|
td {
|
||||||
|
padding: 0.75rem 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--ui-border);
|
||||||
|
text-align: center;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
th:first-child {
|
||||||
|
width: 52%;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.compare-date-field {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.compare-dates-button {
|
||||||
|
justify-self: start;
|
||||||
|
}
|
||||||
|
.placement-stepper,
|
||||||
|
.trace-toolbar,
|
||||||
|
.trace-decisions nav,
|
||||||
|
.trace-phase-stepper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
.placement-stepper button,
|
||||||
|
.trace-phase-stepper button {
|
||||||
|
padding: 0.375rem;
|
||||||
|
}
|
||||||
|
output {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
padding: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-facts {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.compact-facts > div {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--ui-panel-bg);
|
||||||
|
border-radius: var(--ui-radius-sm);
|
||||||
|
}
|
||||||
|
.compact-facts dt {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
}
|
||||||
|
.compact-facts dd {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.reason-list {
|
||||||
|
padding: 1rem 1rem 1rem 2rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.inline-error,
|
||||||
|
.counterfactual-inline-error,
|
||||||
|
.conflict-explanation {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-inline-start: 4px solid var(--ui-error-text);
|
||||||
|
background: var(--ui-error-bg);
|
||||||
|
color: var(--ui-error-text);
|
||||||
|
}
|
||||||
|
.counterfactual-inline-error {
|
||||||
|
display: grid;
|
||||||
|
justify-items: start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.decisive-summary {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-inline-start: 3px solid var(--mat-sys-primary);
|
||||||
|
background: var(--ui-panel-bg);
|
||||||
|
}
|
||||||
|
.incomplete-explanation,
|
||||||
|
.incomplete-incumbent,
|
||||||
|
.one-workday-weeks {
|
||||||
|
padding: 1rem;
|
||||||
|
border-inline-start: 4px solid var(--ui-warning-text);
|
||||||
|
background: var(--ui-warning-bg);
|
||||||
|
color: var(--ui-warning-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calculation-content,
|
||||||
|
.criteria-detail,
|
||||||
|
.balance-detail {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.calculation-content {
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
.calculation-intro,
|
||||||
|
.balance-detail > p {
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.balance-ledger {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem 2rem;
|
||||||
|
margin: 0;
|
||||||
|
dt {
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
dd {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.criterion-help {
|
||||||
|
summary {
|
||||||
|
padding: 0;
|
||||||
|
align-content: center;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
padding-block: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.decisive-row {
|
||||||
|
background: var(--ui-panel-bg);
|
||||||
|
}
|
||||||
|
.decisive-label {
|
||||||
|
display: inline-block;
|
||||||
|
margin-top: 0.375rem;
|
||||||
|
padding: 0.125rem 0.375rem;
|
||||||
|
border: 1px solid var(--mat-sys-primary);
|
||||||
|
border-radius: var(--ui-radius-sm);
|
||||||
|
color: var(--mat-sys-primary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.tie-breaks {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-inline-start: 2px solid var(--ui-border);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
ol {
|
||||||
|
margin: 0.5rem 0 0;
|
||||||
|
padding-inline-start: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.audit-data {
|
||||||
|
border-top: 1px solid var(--ui-border);
|
||||||
|
summary {
|
||||||
|
padding-inline: 0;
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
max-height: 24rem;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: var(--ui-panel-bg);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.counterfactual-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: calc(100dvh - 2rem);
|
||||||
|
background: var(--ui-card-bg);
|
||||||
|
color: var(--mat-sys-on-surface);
|
||||||
|
}
|
||||||
|
.comparison-heading,
|
||||||
|
.comparison-footer,
|
||||||
|
.comparison-result-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.comparison-heading {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 1.5rem;
|
||||||
|
h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.comparison-close {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 0;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
.counterfactual-panel .comparison-content {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: none;
|
||||||
|
padding: 0 1.5rem 1.5rem;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
.comparison-date-pair {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
> mat-icon {
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
}
|
||||||
|
> .original-date {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--ui-border);
|
||||||
|
border-radius: var(--ui-radius-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.original-date {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
background: var(--ui-panel-bg);
|
||||||
|
span {
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
}
|
||||||
|
strong {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.comparison-range {
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
.nearby-dates {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
> span {
|
||||||
|
flex-basis: 100%;
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.comparison-result-heading {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding-top: 0.5rem;
|
||||||
|
}
|
||||||
|
.available-date,
|
||||||
|
.unavailable-date,
|
||||||
|
.comparison-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
mat-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.available-date {
|
||||||
|
color: var(--ui-success-text);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
}
|
||||||
|
.unavailable-date {
|
||||||
|
align-items: flex-start;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--ui-radius-sm);
|
||||||
|
background: var(--ui-warning-bg);
|
||||||
|
color: var(--ui-warning-text);
|
||||||
|
p {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.comparison-empty {
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--ui-panel-bg);
|
||||||
|
border-radius: var(--ui-radius-sm);
|
||||||
|
}
|
||||||
|
.comparison-loading {
|
||||||
|
padding-block: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.comparison-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-top: 1px solid var(--ui-border);
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: var(--ui-text-secondary);
|
||||||
|
.comparison-done {
|
||||||
|
background: var(--mat-sys-primary);
|
||||||
|
color: var(--mat-sys-on-primary);
|
||||||
|
&:hover {
|
||||||
|
background: var(--mat-sys-primary);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 560px) {
|
||||||
|
.comparison-heading {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.counterfactual-panel .comparison-content {
|
||||||
|
padding: 0 1rem 1rem;
|
||||||
|
}
|
||||||
|
.comparison-date-pair {
|
||||||
|
grid-template-columns: minmax(0, 1fr);
|
||||||
|
> mat-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
> div {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.comparison-footer {
|
||||||
|
padding: 1rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nearby-dates button[aria-pressed='true'] {
|
||||||
|
border-color: var(--mat-sys-primary);
|
||||||
|
}
|
||||||
@@ -8,6 +8,8 @@ import {
|
|||||||
SelectedDates,
|
SelectedDates,
|
||||||
} from './calendar-service';
|
} from './calendar-service';
|
||||||
import { DateRange } from '@angular/material/datepicker';
|
import { DateRange } from '@angular/material/datepicker';
|
||||||
|
import Holidays from 'date-holidays';
|
||||||
|
import { frenchPublicHolidaysForYear } from './declared-non-working-days';
|
||||||
|
|
||||||
describe('CalendarService', () => {
|
describe('CalendarService', () => {
|
||||||
let service: CalendarService;
|
let service: CalendarService;
|
||||||
@@ -158,6 +160,18 @@ describe('CalendarService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getHolidays', () => {
|
describe('getHolidays', () => {
|
||||||
|
it('preserves upstream French public holidays across historical and future years', () => {
|
||||||
|
const upstream = new Holidays('FR', { languages: ['en'] });
|
||||||
|
for (const year of [1900, 1918, 1945, 1999, 2000, 2024, 2038, 2100]) {
|
||||||
|
expect([...frenchPublicHolidaysForYear(year)]).toEqual(
|
||||||
|
upstream
|
||||||
|
.getHolidays(year, 'en')
|
||||||
|
.filter(({ type }) => type === 'public')
|
||||||
|
.map(({ date }) => date.slice(0, 10)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should return an array of holidays', () => {
|
it('should return an array of holidays', () => {
|
||||||
const holidays = service.getHolidays();
|
const holidays = service.getHolidays();
|
||||||
expect(Array.isArray(holidays)).toBe(true);
|
expect(Array.isArray(holidays)).toBe(true);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Holidays from 'date-holidays';
|
import Holidays from 'date-holidays-parser';
|
||||||
|
import frenchHolidays from './french-holidays-data.json';
|
||||||
import { isPlanningDate } from './planning-date';
|
import { isPlanningDate } from './planning-date';
|
||||||
import type { LeaveType } from './leave-planner.core';
|
import type { LeaveType } from './leave-planner.core';
|
||||||
|
|
||||||
@@ -13,7 +14,7 @@ export function frenchPublicHolidaysForYear(year: number): ReadonlySet<string> {
|
|||||||
let holidays = holidaysByYear.get(year);
|
let holidays = holidaysByYear.get(year);
|
||||||
if (!holidays) {
|
if (!holidays) {
|
||||||
holidays = new Set(
|
holidays = new Set(
|
||||||
new Holidays('FR', { languages: ['en'] })
|
new Holidays(frenchHolidays, 'FR', { languages: ['en'] })
|
||||||
.getHolidays(year, 'en')
|
.getHolidays(year, 'en')
|
||||||
.filter((holiday) => holiday.type === 'public')
|
.filter((holiday) => holiday.type === 'public')
|
||||||
.map((holiday) => String(holiday.date).slice(0, 10)),
|
.map((holiday) => String(holiday.date).slice(0, 10)),
|
||||||
|
|||||||
@@ -228,6 +228,21 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
|||||||
)
|
)
|
||||||
?.nextElementSibling?.textContent?.trim();
|
?.nextElementSibling?.textContent?.trim();
|
||||||
expect(connectedHolidays()).toBe('1');
|
expect(connectedHolidays()).toBe('1');
|
||||||
|
const oneWorkdayWeeks = () => [
|
||||||
|
Array.from(document.querySelectorAll('[role="dialog"] .statistics-summary dt'))
|
||||||
|
.find((label) =>
|
||||||
|
label.textContent?.includes('Semaines avec un seul jour travaillé'),
|
||||||
|
)
|
||||||
|
?.nextElementSibling?.textContent?.trim(),
|
||||||
|
...Array.from(
|
||||||
|
document.querySelectorAll('[role="dialog"] .one-workday-weeks li'),
|
||||||
|
(entry) => entry.textContent?.replace(/\s+/g, ' ').trim(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
expect(oneWorkdayWeeks()).toEqual([
|
||||||
|
'1',
|
||||||
|
'29 avr. 2024 – 5 mai 2024 Seul jour travaillé: 2 mai 2024',
|
||||||
|
]);
|
||||||
const earnedStatistics = () => [
|
const earnedStatistics = () => [
|
||||||
dialog
|
dialog
|
||||||
.querySelector('.statistics-summary > div:last-child dt')
|
.querySelector('.statistics-summary > div:last-child dt')
|
||||||
@@ -266,6 +281,10 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
|||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(connectedHolidays()).toBe('1');
|
expect(connectedHolidays()).toBe('1');
|
||||||
|
expect(oneWorkdayWeeks()).toEqual([
|
||||||
|
'1',
|
||||||
|
'29 avr. 2024 – 5 mai 2024 Seul jour travaillé: 2 mai 2024',
|
||||||
|
]);
|
||||||
expect(earnedStatistics()).toEqual([
|
expect(earnedStatistics()).toEqual([
|
||||||
'Congés acquis avant consommation (jours)',
|
'Congés acquis avant consommation (jours)',
|
||||||
'1',
|
'1',
|
||||||
@@ -343,6 +362,10 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
|||||||
root.querySelector<HTMLButtonElement>('.statistics-button')!.click();
|
root.querySelector<HTMLButtonElement>('.statistics-button')!.click();
|
||||||
await fixture.whenStable();
|
await fixture.whenStable();
|
||||||
expect(connectedHolidays()).toBe('1');
|
expect(connectedHolidays()).toBe('1');
|
||||||
|
expect(oneWorkdayWeeks()).toEqual([
|
||||||
|
'1',
|
||||||
|
'29 avr. 2024 – 5 mai 2024 Seul jour travaillé: 2 mai 2024',
|
||||||
|
]);
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -135,6 +135,10 @@ export interface AppTranslations {
|
|||||||
averageTimeOffPeriod: string;
|
averageTimeOffPeriod: string;
|
||||||
longestTimeOffPeriod: string;
|
longestTimeOffPeriod: string;
|
||||||
weekendConnectedHolidays: string;
|
weekendConnectedHolidays: string;
|
||||||
|
weeksWithOneWorkday: string;
|
||||||
|
oneWorkdayWarning: string;
|
||||||
|
oneWorkdayWarningHelp: string;
|
||||||
|
onlyWorkday: string;
|
||||||
earnedLeave: string;
|
earnedLeave: string;
|
||||||
earnedLeaveByRule: string;
|
earnedLeaveByRule: string;
|
||||||
leaveDaysUnit: string;
|
leaveDaysUnit: string;
|
||||||
|
|||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
// The parser ships these declarations but omits them from its package exports.
|
||||||
|
import '../node_modules/date-holidays-parser/types/index';
|
||||||
@@ -144,6 +144,10 @@
|
|||||||
"averageTimeOffPeriod": "Durchschnittliche Ruhephase",
|
"averageTimeOffPeriod": "Durchschnittliche Ruhephase",
|
||||||
"longestTimeOffPeriod": "Längste Ruhephase",
|
"longestTimeOffPeriod": "Längste Ruhephase",
|
||||||
"weekendConnectedHolidays": "Feiertage mit Wochenendanschluss",
|
"weekendConnectedHolidays": "Feiertage mit Wochenendanschluss",
|
||||||
|
"weeksWithOneWorkday": "Wochen mit nur einem Arbeitstag",
|
||||||
|
"oneWorkdayWarning": "Achtung: Wochen mit nur einem Arbeitstag",
|
||||||
|
"oneWorkdayWarningHelp": "Nur einen Tag in einer vollständigen Woche zu arbeiten, ist ein ungünstiges Muster.",
|
||||||
|
"onlyWorkday": "Einziger Arbeitstag",
|
||||||
"workingDays": "Arbeitstage",
|
"workingDays": "Arbeitstage",
|
||||||
"calendarDays": "Kalendertage",
|
"calendarDays": "Kalendertage",
|
||||||
"partialPlan": "Teilplan",
|
"partialPlan": "Teilplan",
|
||||||
|
|||||||
@@ -144,6 +144,10 @@
|
|||||||
"averageTimeOffPeriod": "Average time-off period",
|
"averageTimeOffPeriod": "Average time-off period",
|
||||||
"longestTimeOffPeriod": "Longest time-off period",
|
"longestTimeOffPeriod": "Longest time-off period",
|
||||||
"weekendConnectedHolidays": "Public holidays connected to a weekend",
|
"weekendConnectedHolidays": "Public holidays connected to a weekend",
|
||||||
|
"weeksWithOneWorkday": "Weeks with one workday",
|
||||||
|
"oneWorkdayWarning": "Warning: weeks with one workday",
|
||||||
|
"oneWorkdayWarningHelp": "Working just one day in a full week is an undesirable pattern.",
|
||||||
|
"onlyWorkday": "Only workday",
|
||||||
"workingDays": "working days",
|
"workingDays": "working days",
|
||||||
"calendarDays": "calendar days",
|
"calendarDays": "calendar days",
|
||||||
"partialPlan": "Partial plan",
|
"partialPlan": "Partial plan",
|
||||||
|
|||||||
@@ -144,6 +144,10 @@
|
|||||||
"averageTimeOffPeriod": "Período de descanso medio",
|
"averageTimeOffPeriod": "Período de descanso medio",
|
||||||
"longestTimeOffPeriod": "Período de descanso más largo",
|
"longestTimeOffPeriod": "Período de descanso más largo",
|
||||||
"weekendConnectedHolidays": "Festivos conectados con un fin de semana",
|
"weekendConnectedHolidays": "Festivos conectados con un fin de semana",
|
||||||
|
"weeksWithOneWorkday": "Semanas con un solo día de trabajo",
|
||||||
|
"oneWorkdayWarning": "Atención: semanas con un solo día de trabajo",
|
||||||
|
"oneWorkdayWarningHelp": "Trabajar solo un día en una semana completa es un patrón poco deseable.",
|
||||||
|
"onlyWorkday": "Único día de trabajo",
|
||||||
"workingDays": "días laborables",
|
"workingDays": "días laborables",
|
||||||
"calendarDays": "días naturales",
|
"calendarDays": "días naturales",
|
||||||
"partialPlan": "Plan parcial",
|
"partialPlan": "Plan parcial",
|
||||||
|
|||||||
@@ -144,6 +144,10 @@
|
|||||||
"averageTimeOffPeriod": "Période de repos moyenne",
|
"averageTimeOffPeriod": "Période de repos moyenne",
|
||||||
"longestTimeOffPeriod": "Plus longue période de repos",
|
"longestTimeOffPeriod": "Plus longue période de repos",
|
||||||
"weekendConnectedHolidays": "Jours fériés reliés à un week-end",
|
"weekendConnectedHolidays": "Jours fériés reliés à un week-end",
|
||||||
|
"weeksWithOneWorkday": "Semaines avec un seul jour travaillé",
|
||||||
|
"oneWorkdayWarning": "Attention : semaines avec un seul jour travaillé",
|
||||||
|
"oneWorkdayWarningHelp": "Travailler un seul jour dans une semaine complète est un rythme peu souhaitable.",
|
||||||
|
"onlyWorkday": "Seul jour travaillé",
|
||||||
"workingDays": "jours ouvrés",
|
"workingDays": "jours ouvrés",
|
||||||
"calendarDays": "jours calendaires",
|
"calendarDays": "jours calendaires",
|
||||||
"partialPlan": "Planning partiel",
|
"partialPlan": "Planning partiel",
|
||||||
|
|||||||
Reference in new issue
Block a user