feat(stats): show work and time-off lengths (#66)
This commit is contained in:
1 parent
d9f3f8d34a
commit
3fbf9c3172
12 files changed
+372
-19
No files matched your search
@@ -77,6 +77,9 @@ const plan: PlanOutcome = {
|
||||
holidayWeekValue: 0,
|
||||
nonWorkingDayGain: 4,
|
||||
longestWorkSequence: 6,
|
||||
averageWorkSequence: 6,
|
||||
averageTimeOffPeriod: null,
|
||||
longestTimeOffPeriod: null,
|
||||
shortSequenceDeficit: 0,
|
||||
supportingPlacements: 0,
|
||||
preferenceSatisfaction: [],
|
||||
@@ -413,13 +416,19 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
{ partial: false, placements: 0 },
|
||||
{ partial: true, placements: 0 },
|
||||
])(
|
||||
'opens leave-usage statistics independently of the trace (partial: $partial, placements: $placements)',
|
||||
'opens calculated statistics independently of the trace (partial: $partial, placements: $placements)',
|
||||
async ({ partial, placements }) => {
|
||||
const pendingTrace = deferred<PlanningTrace | undefined>();
|
||||
const validPlan: PlanOutcome = {
|
||||
...plan,
|
||||
placements: placements ? plan.placements : [],
|
||||
declaredDays: [{ date: '2024-05-07', type: 'rtt' }],
|
||||
metrics: {
|
||||
...plan.metrics,
|
||||
averageWorkSequence: 10 / 3,
|
||||
averageTimeOffPeriod: 2.5,
|
||||
longestTimeOffPeriod: 4,
|
||||
},
|
||||
};
|
||||
const outcome: PlanningOutcome = partial
|
||||
? {
|
||||
@@ -464,8 +473,20 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
fixture.detectChanges();
|
||||
const dialog = document.querySelector('[role="dialog"]')!;
|
||||
expect(dialog.textContent).toContain('Plan statistics');
|
||||
expect(dialog.querySelector('dt')?.textContent).toContain('New leave days');
|
||||
expect(dialog.querySelector('dd')?.textContent?.trim()).toBe(String(placements));
|
||||
const statistics = () =>
|
||||
Array.from(dialog.querySelectorAll('dt'), (label) => [
|
||||
label.textContent?.replace(/\s+/g, ' ').trim(),
|
||||
label.nextElementSibling?.textContent?.trim(),
|
||||
]);
|
||||
const expectedStatistics = [
|
||||
['New leave days', String(placements)],
|
||||
['Non-working days gained (calendar days)', '4'],
|
||||
['Average work sequence (working days)', '3.333'],
|
||||
['Longest work sequence (working days)', '6'],
|
||||
['Average time-off period (calendar days)', '2.5'],
|
||||
['Longest time-off period (calendar days)', '4'],
|
||||
];
|
||||
expect(statistics()).toEqual(expectedStatistics);
|
||||
expect(dialog.textContent?.includes('Partial plan')).toBe(partial);
|
||||
expect(input.planningOutcomeSignal()).toBe(outcome);
|
||||
expect(input.vacationNumberSignal()).toEqual(before.balances);
|
||||
@@ -477,9 +498,7 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
pendingTrace.reject(new Error('Trace delivery failed'));
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe(
|
||||
String(placements),
|
||||
);
|
||||
expect(statistics()).toEqual(expectedStatistics);
|
||||
input.updateBalances(() => []);
|
||||
input.toggleDeclaredDay('2024-05-10', 'other');
|
||||
rules.deleteRule(rules.rules[0].id);
|
||||
@@ -487,16 +506,61 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
input.declarationEntrySignal.set(true);
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe(
|
||||
String(placements),
|
||||
);
|
||||
expect(statistics()).toEqual(expectedStatistics);
|
||||
expect(input.planningOutcomeSignal()).toBe(outcome);
|
||||
expect(calendar.createPlanningCalculation).toHaveBeenCalledOnce();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
'distinguishes empty averages from zero work and gain in a valid plan (all workdays off: %s)',
|
||||
async (allDaysOff) => {
|
||||
const core = await import('../../services/leave-planner.core');
|
||||
const validPlan = core.calculatePlanningInternal({
|
||||
horizon: { start: '2024-01-08', end: '2024-01-13' },
|
||||
balances: [],
|
||||
rules: [],
|
||||
declaredDays: allDaysOff
|
||||
? ['08', '09', '10', '11', '12'].map((day) => ({
|
||||
date: `2024-01-${day}`,
|
||||
type: 'other',
|
||||
}))
|
||||
: [],
|
||||
});
|
||||
if (validPlan.kind !== 'Plan') throw new Error('Expected a valid plan.');
|
||||
const { calendar } = calendarFor(validPlan);
|
||||
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();
|
||||
expect(
|
||||
Array.from(document.querySelectorAll('[role="dialog"] dd'), (value) =>
|
||||
value.textContent?.trim(),
|
||||
),
|
||||
).toEqual(allDaysOff ? ['0', '4', '—', '0', '9', '9'] : ['0', '0', '5', '5', '—', '—']);
|
||||
},
|
||||
);
|
||||
|
||||
it('localizes statistics and supports keyboard dismissal with focus restored to its opener', async () => {
|
||||
const { calendar } = calendarFor(plan);
|
||||
const { calendar } = calendarFor({
|
||||
...plan,
|
||||
metrics: {
|
||||
...plan.metrics,
|
||||
averageWorkSequence: 10 / 3,
|
||||
averageTimeOffPeriod: 2.5,
|
||||
longestTimeOffPeriod: 4,
|
||||
},
|
||||
});
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CalendarYear],
|
||||
providers: [
|
||||
@@ -520,8 +584,19 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
expect(dialog.getAttribute('aria-labelledby')).toBe('plan-statistics-title');
|
||||
expect(dialog.getAttribute('aria-describedby')).toBe('plan-statistics-help');
|
||||
await vi.waitFor(() => expect(document.activeElement).toBe(dialog.querySelector('h2')));
|
||||
translation.setLanguage('fr');
|
||||
for (const [language, workUnit, calendarUnit] of [
|
||||
['de', 'Arbeitstage', 'Kalendertage'],
|
||||
['es', 'días laborables', 'días naturales'],
|
||||
['fr', 'jours ouvrés', 'jours calendaires'],
|
||||
]) {
|
||||
translation.setLanguage(language);
|
||||
fixture.detectChanges();
|
||||
expect(dialog.textContent).toContain(workUnit);
|
||||
expect(dialog.textContent).toContain(calendarUnit);
|
||||
expect(
|
||||
Array.from(dialog.querySelectorAll('dd'), (value) => value.textContent?.trim()),
|
||||
).toEqual(['2', '4', '3,333', '6', '2,5', '4']);
|
||||
}
|
||||
expect(dialog.textContent).toContain('Statistiques du planning');
|
||||
expect(dialog.querySelector('dt')?.textContent).toContain('Nouveaux jours de congé posés');
|
||||
expect(dialog.querySelector('button')?.textContent?.trim()).toBe('Fermer');
|
||||
@@ -2812,7 +2887,11 @@ describe('CalendarYear planning outcome boundary', () => {
|
||||
expect(root.querySelector('.trace-state-summary')?.textContent).toContain('1 placements');
|
||||
activateWithKeyboard(root.querySelector<HTMLButtonElement>('.statistics-button')!);
|
||||
await fixture.whenStable();
|
||||
expect(document.querySelector('[role="dialog"] dd')?.textContent?.trim()).toBe('2');
|
||||
expect(
|
||||
Array.from(document.querySelectorAll('[role="dialog"] dd'), (value) =>
|
||||
value.textContent?.trim(),
|
||||
),
|
||||
).toEqual(['2', '4', '6', '6', '—', '—']);
|
||||
document.querySelector<HTMLButtonElement>('[role="dialog"] button')!.click();
|
||||
await vi.waitFor(() => expect(document.querySelector('[role="dialog"]')).toBeNull());
|
||||
activateWithKeyboard(afterButton);
|
||||
|
||||
@@ -1257,6 +1257,44 @@
|
||||
<dt>{{ tr().planning.statistics.leaveDays }}</dt>
|
||||
<dd>{{ plan.placements.length }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
{{ tr().planning.daysGained }} ({{ tr().planning.statistics.calendarDays }})
|
||||
</dt>
|
||||
<dd>{{ formatStatistic(plan.metrics.nonWorkingDayGain) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
{{ tr().planning.statistics.averageWorkSequence }} ({{
|
||||
tr().planning.statistics.workingDays
|
||||
}})
|
||||
</dt>
|
||||
<dd>{{ formatStatistic(plan.metrics.averageWorkSequence) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
{{ tr().planning.longestWorkSequence }} ({{
|
||||
tr().planning.statistics.workingDays
|
||||
}})
|
||||
</dt>
|
||||
<dd>{{ formatStatistic(plan.metrics.longestWorkSequence) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
{{ tr().planning.statistics.averageTimeOffPeriod }} ({{
|
||||
tr().planning.statistics.calendarDays
|
||||
}})
|
||||
</dt>
|
||||
<dd>{{ formatStatistic(plan.metrics.averageTimeOffPeriod) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>
|
||||
{{ tr().planning.statistics.longestTimeOffPeriod }} ({{
|
||||
tr().planning.statistics.calendarDays
|
||||
}})
|
||||
</dt>
|
||||
<dd>{{ formatStatistic(plan.metrics.longestTimeOffPeriod) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
@@ -762,6 +762,10 @@ export class CalendarYear {
|
||||
this.userInput.remainingVacationDaysSignal.set([]);
|
||||
}
|
||||
|
||||
protected formatStatistic(value: number | null): string {
|
||||
return value === null ? '—' : value.toLocaleString(this.translationService.locale());
|
||||
}
|
||||
|
||||
protected openStatistics(): void {
|
||||
if (!this.displayedPlan() || this.statisticsDialog) return;
|
||||
const dialog = this.dialog.open(this.statisticsTemplate(), {
|
||||
|
||||
@@ -79,6 +79,13 @@ describe('user cancellation', () => {
|
||||
expect(outcome.reasons[0].code).toBe('user-cancelled');
|
||||
expect(outcome.incumbent.timeOffPeriods).toEqual(completed.timeOffPeriods);
|
||||
expect(outcome.incumbent.metrics).toEqual(completed.metrics);
|
||||
expect(outcome.incumbent.metrics).toMatchObject({
|
||||
nonWorkingDayGain: 2,
|
||||
averageWorkSequence: 2,
|
||||
longestWorkSequence: 3,
|
||||
averageTimeOffPeriod: 3,
|
||||
longestTimeOffPeriod: 3,
|
||||
});
|
||||
expect(artifacts.explanation).toMatchObject({
|
||||
kind: 'PlanExplanation',
|
||||
declaredDays: submitted.declaredDays,
|
||||
@@ -344,6 +351,36 @@ describe('user cancellation', () => {
|
||||
expect(await calculation.trace).toEqual(trace);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
'preserves calculated statistics from a retained checkpoint after worker failure (declared period: %s)',
|
||||
async (withPeriod) => {
|
||||
const core = await import('./leave-planner.core');
|
||||
const submitted = {
|
||||
horizon: { start: '2024-01-02', end: '2024-01-09' },
|
||||
balances: [],
|
||||
rules: [],
|
||||
declaredDays: withPeriod ? [{ date: '2024-01-05', type: 'other' as const }] : [],
|
||||
};
|
||||
const checkpoint = core.calculatePlanningInternal(submitted);
|
||||
if (checkpoint.kind !== 'Plan') throw new Error('Expected a valid checkpoint.');
|
||||
const calculation = createPlanningCalculation(submitted);
|
||||
currentWorker().emit({ type: 'checkpoint', plan: structuredClone(checkpoint) });
|
||||
currentWorker().onerror?.();
|
||||
const outcome = await calculation.outcome;
|
||||
if (outcome.kind !== 'PlanningIncomplete' || !outcome.incumbent)
|
||||
throw new Error('Expected a retained checkpoint.');
|
||||
expect(outcome.reasons[0].code).toBe('worker-error');
|
||||
expect(outcome.incumbent.metrics).toEqual(checkpoint.metrics);
|
||||
expect(outcome.incumbent.metrics).toMatchObject({
|
||||
nonWorkingDayGain: withPeriod ? 2 : 0,
|
||||
averageWorkSequence: withPeriod ? 2 : 5,
|
||||
longestWorkSequence: withPeriod ? 3 : 5,
|
||||
averageTimeOffPeriod: withPeriod ? 3 : null,
|
||||
longestTimeOffPeriod: withPeriod ? 3 : null,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('reports worker failure distinctly after a cancellation request', async () => {
|
||||
const calculation = createPlanningCalculation(request);
|
||||
calculation.cancel();
|
||||
|
||||
@@ -460,6 +460,9 @@ export interface PlanningMetrics {
|
||||
holidayWeekValue: number;
|
||||
nonWorkingDayGain: number;
|
||||
longestWorkSequence: number;
|
||||
averageWorkSequence: number | null;
|
||||
averageTimeOffPeriod: number | null;
|
||||
longestTimeOffPeriod: number | null;
|
||||
shortSequenceDeficit: number;
|
||||
supportingPlacements: number;
|
||||
preferenceSatisfaction: number[];
|
||||
@@ -5026,6 +5029,7 @@ function qualityMetrics(
|
||||
} else current++;
|
||||
}
|
||||
if (context.workingDays.length) sequences.push(current);
|
||||
const nonEmptySequences = sequences.filter((length) => length > 0);
|
||||
const internal = hasTimeOff ? sequences.slice(1, -1) : [];
|
||||
const shortSequenceDeficit = internal.reduce(
|
||||
(sum, length) => sum + (length > 0 && length < 5 ? 5 - length : 0),
|
||||
@@ -5036,6 +5040,15 @@ function qualityMetrics(
|
||||
holidayWeekValue: holidayWeekValue(state, context),
|
||||
nonWorkingDayGain: periods.reduce((sum, period) => sum + period.nonWorkingDayGain, 0),
|
||||
longestWorkSequence: sequences.length ? Math.max(...sequences) : 0,
|
||||
averageWorkSequence: nonEmptySequences.length
|
||||
? nonEmptySequences.reduce((sum, length) => sum + length, 0) / nonEmptySequences.length
|
||||
: null,
|
||||
averageTimeOffPeriod: periods.length
|
||||
? periods.reduce((sum, period) => sum + period.duration, 0) / periods.length
|
||||
: null,
|
||||
longestTimeOffPeriod: periods.length
|
||||
? Math.max(...periods.map((period) => period.duration))
|
||||
: null,
|
||||
shortSequenceDeficit,
|
||||
supportingPlacements: [...state.placements.values()].filter(
|
||||
(placement) => placement.supporting,
|
||||
|
||||
@@ -48,6 +48,129 @@ async function runInNonBrowser(request: Parameters<typeof planLeave>[0]) {
|
||||
beforeEach(() => vi.setSystemTime(new Date('2024-01-01T12:00:00Z')));
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
describe('calculated work-sequence and time-off-period statistics', () => {
|
||||
it('keeps fractional work means and merges consecutive declared days across a weekend', async () => {
|
||||
const result = isPlan(
|
||||
await runInNonBrowser({
|
||||
horizon: { start: '2024-01-02', end: '2024-01-11' },
|
||||
balances: [],
|
||||
rules: [],
|
||||
declaredDays: ['2024-01-05', '2024-01-08'].map((date) => ({
|
||||
date,
|
||||
type: 'other',
|
||||
})),
|
||||
}),
|
||||
);
|
||||
expect(result.metrics).toMatchObject({
|
||||
nonWorkingDayGain: 2,
|
||||
averageWorkSequence: 2.5,
|
||||
longestWorkSequence: 3,
|
||||
averageTimeOffPeriod: 4,
|
||||
longestTimeOffPeriod: 4,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
scenario: 'averages the 10, 5 and 15 working days including both horizon edges',
|
||||
horizon: { start: '2024-01-08', end: '2024-02-21' },
|
||||
declaredDates: ['2024-01-22', '2024-01-30'],
|
||||
metrics: {
|
||||
averageWorkSequence: 10,
|
||||
longestWorkSequence: 15,
|
||||
averageTimeOffPeriod: 2,
|
||||
longestTimeOffPeriod: 3,
|
||||
nonWorkingDayGain: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: 'keeps the unrounded mean of work sequences of 3, 1 and 6 days',
|
||||
horizon: { start: '2024-01-02', end: '2024-01-18' },
|
||||
declaredDates: ['2024-01-05', '2024-01-09'],
|
||||
metrics: {
|
||||
averageWorkSequence: 10 / 3,
|
||||
longestWorkSequence: 6,
|
||||
averageTimeOffPeriod: 2,
|
||||
longestTimeOffPeriod: 3,
|
||||
nonWorkingDayGain: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: 'averages one-day and two-day breaks without adding zero-length work samples',
|
||||
horizon: { start: '2024-01-02', end: '2024-01-12' },
|
||||
declaredDates: ['2024-01-03', '2024-01-09', '2024-01-10'],
|
||||
metrics: {
|
||||
averageWorkSequence: 5 / 3,
|
||||
longestWorkSequence: 3,
|
||||
averageTimeOffPeriod: 1.5,
|
||||
longestTimeOffPeriod: 2,
|
||||
nonWorkingDayGain: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: 'does not count or split work on intervening holidays and weekends',
|
||||
horizon: { start: '2024-05-06', end: '2024-05-15' },
|
||||
declaredDates: [],
|
||||
metrics: {
|
||||
averageWorkSequence: 5,
|
||||
longestWorkSequence: 5,
|
||||
averageTimeOffPeriod: null,
|
||||
longestTimeOffPeriod: null,
|
||||
nonWorkingDayGain: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario:
|
||||
'retains an empty work average and a measured zero when every workday is declared',
|
||||
horizon: { start: '2024-01-08', end: '2024-01-13' },
|
||||
declaredDates: ['2024-01-08', '2024-01-09', '2024-01-10', '2024-01-11', '2024-01-12'],
|
||||
metrics: {
|
||||
averageWorkSequence: null,
|
||||
longestWorkSequence: 0,
|
||||
averageTimeOffPeriod: 9,
|
||||
longestTimeOffPeriod: 9,
|
||||
nonWorkingDayGain: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario: 'distinguishes no samples from zero gain in a weekend-only horizon',
|
||||
horizon: { start: '2024-01-06', end: '2024-01-08' },
|
||||
declaredDates: [],
|
||||
metrics: {
|
||||
averageWorkSequence: null,
|
||||
longestWorkSequence: 0,
|
||||
averageTimeOffPeriod: null,
|
||||
longestTimeOffPeriod: null,
|
||||
nonWorkingDayGain: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
scenario:
|
||||
'counts Christmas on a Saturday once alongside its Sunday in a declared break',
|
||||
horizon: { start: '2021-12-20', end: '2021-12-28' },
|
||||
declaredDates: ['2021-12-24'],
|
||||
metrics: {
|
||||
averageWorkSequence: 2.5,
|
||||
longestWorkSequence: 4,
|
||||
averageTimeOffPeriod: 3,
|
||||
longestTimeOffPeriod: 3,
|
||||
nonWorkingDayGain: 2,
|
||||
},
|
||||
},
|
||||
])('$scenario', async ({ horizon, declaredDates, metrics }) => {
|
||||
const result = isPlan(
|
||||
await runInNonBrowser({
|
||||
horizon,
|
||||
balances: [],
|
||||
rules: [],
|
||||
declaredDays: declaredDates.map((date) => ({ date, type: 'other' })),
|
||||
}),
|
||||
);
|
||||
expect(result.placements).toEqual([]);
|
||||
expect(result.metrics).toMatchObject(metrics);
|
||||
});
|
||||
});
|
||||
|
||||
describe('declared holiday patterns and captured entitlement date', () => {
|
||||
const producer = {
|
||||
id: 'producer',
|
||||
@@ -1214,7 +1337,13 @@ describe('planLeave', () => {
|
||||
nonWorkingDayGain: 2,
|
||||
},
|
||||
]);
|
||||
expect(result.metrics).toMatchObject({ nonWorkingDayGain: 2, longestWorkSequence: 3 });
|
||||
expect(result.metrics).toMatchObject({
|
||||
nonWorkingDayGain: 2,
|
||||
averageWorkSequence: 2,
|
||||
longestWorkSequence: 3,
|
||||
averageTimeOffPeriod: 3,
|
||||
longestTimeOffPeriod: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('merges declarations, placements, weekends and national holidays without counting dates twice', async () => {
|
||||
@@ -1254,7 +1383,14 @@ describe('planLeave', () => {
|
||||
nonWorkingDayGain: 6,
|
||||
},
|
||||
]);
|
||||
expect(result.metrics).toMatchObject({ nonWorkingDayGain: 6, holidayWeekValue: 5 });
|
||||
expect(result.metrics).toMatchObject({
|
||||
nonWorkingDayGain: 6,
|
||||
holidayWeekValue: 5,
|
||||
averageWorkSequence: null,
|
||||
longestWorkSequence: 0,
|
||||
averageTimeOffPeriod: 10,
|
||||
longestTimeOffPeriod: 10,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses declared days to split work sequences while weekends and holidays alone never split them', async () => {
|
||||
@@ -1319,6 +1455,13 @@ describe('planLeave', () => {
|
||||
declaredDates: ['2024-01-02', '2024-01-03', '2024-01-05'],
|
||||
nonWorkingDayGain: 5,
|
||||
});
|
||||
expect(result.metrics).toMatchObject({
|
||||
nonWorkingDayGain: 5,
|
||||
averageWorkSequence: null,
|
||||
longestWorkSequence: 0,
|
||||
averageTimeOffPeriod: 9,
|
||||
longestTimeOffPeriod: 9,
|
||||
});
|
||||
const unanchored = isPlan(
|
||||
await runInNonBrowser({
|
||||
horizon: selectedHorizon,
|
||||
@@ -1328,7 +1471,13 @@ describe('planLeave', () => {
|
||||
}),
|
||||
);
|
||||
expect(unanchored.timeOffPeriods).toEqual([]);
|
||||
expect(unanchored.metrics.nonWorkingDayGain).toBe(0);
|
||||
expect(unanchored.metrics).toMatchObject({
|
||||
nonWorkingDayGain: 0,
|
||||
averageWorkSequence: 2,
|
||||
longestWorkSequence: 2,
|
||||
averageTimeOffPeriod: null,
|
||||
longestTimeOffPeriod: null,
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -197,6 +197,13 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
||||
: undefined;
|
||||
if (!plan) throw new Error('Expected a valid earned-chain plan.');
|
||||
expect(plan.calculationDate).toBe('2024-05-01');
|
||||
expect(plan.metrics).toMatchObject({
|
||||
nonWorkingDayGain: 5,
|
||||
averageWorkSequence: 1,
|
||||
longestWorkSequence: 1,
|
||||
averageTimeOffPeriod: 4.5,
|
||||
longestTimeOffPeriod: 5,
|
||||
});
|
||||
expect(plan.placements).toMatchObject([
|
||||
{
|
||||
date: '2024-05-03',
|
||||
@@ -250,6 +257,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
||||
throw new Error(`Expected the same direct plan: ${JSON.stringify(direct)}`);
|
||||
expect(plan.placements).toEqual(direct.placements);
|
||||
expect(plan.ifThenContributions).toEqual(direct.ifThenContributions);
|
||||
expect(plan.metrics).toEqual(direct.metrics);
|
||||
const root: HTMLElement = fixture.nativeElement;
|
||||
expect(root.textContent).toContain('Droit futur généré');
|
||||
expect(root.textContent).toContain('CP autour du férié');
|
||||
|
||||
@@ -131,6 +131,11 @@ export interface AppTranslations {
|
||||
open: string;
|
||||
title: string;
|
||||
leaveDays: string;
|
||||
averageWorkSequence: string;
|
||||
averageTimeOffPeriod: string;
|
||||
longestTimeOffPeriod: string;
|
||||
workingDays: string;
|
||||
calendarDays: string;
|
||||
partialPlan: string;
|
||||
help: string;
|
||||
};
|
||||
|
||||
+6
-1
@@ -140,8 +140,13 @@
|
||||
"open": "Statistik",
|
||||
"title": "Planstatistik",
|
||||
"leaveDays": "Neu geplante Urlaubstage",
|
||||
"averageWorkSequence": "Durchschnittliche Arbeitsphase",
|
||||
"averageTimeOffPeriod": "Durchschnittliche Ruhephase",
|
||||
"longestTimeOffPeriod": "Längste Ruhephase",
|
||||
"workingDays": "Arbeitstage",
|
||||
"calendarDays": "Kalendertage",
|
||||
"partialPlan": "Teilplan",
|
||||
"help": "Urlaubsverbrauch des berechneten Plans. Bereits gebuchte Tage sind ausgeschlossen."
|
||||
"help": "Statistik des berechneten Plans. Bereits gebuchte Tage prägen Ruhe- und Arbeitsphasen, ohne das verbleibende Guthaben erneut zu verbrauchen."
|
||||
},
|
||||
"calculationDate": "Berechnungsdatum",
|
||||
"futureEntitlement": "Erzeugter künftiger Anspruch",
|
||||
|
||||
+6
-1
@@ -140,8 +140,13 @@
|
||||
"open": "Statistics",
|
||||
"title": "Plan statistics",
|
||||
"leaveDays": "New leave days",
|
||||
"averageWorkSequence": "Average work sequence",
|
||||
"averageTimeOffPeriod": "Average time-off period",
|
||||
"longestTimeOffPeriod": "Longest time-off period",
|
||||
"workingDays": "working days",
|
||||
"calendarDays": "calendar days",
|
||||
"partialPlan": "Partial plan",
|
||||
"help": "Leave usage for the calculated plan. Already booked days are excluded."
|
||||
"help": "Statistics for the calculated plan. Already booked days shape breaks and working stretches without consuming remaining balances."
|
||||
},
|
||||
"calculationDate": "Calculation date",
|
||||
"futureEntitlement": "Future entitlement generated",
|
||||
|
||||
+6
-1
@@ -140,8 +140,13 @@
|
||||
"open": "Estadísticas",
|
||||
"title": "Estadísticas del plan",
|
||||
"leaveDays": "Nuevos días de permiso",
|
||||
"averageWorkSequence": "Secuencia de trabajo media",
|
||||
"averageTimeOffPeriod": "Período de descanso medio",
|
||||
"longestTimeOffPeriod": "Período de descanso más largo",
|
||||
"workingDays": "días laborables",
|
||||
"calendarDays": "días naturales",
|
||||
"partialPlan": "Plan parcial",
|
||||
"help": "Permisos utilizados por el plan calculado. Se excluyen los días ya reservados."
|
||||
"help": "Estadísticas del plan calculado. Los días ya reservados definen los períodos de descanso y trabajo sin consumir de nuevo los saldos restantes."
|
||||
},
|
||||
"calculationDate": "Fecha del cálculo",
|
||||
"futureEntitlement": "Derecho futuro generado",
|
||||
|
||||
+6
-1
@@ -140,8 +140,13 @@
|
||||
"open": "Statistiques",
|
||||
"title": "Statistiques du planning",
|
||||
"leaveDays": "Nouveaux jours de congé posés",
|
||||
"averageWorkSequence": "Séquence de travail moyenne",
|
||||
"averageTimeOffPeriod": "Période de repos moyenne",
|
||||
"longestTimeOffPeriod": "Plus longue période de repos",
|
||||
"workingDays": "jours ouvrés",
|
||||
"calendarDays": "jours calendaires",
|
||||
"partialPlan": "Planning partiel",
|
||||
"help": "Congés utilisés par le planning calculé. Les jours déjà posés sont exclus."
|
||||
"help": "Statistiques du planning calculé. Les jours déjà posés contribuent aux périodes de repos et de travail sans consommer à nouveau les soldes."
|
||||
},
|
||||
"calculationDate": "Date du calcul",
|
||||
"futureEntitlement": "Droit futur généré",
|
||||
|
||||
Reference in new issue
Block a user