Add cadence target calculation for timeframe-based goal progress resolution:

- Implemented `computeCadenceTargetForTimeframe` utility to calculate dynamic targets based on timeframe and cadence criteria.
- Extended tests to validate timeframe-specific progress key selection, slice resolution, and cadence-based targets.
- Refactored `DepartmentGoals` to integrate cadence-aware target resolution and fallback logic.
- Enhanced e2e tests to cover bi-weekly cadence goals and dynamic target updates across timeframes.
This commit is contained in:
Jeppe Bundgaard
2026-03-24 16:32:35 +01:00
parent bc043b95f9
commit 477688d16e
4 changed files with 292 additions and 8 deletions
+112 -8
View File
@@ -4,8 +4,12 @@ import { bookingTestData, loginAsOperator } from './fixtures';
type GoalProgressBucket = {
count: number;
target: number;
date_from?: string;
date_end?: string;
};
type GoalProgressByKey = Record<string, GoalProgressBucket>;
type MockGoal = {
id: number;
created_by: number;
@@ -27,15 +31,11 @@ type MockGoal = {
progress_alert_weekdays: string[];
progress_alert_time_of_day: string | null;
department_daily_targets: Record<string, number>;
target_duration?: 'ENTIRE_DURATION' | 'WEEKS' | 'MONTHS' | 'YEARS' | null;
target_duration_every?: number | null;
};
progress: {
all: GoalProgressBucket;
today: GoalProgressBucket;
week: GoalProgressBucket;
month: GoalProgressBucket;
year: GoalProgressBucket;
to_date: GoalProgressBucket;
departmental_distribution: Record<string, Record<'all' | 'today' | 'week' | 'month' | 'year' | 'to_date', GoalProgressBucket>>;
progress: GoalProgressByKey & {
departmental_distribution: Record<string, GoalProgressByKey>;
};
created_at: string;
updated_at: string;
@@ -321,6 +321,110 @@ test.describe('Admin Module - Goals', () => {
await expect(timeframeTabs.locator('li').nth(0)).toHaveClass(/is-active/);
});
test('updates progress target when timeframe changes for cadence goals', async ({ page }) => {
const departmentId = bookingTestData.departmentId;
const nowIso = new Date().toISOString();
const cadenceGoal: MockGoal = {
id: 301,
created_by: 11,
departments: [departmentId],
criteria: {
label: 'Cadence Biweekly Goal',
type: 'PRODUCT',
target: 7,
start: '2026-01-01T00:00:00+01:00',
end: '2026-12-31T23:59:59+01:00',
products: [1],
users: [],
departments: [departmentId],
progress_alert_frequency: 'NONE',
progress_alert_destination: 'NONE',
progress_alert_progress_type: 'ALL',
progress_alert_style: 'NONE',
progress_alert_format: null,
progress_alert_weekdays: ['MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', 'SUNDAY'],
progress_alert_time_of_day: null,
department_daily_targets: {},
target_duration: 'WEEKS',
target_duration_every: 2,
},
progress: {
all: { count: 11, target: 189, date_from: '2026-01-01T00:00:00+01:00', date_end: '2026-12-31T23:59:59+01:00' },
to_date: { count: 6, target: 42, date_from: '2026-01-01T00:00:00+01:00', date_end: '2026-03-24T23:59:59+01:00' },
today: { count: 1, target: 2, date_from: '2026-04-04T00:00:00+01:00', date_end: '2026-04-04T23:59:59+01:00' },
week: { count: 4, target: 7, date_from: '2026-03-23T00:00:00+01:00', date_end: '2026-03-29T23:59:59+01:00' },
month: { count: 9, date_from: '2026-04-01T00:00:00+01:00', date_end: '2026-04-30T23:59:59+01:00' },
year: { count: 20, date_from: '2026-01-01T00:00:00+01:00', date_end: '2026-12-31T23:59:59+01:00' },
every_2_weeks: { count: 5, target: 14, date_from: '2026-03-12T00:00:00+01:00', date_end: '2026-03-25T23:59:59+01:00' },
departmental_distribution: {
[String(departmentId)]: {
all: { count: 11, target: 189 },
to_date: { count: 6, target: 42 },
today: { count: 1, target: 2 },
week: { count: 4, target: 7 },
month: { count: 9 },
year: { count: 20 },
every_2_weeks: { count: 5, target: 14 },
},
},
},
created_at: nowIso,
updated_at: nowIso,
};
await page.unroute('**/goals/department**');
await page.route('**/goals/department**', async (route) => {
const method = route.request().method();
if (method === 'GET') {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: [cadenceGoal] }),
});
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ data: {} }),
});
});
await page.reload();
const timeframeTabs = page.locator('.tabs.is-toggle').nth(1);
const card = page.locator('.goal-card', { hasText: 'Cadence Biweekly Goal' }).first();
const progressBar = card.locator('progress.progress').first();
const expectProgress = async (value: number, max: number) => {
await expect(progressBar).toHaveAttribute('value', new RegExp(`^${value}(?:\\.0+)?$`));
await expect(progressBar).toHaveAttribute('max', new RegExp(`^${max}(?:\\.0+)?$`));
};
await expectProgress(11, 189);
await timeframeTabs.locator('li').nth(1).click(); // Indtil nu
await expect(timeframeTabs.locator('li').nth(1)).toHaveClass(/is-active/);
await expectProgress(6, 42);
await timeframeTabs.locator('li').nth(2).click(); // I dag
await expect(timeframeTabs.locator('li').nth(2)).toHaveClass(/is-active/);
await expectProgress(1, 2);
await timeframeTabs.locator('li').nth(3).click(); // Denne uge
await expect(timeframeTabs.locator('li').nth(3)).toHaveClass(/is-active/);
await expectProgress(5, 14);
await timeframeTabs.locator('li').nth(4).click(); // Denne måned
await expect(timeframeTabs.locator('li').nth(4)).toHaveClass(/is-active/);
await expectProgress(9, 21);
await timeframeTabs.locator('li').nth(5).click(); // År
await expect(timeframeTabs.locator('li').nth(5)).toHaveClass(/is-active/);
await expectProgress(20, 189);
});
test('opens create goal modal and supports cancel', async ({ page }) => {
await openCreateGoalModalProgrammatically(page, bookingTestData.departmentId);