feat: add PlanningDurationService for estimating planning duration

- Implemented PlanningDurationService to provide an indicative wait time before calculation based on user input and calendar settings.
- Introduced duration estimation logic that considers horizon dates, balances, and rules without triggering a calculation.
- Enhanced user input component to display estimated duration using the new service.
- Updated translation files to include new duration estimate strings in multiple languages.
- Added tests for PlanningDurationService to ensure accurate duration estimation and calibration.
- Refactored leave planner service to remove hardcoded deadlines and improve worker responsiveness.
This commit is contained in:
nessar committed 2026-10-05 17:06:17 +02:00
1 parent 2edf2004f5
commit bc8570d8ca
29 files changed
+1064 -123

No files matched your search

@@ -8,9 +8,22 @@ import {
describe('leave planner benchmark protocol', () => {
// Warm-up and two repetitions include complete trace delivery after planning.
it('keeps five deterministic scenarios and exercises a later earned consumer', async () => {
it('keeps seven deterministic scenarios including two and three years and a later earned consumer', async () => {
const scenarios = benchmarkScenarios();
expect(scenarios).toHaveLength(5);
expect(scenarios).toHaveLength(7);
for (const [name, end] of [
['two years', '2026-01-01'],
['three years', '2027-01-01'],
]) {
const scenario = scenarios.find((item) => item.name === name)!;
expect(scenario.request.horizon).toEqual({ start: '2024-01-01', end });
expect(
scenario.request.balances.some(
(item) => String(item.availableFrom) >= '2025-01-01',
),
).toBe(true);
expect(scenario.request.deadlineMs).toBeUndefined();
}
const typedObligations = scenarios.find(
(scenario) => scenario.name === 'typed obligations',
);
@@ -49,6 +62,19 @@ describe('leave planner benchmark protocol', () => {
).toHaveLength(2);
}
const measurements = await runLeavePlannerBenchmark(2);
for (const [name, placements] of [
['two years', 60],
['three years', 90],
] as const) {
const longer = measurements.filter((measurement) => measurement.scenario === name);
expect(longer).toHaveLength(2);
expect(longer.map((item) => item.outcome)).toEqual(['Plan', 'Plan']);
expect(longer.map((item) => item.lastCompletedStage)).toEqual(['dates', 'dates']);
expect(longer.map((item) => item.placements)).toEqual([placements, placements]);
expect(longer.map((item) => item.deterministic)).toEqual(['baseline', 'yes']);
expect(longer.map((item) => item.traceStatus)).toEqual(['ready', 'ready']);
expect(longer[0].traceCandidates).toBeGreaterThan(0);
}
const chained = measurements.find(
(measurement) => measurement.scenario === 'chained earned entitlements',
);
@@ -74,7 +100,7 @@ describe('leave planner benchmark protocol', () => {
expect(loaded[0].traceStatus).toBe('ready');
expect(loaded[0].traceCandidates).toBeGreaterThan(0);
const report = benchmarkReport(measurements);
expect(report.split('\n')).toHaveLength(11);
expect(report.split('\n')).toHaveLength(15);
expect(report).toContain('planning_ms\ttrace_delivery_ms');
expect(report).toContain('planning_work_ms\ttrace_capture_ms');
expect(report).toContain('\tbaseline\t');