import { expect, test } from "@playwright/test"; import { mockApi, seedAuthenticatedState } from "./support/network.js"; const adminPermissions = [ "admin", "list_department_daily_reports", "list_bookings", "department_access_1", "department_access_2", "department_access_3", ]; const json = (body: unknown, status = 200) => ({ status, contentType: "application/json", body: JSON.stringify(body), }); const departments = [ { id: 1, name: "North" }, { id: 2, name: "South" }, { id: 3, name: "Missing Hours" }, ]; const overviewByRange: Record> = { "2026-03-23|2026-03-24": { department_ids: [1, 2, 3], date: "2026-03-23", date_to: "2026-03-24", metrics: { bookings: { state: "ready", value: 9, out_of: 12, message: null }, complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu.", }, night_washes: { state: "ready", value: 5, out_of: null, message: null, by_source: { orders: 1, xlvask: 2, selfserve: 2, }, has_missing_opening_hours: true, missing_department_ids: [3], }, revenue: { state: "ready", value: 5400, out_of: null, message: null }, washes: { state: "ready", value: 18, out_of: null, message: null }, products_sold: { state: "ready", value: 26, out_of: null, message: null }, transactions: { state: "ready", value: 15, out_of: null, message: null }, water_usage: { state: "ready", value: 44, out_of: null, message: null }, overtime: { state: "ready", value: 2.5, out_of: null, message: null }, }, products: [], }, "2026-03-23|2026-03-25": { department_ids: [1, 2, 3], date: "2026-03-23", date_to: "2026-03-25", metrics: { bookings: { state: "ready", value: 12, out_of: 16, message: null }, complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu.", }, night_washes: { state: "ready", value: 7, out_of: null, message: null, by_source: { orders: 2, xlvask: 2, selfserve: 3, }, has_missing_opening_hours: true, missing_department_ids: [3], }, revenue: { state: "ready", value: 6900, out_of: null, message: null }, washes: { state: "ready", value: 23, out_of: null, message: null }, products_sold: { state: "ready", value: 31, out_of: null, message: null }, transactions: { state: "ready", value: 19, out_of: null, message: null }, water_usage: { state: "ready", value: 58, out_of: null, message: null }, overtime: { state: "ready", value: 3.25, out_of: null, message: null }, }, products: [], }, }; const transactionCountsByRange: Record>> = { "2026-03-23|2026-03-24": { 1: { earnings: 2200, products: 9, quantity: 6, washes: 8, outside_hours: { total: 2, by_source: { orders: 1, xlvask: 1, selfserve: 0 }, has_missing_opening_hours: false, missing_department_ids: [], }, }, 2: { earnings: 3200, products: 12, quantity: 9, washes: 10, outside_hours: { total: 3, by_source: { orders: 0, xlvask: 1, selfserve: 2 }, has_missing_opening_hours: false, missing_department_ids: [], }, }, 3: { earnings: 500, products: 2, quantity: 1, washes: 2, outside_hours: { total: 0, by_source: { orders: 0, xlvask: 0, selfserve: 0 }, has_missing_opening_hours: true, missing_department_ids: [3], }, }, }, "2026-03-23|2026-03-25": { 1: { earnings: 2700, products: 11, quantity: 8, washes: 10, outside_hours: { total: 3, by_source: { orders: 1, xlvask: 1, selfserve: 1 }, has_missing_opening_hours: false, missing_department_ids: [], }, }, 2: { earnings: 3700, products: 14, quantity: 10, washes: 11, outside_hours: { total: 4, by_source: { orders: 1, xlvask: 1, selfserve: 2 }, has_missing_opening_hours: false, missing_department_ids: [], }, }, 3: { earnings: 500, products: 2, quantity: 1, washes: 2, outside_hours: { total: 0, by_source: { orders: 0, xlvask: 0, selfserve: 0 }, has_missing_opening_hours: true, missing_department_ids: [3], }, }, }, }; const trendByRange: Record> = { "2026-03-23|2026-03-24": { points: [ { date: "2026-03-23", total: 2, by_source: { orders: 1, xlvask: 0, selfserve: 1, }, }, { date: "2026-03-24", total: 3, by_source: { orders: 0, xlvask: 2, selfserve: 1, }, }, ], has_missing_opening_hours: true, missing_department_ids: [3], }, "2026-03-23|2026-03-25": { points: [ { date: "2026-03-23", total: 2, by_source: { orders: 1, xlvask: 0, selfserve: 1, }, }, { date: "2026-03-24", total: 3, by_source: { orders: 0, xlvask: 2, selfserve: 1, }, }, { date: "2026-03-25", total: 2, by_source: { orders: 1, xlvask: 0, selfserve: 1, }, }, ], has_missing_opening_hours: true, missing_department_ids: [3], }, }; async function mockOverviewDependencies(page) { await seedAuthenticatedState(page); await mockApi(page, { authenticated: true, permissions: adminPermissions, }); await page.route(/\/departments(\?.*)?$/i, async (route) => { const url = new URL(route.request().url()); const departmentId = Number(url.searchParams.get("id")); if (departmentId > 0) { await route.fulfill( json({ data: departments.find((department) => department.id === departmentId) || null, }) ); return; } await route.fulfill(json({ data: departments })); }); await page.route(/\/departments\/weather(\?.*)?$/i, async (route) => { await route.fulfill(json({ data: [] })); }); await page.route(/\/departments\/daily-reports\/get(\?.*)?$/i, async (route) => { const url = new URL(route.request().url()); const date = url.searchParams.get("date") || "2026-03-23"; await route.fulfill( json({ data: { id: 10, department_id: 1, water_usage: 32, notes: "", filled_by: 1, created_at: `${date} 00:00:00`, }, }) ); }); await page.route(/\/departments\/daily-reports\/overview(\?.*)?$/i, async (route) => { const url = new URL(route.request().url()); const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to") || url.searchParams.get("date")}`; await route.fulfill( json({ data: overviewByRange[key] || overviewByRange["2026-03-23|2026-03-24"], }) ); }); await page.route(/\/departments\/daily-reports\/transaction-count(\?.*)?$/i, async (route) => { const url = new URL(route.request().url()); const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to") || url.searchParams.get("date")}`; const departmentId = Number(url.searchParams.get("department_id")); const payload = transactionCountsByRange[key]?.[departmentId] || transactionCountsByRange["2026-03-23|2026-03-24"][departmentId]; await route.fulfill( json({ data: payload, }) ); }); await page.route(/\/departments\/daily-reports\/outside-hours-trend(\?.*)?$/i, async (route) => { const url = new URL(route.request().url()); const key = `${url.searchParams.get("date")}|${url.searchParams.get("date_to")}`; await route.fulfill( json({ data: trendByRange[key] || trendByRange["2026-03-23|2026-03-24"], }) ); }); await page.route( /\/departments\/daily-reports(?!\/overview|\/transaction-count|\/outside-hours-trend|\/get)(\?.*)?$/i, async (route) => { await route.fulfill(json({ data: [] })); } ); } async function expandOverviewReportsOnMobile(page, testInfo) { if (!testInfo.project.use.isMobile) { return; } if (await page.getByTestId("daily-report-page").count()) { return; } await page.getByTestId("admin-mobile-reports-toggle").click(); await expect(page.getByTestId("daily-report-page")).toBeVisible(); } test.describe("Admin overview night washes", () => { test("renders deduped Døgnvask totals across cards, list, and chart with stable range updates", async ({ page, }, testInfo) => { await mockOverviewDependencies(page); await page.goto("/admin?departmentIds=1,2,3&dateFrom=2026-03-23&dateTo=2026-03-24"); await expandOverviewReportsOnMobile(page, testInfo); await expect(page.getByTestId("daily-report-page")).toBeVisible(); await expect(page.getByTestId("daily-report-tile-night-washes")).toContainText("5"); await expect(page.getByTestId("daily-report-night-washes-breakdown")).toContainText("Ordre: 1"); await expect(page.getByTestId("daily-report-night-washes-breakdown")).toContainText("XL Vask: 2"); await expect(page.getByTestId("daily-report-night-washes-breakdown")).toContainText("Self-serve: 2"); await expect(page.getByTestId("daily-report-night-washes-warning")).toContainText("Missing Hours"); const tabs = page.locator(".tabs li"); await tabs.nth(1).click(); await expect(page.getByTestId("overview-summary-night-washes-total")).toContainText("5"); await expect(page.getByTestId("overview-summary-night-washes-breakdown")).toContainText("Self-serve: 2"); await expect(page.getByTestId("overview-summary-night-washes-warning")).toContainText("Missing Hours"); await expect(page.getByTestId("overview-department-1-night-washes-total")).toContainText("2"); await expect(page.getByTestId("overview-department-2-night-washes-total")).toContainText("3"); await expect(page.getByTestId("overview-department-3-night-washes-total")).toContainText("0"); await tabs.nth(2).click(); await expect(page.getByTestId("outside-hours-chart")).toBeVisible(); await expect(page.getByTestId("outside-hours-chart-warning")).toContainText("Missing Hours"); const endDateInput = page.locator("[data-testid='date-period-end']:visible").first(); await endDateInput.fill("2026-03-25"); await endDateInput.blur(); await tabs.nth(0).click(); await expect(page.getByTestId("daily-report-tile-night-washes")).toContainText("7"); await expect(page.getByTestId("daily-report-night-washes-breakdown")).toContainText("Self-serve: 3"); await tabs.nth(1).click(); await expect(page.getByTestId("overview-summary-night-washes-total")).toContainText("7"); await expect(page.getByTestId("overview-summary-night-washes-breakdown")).toContainText("Ordre: 2"); await expect(page.getByTestId("overview-summary-night-washes-breakdown")).toContainText("XL Vask: 2"); await expect(page.getByTestId("overview-summary-night-washes-breakdown")).toContainText("Self-serve: 3"); await tabs.nth(2).click(); await expect(page.getByTestId("outside-hours-chart")).toBeVisible(); if (testInfo.project.use.isMobile) { await tabs.nth(0).click(); await expect(page.getByTestId("daily-report-tile-night-washes")).toBeVisible(); await tabs.nth(1).click(); await expect(page.getByTestId("overview-summary-night-washes-total")).toBeVisible(); } }); });