793 lines
32 KiB
TypeScript
793 lines
32 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { mockApi, seedAuthenticatedState } from "./support/network.js";
|
|
import { isCompactProject } from "./support/projects";
|
|
|
|
const adminPermissions = [
|
|
"admin",
|
|
"list_department_daily_reports",
|
|
"list_bookings",
|
|
"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 complaintWashDateInput = (page: Page) =>
|
|
page.getByTestId("daily-report-complaints-wash-date-input").locator("input");
|
|
|
|
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,
|
|
target_percentage: null,
|
|
target_department_id: null,
|
|
},
|
|
{
|
|
product_id: 25,
|
|
slug: "faelg-flex",
|
|
title: "Fælg flex pr. enhed",
|
|
state: "ready",
|
|
value: 2,
|
|
out_of: 8,
|
|
target_percentage: null,
|
|
target_department_id: null,
|
|
},
|
|
{
|
|
product_id: 27,
|
|
slug: "extraordinary-10-min",
|
|
title: "Ekstraordinær pr. 10 min inkl. kemi",
|
|
state: "ready",
|
|
value: 1,
|
|
out_of: 8,
|
|
target_percentage: null,
|
|
target_department_id: null,
|
|
},
|
|
{
|
|
product_id: 26,
|
|
slug: "hoejglans",
|
|
title: "Højglans - Voksforsegling pr. enhed",
|
|
state: "ready",
|
|
value: 4,
|
|
out_of: 8,
|
|
target_percentage: null,
|
|
target_department_id: null,
|
|
},
|
|
{
|
|
product_id: 21,
|
|
slug: "undervognsskyl",
|
|
title: "Undervognsskyl pr. enhed",
|
|
state: "ready",
|
|
value: 2,
|
|
out_of: 8,
|
|
target_percentage: null,
|
|
target_department_id: null,
|
|
},
|
|
{
|
|
product_id: 22,
|
|
slug: "double-duty-kemi",
|
|
title: "Tillæg for Specialsæbe - DD",
|
|
state: "ready",
|
|
value: 5,
|
|
out_of: 8,
|
|
target_percentage: null,
|
|
target_department_id: null,
|
|
},
|
|
],
|
|
...overrides
|
|
}: Record<string, unknown> = {}) => ({
|
|
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<typeof json>> | ReturnType<typeof json>;
|
|
weatherHandler?: (url: URL) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
|
complaintHandler?: (
|
|
body: Record<string, unknown>
|
|
) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
|
complaintCustomerLookupHandler?: (url: URL) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
|
productTargetHandler?: (
|
|
body: Record<string, unknown>
|
|
) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>;
|
|
permissions?: string[];
|
|
}
|
|
| ((url: URL) => Promise<ReturnType<typeof json>> | ReturnType<typeof json>)
|
|
) {
|
|
const options = typeof input === "function" ? { overviewHandler: input } : input;
|
|
const weatherHandler = options.weatherHandler || (async () => json({ data: [] }));
|
|
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: [] }));
|
|
const productTargetHandler =
|
|
options.productTargetHandler ||
|
|
(async (body) =>
|
|
json({
|
|
data: {
|
|
department_id: body.department_id,
|
|
product_id: body.product_id,
|
|
target_percentage: body.target_percentage ?? null,
|
|
},
|
|
}));
|
|
|
|
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) => {
|
|
const url = new URL(route.request().url());
|
|
const response = await weatherHandler(url);
|
|
await route.fulfill(response);
|
|
});
|
|
|
|
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<string, unknown>;
|
|
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\/product-targets$/i, async (route) => {
|
|
const body = route.request().postDataJSON() as Record<string, unknown>;
|
|
const response = await productTargetHandler(body);
|
|
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 (!isCompactProject(testInfo)) {
|
|
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(".card[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("lets permitted users set a target from an optional product percentage", async ({ page }) => {
|
|
let targetPercentage: number | null = null;
|
|
const targetRequests: Array<Record<string, unknown>> = [];
|
|
|
|
await mockDailyReportDependencies(page, {
|
|
permissions: [...adminPermissions, "set_department_daily_report_product_targets"],
|
|
overviewHandler: async () =>
|
|
json(
|
|
buildOverviewPayload({
|
|
products: [
|
|
{
|
|
product_id: 24,
|
|
slug: "spot-free-lastbil",
|
|
title: "Spot Free (Lastbil)",
|
|
state: "ready",
|
|
value: 3,
|
|
out_of: 8,
|
|
target_percentage: targetPercentage,
|
|
target_department_id: targetPercentage === null ? null : 1,
|
|
},
|
|
],
|
|
})
|
|
),
|
|
productTargetHandler: async (body) => {
|
|
targetRequests.push(body);
|
|
targetPercentage = body.target_percentage === null ? null : Number(body.target_percentage);
|
|
|
|
return json({
|
|
data: {
|
|
department_id: body.department_id,
|
|
product_id: body.product_id,
|
|
target_percentage: targetPercentage,
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
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 productTile = page.getByTestId("daily-report-tile-spot-free-lastbil");
|
|
const percentageButton = page.getByTestId("daily-report-tile-spot-free-lastbil-percentage-button");
|
|
await expect(percentageButton).toBeVisible();
|
|
await expect(percentageButton).toHaveClass(/is-outlined/);
|
|
await expect(page.locator('[data-testid="daily-report-tile-spot-free-lastbil-target-button"]')).toHaveCount(0);
|
|
|
|
await percentageButton.click();
|
|
await page.getByTestId("daily-report-tile-spot-free-lastbil-target-input").fill("55.5");
|
|
await page.getByTestId("daily-report-tile-spot-free-lastbil-target-save").click();
|
|
|
|
await expect.poll(() => targetRequests.length).toBe(1);
|
|
expect(targetRequests[0]).toMatchObject({
|
|
department_id: 1,
|
|
product_id: 24,
|
|
target_percentage: 55.5,
|
|
});
|
|
await expect(page.getByTestId("daily-report-tile-spot-free-lastbil-target-button")).toContainText("55.5%");
|
|
await expect(productTile.locator(".daily-report-count__target-background-icon")).toHaveCount(1);
|
|
});
|
|
|
|
test("does not open the product target editor without permission", async ({ page }) => {
|
|
await mockDailyReportDependencies(page, {
|
|
overviewHandler: async () => json(buildOverviewPayload()),
|
|
permissions: adminPermissions,
|
|
});
|
|
|
|
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-tile-spot-free-lastbil-percentage-button").click();
|
|
|
|
await expect(page.locator('[data-testid="daily-report-tile-spot-free-lastbil-target-input"]')).toHaveCount(0);
|
|
await expect(page.locator('[data-testid="daily-report-tile-spot-free-lastbil-target-button"]')).toHaveCount(0);
|
|
});
|
|
|
|
test("renders rounded-up weather hours and keeps unknown productivity gray", async ({ page }) => {
|
|
await mockDailyReportDependencies(page, {
|
|
overviewHandler: async () => json(buildOverviewPayload()),
|
|
weatherHandler: async () =>
|
|
json({
|
|
data: [
|
|
{
|
|
date: "2026-03-23",
|
|
time: "06:00",
|
|
current: false,
|
|
weather: "rain",
|
|
washes: 0,
|
|
hours: 0.5,
|
|
status: "unhealthy",
|
|
},
|
|
{
|
|
date: "2026-03-23",
|
|
time: "19:00",
|
|
current: false,
|
|
weather: "clear",
|
|
washes: 20,
|
|
hours: 19.0,
|
|
status: "degraded",
|
|
},
|
|
{
|
|
date: "2026-03-23",
|
|
time: "20:00",
|
|
current: false,
|
|
weather: "clear",
|
|
washes: 10,
|
|
hours: 20.19,
|
|
status: "unhealthy",
|
|
},
|
|
{
|
|
date: "2026-03-24",
|
|
time: "08:00",
|
|
current: false,
|
|
weather: "mostly_clear",
|
|
washes: 2,
|
|
hours: 0,
|
|
status: "unknown",
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
|
|
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("department-weather-hours-2026-03-23-06:00")).toHaveText("1");
|
|
await expect(page.getByTestId("department-weather-hours-2026-03-23-19:00")).toHaveText("19");
|
|
await expect(page.getByTestId("department-weather-hours-2026-03-23-20:00")).toHaveText("21");
|
|
await expect(page.getByTestId("department-weather-productivity-2026-03-23-06:00")).toHaveClass(/red/);
|
|
await expect(page.getByTestId("department-weather-productivity-2026-03-24-08:00")).toHaveClass(/gray/);
|
|
|
|
await page.getByTestId("department-weather-summary-toggle").click();
|
|
|
|
await expect(page.getByTestId("department-weather-hours-2026-03-23-00:00")).toHaveText("40");
|
|
await expect(page.getByTestId("department-weather-hours-2026-03-24-00:00")).toHaveText("0");
|
|
await expect(page.getByTestId("department-weather-productivity-2026-03-23-00:00")).toHaveClass(/red/);
|
|
await expect(page.getByTestId("department-weather-productivity-2026-03-24-00:00")).toHaveClass(/gray/);
|
|
});
|
|
|
|
test("hides future empty weather hours and keeps day summaries based on occurred hours only", async ({ page }) => {
|
|
await mockDailyReportDependencies(page, {
|
|
overviewHandler: async () => json(buildOverviewPayload()),
|
|
weatherHandler: async () =>
|
|
json({
|
|
data: [
|
|
{
|
|
date: "2026-03-23",
|
|
time: "12:00",
|
|
current: false,
|
|
weather: "clear",
|
|
washes: 3,
|
|
hours: 1,
|
|
status: "healthy",
|
|
},
|
|
{
|
|
date: "2026-03-24",
|
|
time: "13:00",
|
|
current: true,
|
|
weather: "rain",
|
|
washes: 1,
|
|
hours: 0.25,
|
|
status: "healthy",
|
|
},
|
|
{
|
|
date: "2026-03-24",
|
|
time: "14:00",
|
|
current: false,
|
|
weather: "rain",
|
|
washes: 0,
|
|
hours: 0,
|
|
status: "unknown",
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
|
|
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("department-weather-hours-2026-03-23-12:00")).toHaveText("1");
|
|
await expect(page.getByTestId("department-weather-hours-2026-03-24-13:00")).toHaveText("1");
|
|
await expect(page.getByTestId("department-weather-hours-2026-03-24-14:00")).toHaveCount(0);
|
|
|
|
await page.getByTestId("department-weather-summary-toggle").click();
|
|
|
|
await expect(page.getByTestId("department-weather-hours-2026-03-23-00:00")).toHaveText("1");
|
|
await expect(page.getByTestId("department-weather-hours-2026-03-24-00:00")).toHaveText("1");
|
|
});
|
|
|
|
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<Record<string, unknown>> = [];
|
|
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 = complaintWashDateInput(page);
|
|
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(formatDisplayedDate("2026-03-24"));
|
|
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<Record<string, unknown>> = [];
|
|
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 = complaintWashDateInput(page);
|
|
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<Record<string, unknown>> = [];
|
|
|
|
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("Vælg 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")
|
|
.getByTestId("date-period-end")
|
|
.locator("input")
|
|
.first();
|
|
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")
|
|
.getByTestId("date-period-end")
|
|
.locator("input")
|
|
.first();
|
|
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");
|
|
});
|
|
});
|