import { expect, test } from "@playwright/test"; import { mockApi, seedAuthenticatedState } from "./support/network.js"; const adminPermissions = [ "admin", "create_department_daily_report_complaints", "department_access_1", "department_access_2", ]; const json = (body: unknown, status = 200) => ({ status, contentType: "application/json", body: JSON.stringify(body), }); const formatDisplayedDate = (isoDate: string) => { const [year, month, day] = isoDate.split("-"); return `${day}/${month}/${year}`; }; const buildOverviewPayload = ({ complaintsState = "ready", complaintsValue = 2, complaintsMessage = null, metrics = {}, products = [ { product_id: 24, slug: "spot-free-lastbil", title: "Spot Free (Lastbil)", state: "ready", value: 3, out_of: 8 }, { product_id: 25, slug: "faelg-flex", title: "Fælg flex pr. enhed", state: "ready", value: 2, out_of: 8 }, { product_id: 27, slug: "extraordinary-10-min", title: "Ekstraordinær pr. 10 min inkl. kemi", state: "ready", value: 1, out_of: 8 }, { product_id: 26, slug: "hoejglans", title: "Højglans - Voksforsegling pr. enhed", state: "ready", value: 4, out_of: 8 }, { product_id: 21, slug: "undervognsskyl", title: "Undervognsskyl pr. enhed", state: "ready", value: 2, out_of: 8 }, { product_id: 22, slug: "double-duty-kemi", title: "Tillæg for Specialsæbe - DD", state: "ready", value: 5, out_of: 8 }, ], ...overrides }: Record = {}) => ({ data: { department_ids: [1], date: "2026-03-23", date_to: "2026-03-23", metrics: { bookings: { state: "ready", value: 4, out_of: 6, message: null }, complaints: { state: complaintsState, value: complaintsState === "ready" ? complaintsValue : null, out_of: null, message: complaintsMessage, }, night_washes: { state: "ready", value: 1, out_of: null, message: null }, revenue: { state: "ready", value: 2400, out_of: null, message: null }, washes: { state: "ready", value: 8, out_of: null, message: null }, products_sold: { state: "ready", value: 19, out_of: null, message: null }, transactions: { state: "ready", value: 11, out_of: null, message: null }, water_usage: { state: "ready", value: 32, out_of: null, message: null }, overtime: { state: "ready", value: 1.5, out_of: null, message: null }, ...metrics, }, products, ...overrides, }, }); async function mockDailyReportDependencies( page, input: { overviewHandler: (url: URL) => Promise> | ReturnType; complaintHandler?: (body: Record) => Promise> | ReturnType; complaintCustomerLookupHandler?: (url: URL) => Promise> | ReturnType; permissions?: string[]; } | ((url: URL) => Promise> | ReturnType) ) { const options = typeof input === "function" ? { overviewHandler: input } : input; const complaintHandler = options.complaintHandler || (async (body) => json({ data: { id: 99, department_id: body.department_id, customer_number: body.customer_number ?? null, wash_date: body.wash_date ?? null, category: body.category ?? null, description: body.description, created_by: 1, created_at: "2026-04-08 10:00:00", }, })); const complaintCustomerLookupHandler = options.complaintCustomerLookupHandler || (async () => json({ data: [] })); await seedAuthenticatedState(page); await mockApi(page, { authenticated: true, permissions: options.permissions || adminPermissions, }); await page.route(/\/departments(\?.*)?$/i, async (route) => { await route.fulfill(json({ data: [ { id: 1, name: "North" }, { id: 2, name: "South" }, ], })); }); 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\/complaints$/i, async (route) => { const body = route.request().postDataJSON() as Record; const response = await complaintHandler(body); await route.fulfill(response); }); await page.route(/\/departments\/daily-reports\/complaints\/customers(\?.*)?$/i, async (route) => { const url = new URL(route.request().url()); const response = await complaintCustomerLookupHandler(url); await route.fulfill(response); }); await page.route(/\/departments\/daily-reports(?:\/overview)?(\?.*)?$/i, async (route) => { const url = new URL(route.request().url()); if (url.pathname.endsWith("/departments/daily-reports/overview")) { const response = await options.overviewHandler(url); await route.fulfill(response); return; } await route.fulfill(json({ data: [] })); }); } async function expandDepartmentFiltersOnMobile(page, testInfo) { if (!testInfo.project.use.isMobile) { return; } if (await page.getByTestId("daily-report-department-controls").count()) { return; } await page.getByTestId("daily-report-mobile-departments-toggle").click(); await expect(page.getByTestId("daily-report-department-controls")).toBeVisible(); } test.describe("Admin daily report", () => { test("renders every daily report tile from the overview payload", async ({ page }) => { await mockDailyReportDependencies(page, async () => json(buildOverviewPayload())); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); await expect(page.locator("[data-testid^='daily-report-tile-']")).toHaveCount(17); await expect(page.getByTestId("daily-report-tile-bookings")).toContainText("4"); await expect(page.getByTestId("daily-report-tile-complaints")).toContainText("2"); await expect(page.getByTestId("daily-report-complaints-add-button")).toBeVisible(); await expect(page.getByTestId("daily-report-tile-night-washes")).toContainText("1"); await expect(page.getByTestId("daily-report-tile-overtime")).toContainText("1,5"); await expect(page.getByTestId("daily-report-tile-spot-free-lastbil")).toContainText("Spot Free (Lastbil)"); await expect(page.getByTestId("daily-report-tile-spot-free-lastbil")).toContainText("3"); await expect(page.getByTestId("daily-report-tile-faelg-flex")).toContainText("Fælg flex pr. enhed"); await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText("Ekstraordinær pr. 10 min inkl. kemi"); await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed"); await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed"); await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD"); }); test("opens and closes the complaints modal, requires wash date/category on multi-day ranges, clears a selected customer, submits without customer number, and refreshes the count", async ({ page }) => { const complaintRequests: Array> = []; let complaintCount = 2; await mockDailyReportDependencies(page, { overviewHandler: async () => json(buildOverviewPayload({ complaintsValue: complaintCount })), complaintHandler: async (body) => { complaintRequests.push(body); complaintCount = 3; return json({ data: { id: 501, department_id: body.department_id, customer_number: null, wash_date: body.wash_date, category: body.category, description: body.description, created_by: 1, created_at: "2026-04-08 10:00:00", }, }); }, complaintCustomerLookupHandler: async () => json({ data: [ { customer_number: 12345, customer_name: "Acme Transport" }, ], }), }); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-24"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); await expect(page.getByTestId("daily-report-tile-complaints")).toContainText("2"); const washDateInput = page.getByTestId("daily-report-complaints-wash-date-input"); await page.getByTestId("daily-report-complaints-add-button").click(); await expect(page.getByTestId("daily-report-complaints-modal")).toBeVisible(); await expect(page.getByTestId("daily-report-complaints-department-select")).toHaveValue("1"); await expect(washDateInput).toHaveValue(""); await expect(page.getByTestId("daily-report-complaints-date-note")).toContainText("konkrete vask"); await page.getByTestId("daily-report-complaints-cancel").click(); await expect(page.getByTestId("daily-report-complaints-modal")).toHaveCount(0); await page.getByTestId("daily-report-complaints-add-button").click(); await page.getByTestId("daily-report-complaints-customer-search-input").fill("ac"); await expect(page.getByTestId("daily-report-complaints-customer-option-12345")).toBeVisible(); await expect(page.getByTestId("daily-report-complaints-description-textarea")).toBeVisible(); await page.getByTestId("daily-report-complaints-customer-option-12345").click(); await expect(page.getByTestId("daily-report-complaints-customer-selected")).toHaveValue("Acme Transport - 12345"); await page.getByTestId("daily-report-complaints-customer-clear").click(); await expect(page.getByTestId("daily-report-complaints-customer-search-input")).toBeVisible(); await page.getByTestId("daily-report-complaints-submit").click(); await expect(page.getByTestId("daily-report-complaints-validation-error")).toContainText("Dato for vask"); await washDateInput.fill("24/03/2026"); await washDateInput.press("Tab"); await expect(washDateInput).toHaveValue("24/03/2026"); await page.getByTestId("daily-report-complaints-submit").click(); await expect(page.getByTestId("daily-report-complaints-validation-error")).toContainText("Kategori"); await page.getByTestId("daily-report-complaints-category-select").selectOption("service"); await page.getByTestId("daily-report-complaints-description-textarea").fill("Delayed service response"); await page.getByTestId("daily-report-complaints-submit").click(); await expect.poll(() => complaintRequests.length).toBe(1); expect(complaintRequests[0]).toEqual({ department_id: 1, wash_date: "2026-03-24", category: "service", description: "Delayed service response", }); await expect(page.getByTestId("daily-report-tile-complaints")).toContainText("3"); await expect(page.getByTestId("daily-report-complaints-modal")).toHaveCount(0); }); test("submits complaints with an optional customer selected from search", async ({ page }) => { const complaintRequests: Array> = []; const customerLookupQueries: string[] = []; await mockDailyReportDependencies(page, { overviewHandler: async () => json(buildOverviewPayload({ complaintsValue: 2 })), complaintHandler: async (body) => { complaintRequests.push(body); return json({ data: { id: 502, department_id: body.department_id, customer_number: body.customer_number, wash_date: body.wash_date, category: body.category, description: body.description, created_by: 1, created_at: "2026-04-08 10:10:00", }, }); }, complaintCustomerLookupHandler: async (url) => { customerLookupQueries.push(url.searchParams.get("search") || ""); return json({ data: [ { customer_number: 12345, customer_name: "Acme Transport" }, ], }); }, }); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); const washDateInput = page.getByTestId("daily-report-complaints-wash-date-input"); await page.getByTestId("daily-report-complaints-add-button").click(); await expect(washDateInput).toHaveValue(formatDisplayedDate("2026-03-23")); await expect(page.locator('[data-testid="daily-report-complaints-date-note"]')).toHaveCount(0); await page.getByTestId("daily-report-complaints-customer-search-input").fill("ac"); await expect(page.getByTestId("daily-report-complaints-customer-option-12345")).toBeVisible(); await page.getByTestId("daily-report-complaints-customer-option-12345").click(); await expect(page.getByTestId("daily-report-complaints-customer-selected")).toHaveValue("Acme Transport - 12345"); await page.getByTestId("daily-report-complaints-category-select").selectOption("wash_quality"); await page.getByTestId("daily-report-complaints-description-textarea").fill("Customer-linked complaint"); await page.getByTestId("daily-report-complaints-submit").click(); await expect.poll(() => complaintRequests.length).toBe(1); expect(customerLookupQueries).toContain("ac"); expect(complaintRequests[0]).toEqual({ department_id: 1, customer_number: 12345, wash_date: "2026-03-23", category: "wash_quality", description: "Customer-linked complaint", }); }); test("blocks complaint submission when search text is typed without selecting a customer", async ({ page }) => { const complaintRequests: Array> = []; await mockDailyReportDependencies(page, { overviewHandler: async () => json(buildOverviewPayload({ complaintsValue: 2 })), complaintHandler: async (body) => { complaintRequests.push(body); return json({ data: body }); }, complaintCustomerLookupHandler: async () => json({ data: [] }), }); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); await page.getByTestId("daily-report-complaints-add-button").click(); await page.getByTestId("daily-report-complaints-customer-search-input").fill("12345"); await expect(page.getByTestId("daily-report-complaints-customer-no-results")).toBeVisible(); await page.getByTestId("daily-report-complaints-category-select").selectOption("service"); await page.getByTestId("daily-report-complaints-description-textarea").fill("Customer lookup not selected"); await page.getByTestId("daily-report-complaints-submit").click(); await expect(page.getByTestId("daily-report-complaints-validation-error")).toContainText("Vaelg en kunde"); expect(complaintRequests).toHaveLength(0); }); test("keeps the complaints modal open and shows an API error when submission fails", async ({ page }) => { await mockDailyReportDependencies(page, { overviewHandler: async () => json(buildOverviewPayload({ complaintsValue: 2 })), complaintHandler: async () => json({ message: "Customer not found" }, 400), }); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); await page.getByTestId("daily-report-complaints-add-button").click(); await page.getByTestId("daily-report-complaints-category-select").selectOption("service"); await page.getByTestId("daily-report-complaints-description-textarea").fill("Broken mirror"); await page.getByTestId("daily-report-complaints-submit").click(); await expect(page.getByTestId("daily-report-complaints-modal")).toBeVisible(); await expect(page.getByTestId("daily-report-complaints-error")).toContainText("Customer not found"); }); test("hides the complaints plus action without create permission", async ({ page }) => { await mockDailyReportDependencies(page, { overviewHandler: async () => json(buildOverviewPayload({ complaintsValue: 2 })), permissions: adminPermissions.filter((permission) => permission !== "create_department_daily_report_complaints"), }); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); await expect(page.locator('[data-testid="daily-report-complaints-add-button"]')).toHaveCount(0); }); test("falls back to local optional product names when the overview payload omits titles", async ({ page }) => { await mockDailyReportDependencies(page, async () => json(buildOverviewPayload({ products: [ { product_id: 24, slug: "spot-free-lastbil", title: "", state: "ready", value: 3, out_of: 8 }, { product_id: 25, slug: "faelg-flex", title: null, state: "ready", value: 2, out_of: 8 }, { product_id: 27, slug: "extraordinary-10-min", state: "ready", value: 1, out_of: 8 }, { product_id: 26, slug: "hoejglans", title: " ", state: "ready", value: 4, out_of: 8 }, ], }))); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); await expect(page.getByTestId("daily-report-tile-spot-free-lastbil")).toContainText("Spot Free (Lastbil)"); await expect(page.getByTestId("daily-report-tile-faelg-flex")).toContainText("Fælg flex pr. enhed"); await expect(page.getByTestId("daily-report-tile-extraordinary-10-min")).toContainText("Ekstraordinær pr. 10 min inkl. kemi"); await expect(page.getByTestId("daily-report-tile-hoejglans")).toContainText("Højglans - Voksforsegling pr. enhed"); await expect(page.getByTestId("daily-report-tile-undervognsskyl")).toContainText("Undervognsskyl pr. enhed"); await expect(page.getByTestId("daily-report-tile-double-duty-kemi")).toContainText("Tillæg for Specialsæbe - DD"); }); test("updates departments and date query params with one overview request per UI action", async ({ page }, testInfo) => { const overviewRequests: URL[] = []; await mockDailyReportDependencies(page, async (url) => { overviewRequests.push(url); return json(buildOverviewPayload({ department_ids: (url.searchParams.get("department_ids") || "1").split(",").map((value) => Number(value)), date_to: url.searchParams.get("date_to") || "2026-03-23", })); }); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); await expect.poll(() => overviewRequests.length).toBe(1); await expandDepartmentFiltersOnMobile(page, testInfo); await page.getByTestId("daily-report-department-2").click(); await expect.poll(() => overviewRequests.length).toBe(2); expect(overviewRequests.at(-1)?.searchParams.get("department_ids")).toBe("1,2"); await expect(page).toHaveURL(/departmentIds=1%2C2|departmentIds=1,2/); const endDateInput = page.getByTestId("daily-report-date-controls").locator("input[type='date']:visible").nth(1); await endDateInput.fill("2026-03-24"); await endDateInput.blur(); await expect.poll(() => overviewRequests.length).toBe(3); expect(overviewRequests.at(-1)?.searchParams.get("date_to")).toBe("2026-03-24"); await expect(page).toHaveURL(/dateTo=2026-03-24/); }); test("ignores stale overview responses and keeps unavailable overtime fallback visible", async ({ page }) => { const overviewRequests: URL[] = []; await mockDailyReportDependencies(page, async (url) => { overviewRequests.push(url); const dateTo = url.searchParams.get("date_to") || "2026-03-23"; if (dateTo === "2026-03-23") { await new Promise((resolve) => setTimeout(resolve, 400)); return json(buildOverviewPayload({ metrics: { bookings: { state: "ready", value: 1, out_of: 2, message: null }, complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." }, night_washes: { state: "ready", value: 0, out_of: null, message: null }, revenue: { state: "ready", value: 100, out_of: null, message: null }, washes: { state: "ready", value: 1, out_of: null, message: null }, products_sold: { state: "ready", value: 2, out_of: null, message: null }, transactions: { state: "ready", value: 1, out_of: null, message: null }, water_usage: { state: "ready", value: 3, out_of: null, message: null }, overtime: { state: "ready", value: 0.25, out_of: null, message: null }, }, })); } return json(buildOverviewPayload({ date_to: "2026-03-24", metrics: { bookings: { state: "ready", value: 6, out_of: 9, message: null }, complaints: { state: "unavailable", value: null, out_of: null, message: "Kundeklager er ikke tilgængelig endnu." }, night_washes: { state: "ready", value: 3, out_of: null, message: null }, revenue: { state: "ready", value: 3200, out_of: null, message: null }, washes: { state: "ready", value: 10, out_of: null, message: null }, products_sold: { state: "ready", value: 20, out_of: null, message: null }, transactions: { state: "ready", value: 9, out_of: null, message: null }, water_usage: { state: "ready", value: 40, out_of: null, message: null }, overtime: { state: "unavailable", value: null, out_of: null, message: "Overarbejde kræver Workfeed-kobling." }, }, })); }); await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-23"); await expect(page.getByTestId("daily-report-page")).toBeVisible(); const endDateInput = page.getByTestId("daily-report-date-controls").locator("input[type='date']:visible").nth(1); await endDateInput.fill("2026-03-24"); await endDateInput.blur(); await expect.poll(() => overviewRequests.length).toBe(2); expect(overviewRequests.at(-1)?.searchParams.get("date_to")).toBe("2026-03-24"); await expect(page).toHaveURL(/dateTo=2026-03-24/); await expect(page.getByTestId("daily-report-tile-transactions")).toContainText("9", { timeout: 10000 }); await expect(page.getByTestId("daily-report-tile-night-washes")).toContainText("3"); await expect(page.getByTestId("daily-report-tile-overtime")).toContainText("Ikke tilgængelig"); await expect(page.getByTestId("daily-report-tile-overtime")).toContainText("Workfeed"); }); });