// @vitest-environment jsdom import { nextTick } from "vue"; import { mount } from "@vue/test-utils"; import { beforeEach, describe, expect, it, vi } from "vitest"; const getTransactionCountMock = vi.hoisted(() => vi.fn()); const getDepartmentMock = vi.hoisted(() => vi.fn()); const getLaneStatusTogglesMock = vi.hoisted(() => vi.fn()); const setMachineStatusEnabledMock = vi.hoisted(() => vi.fn()); const setLaneSelfServeEnabledMock = vi.hoisted(() => vi.fn()); const hasPermissionMock = vi.hoisted(() => vi.fn()); const laneHelpers = vi.hoisted(() => { const requiredFields = [ "relay_in_id", "relay_out_id", "relay_machine_id", "relay_machine_program_picker_id", "relay_machine_cleaner_id", "dynamic_image_id", "machine_type_id", ]; const hasConfiguredValue = (value) => value !== null && value !== undefined && String(value).trim() !== "" && String(value).trim() !== "0"; const getWarnings = (lane) => lane?.dognvask_configuration_warnings ?? requiredFields .filter((field) => !hasConfiguredValue(lane?.[field])) .map((field) => ({ field, message: `${field} mangler` })); return { getWarnings, isMachineStatusEnabled: (lane) => typeof lane?.machine_status_enabled === "boolean" ? lane.machine_status_enabled : ["AVAILABLE", "OCCUPIED", "RESERVED"].includes(String(lane?.status ?? "").toUpperCase()), isSelfServeConfigured: (lane) => typeof lane?.dognvask_configured === "boolean" ? lane.dognvask_configured : getWarnings(lane).length === 0, }; }); const sharedDateState = vi.hoisted(() => ({ selected_date: null, selected_date_to: null, })); vi.mock("@/components/session/token/SessionUser.vue", () => ({ SessionUser: { objects: { department_daily_reports: { functions: { getTransactionCount: getTransactionCountMock, }, }, departments: { get: { single: getDepartmentMock, }, }, department_lanes: { functions: { getStatusToggles: getLaneStatusTogglesMock, setMachineStatusEnabled: setMachineStatusEnabledMock, isMachineStatusEnabled: laneHelpers.isMachineStatusEnabled, isSelfServeConfigured: laneHelpers.isSelfServeConfigured, getSelfServeConfigurationWarnings: laneHelpers.getWarnings, }, set: { selfserve_enabled: setLaneSelfServeEnabledMock, }, }, }, hasPermission: hasPermissionMock, functions: { date: { isToday: () => false, }, currency: { toLocal: (value) => String(value), }, }, }, })); vi.mock("@/components/pagination/departmentTabs.vue", async () => { const { ref } = await import("vue"); return { departments: ref([ { id: 10, name: "Dept 10" }, { id: 20, name: "Dept 20" }, ]), }; }); vi.mock("@/views/dashboards/departmentDashboard/other/displays/DepartmentsOverviewObject.vue", () => { let fetchId = 0; return { next_fetch_id: () => { fetchId += 1; return fetchId; }, is_latest_fetch: () => true, is_loading: () => false, finished_loading: vi.fn(), }; }); vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue", async () => { const { ref } = await import("vue"); const selected_date = ref("2026-03-23"); const selected_date_to = ref("2026-03-23"); sharedDateState.selected_date = selected_date; sharedDateState.selected_date_to = selected_date_to; return { selected_date, selected_date_to, selectDate: (startIso, endIso) => { selected_date.value = startIso; selected_date_to.value = endIso; }, }; }); vi.mock("@/views/dashboards/departmentDashboard/other/displays/DepartmentDashboardOverviewNavigation.vue", () => ({ default: { name: "DepartmentDashboardOverviewNavigation", template: "
", }, })); import DepartmentDailyReportThisWeek from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportThisWeek.vue"; import DepartmentDailyReportSmall from "@/views/dashboards/departmentDashboard/other/displays/DepartmentDailyReportSmall.vue"; const flushAll = async () => { await nextTick(); await Promise.resolve(); await nextTick(); await Promise.resolve(); }; describe("Department overview period sync behavior", () => { beforeEach(() => { getTransactionCountMock.mockReset(); getDepartmentMock.mockReset(); getLaneStatusTogglesMock.mockReset(); setMachineStatusEnabledMock.mockReset(); setLaneSelfServeEnabledMock.mockReset(); hasPermissionMock.mockReset(); hasPermissionMock.mockImplementation((permission) => ["modules_selfserve_lane_status_set", "edit_department_lane"].includes(permission) ); getDepartmentMock.mockResolvedValue({ id: 20, name: "Dept 20" }); getLaneStatusTogglesMock.mockResolvedValue({ data: { data: [] } }); setMachineStatusEnabledMock.mockResolvedValue({ data: { data: { status: "AVAILABLE", machine_status_enabled: true } }, }); setLaneSelfServeEnabledMock.mockResolvedValue({ data: { data: { lane: { selfserve_enabled: true } } } }); getTransactionCountMock.mockResolvedValue({ data: { data: { earnings: 10, products: 2, quantity: 3, washes: 4, outside_hours: { total: 1, by_source: { orders: 0, xlvask: 1, selfserve: 0, }, has_missing_opening_hours: false, missing_department_ids: [], }, }, }, }); if (sharedDateState.selected_date) { sharedDateState.selected_date.value = "2026-03-23"; } if (sharedDateState.selected_date_to) { sharedDateState.selected_date_to.value = "2026-03-23"; } }); it("list summary and compact cards fetch against the same shared period", async () => { mount(DepartmentDailyReportThisWeek, { props: { departments: [10], }, global: { mocks: { $t: (value) => value, }, stubs: { DepartmentDashboardOverviewNavigation: true, BSkeleton: true, }, }, }); mount(DepartmentDailyReportSmall, { props: { department_id: 20, }, global: { mocks: { $t: (value) => value, }, stubs: { BLoading: true, }, }, }); await flushAll(); await flushAll(); const initialCalls = getTransactionCountMock.mock.calls.map( ([departmentId, date, dateTo]) => `${departmentId}:${date}:${dateTo}` ); expect(initialCalls).toEqual(expect.arrayContaining(["10:2026-03-23:2026-03-23", "20:2026-03-23:2026-03-23"])); }); it("changing shared range refetches both list and cards with the new period", async () => { mount(DepartmentDailyReportThisWeek, { props: { departments: [10], }, global: { mocks: { $t: (value) => value, }, stubs: { DepartmentDashboardOverviewNavigation: true, BSkeleton: true, }, }, }); mount(DepartmentDailyReportSmall, { props: { department_id: 20, }, global: { mocks: { $t: (value) => value, }, stubs: { BLoading: true, }, }, }); await flushAll(); await flushAll(); getTransactionCountMock.mockClear(); sharedDateState.selected_date_to.value = "2026-03-24"; await flushAll(); await flushAll(); const rangeCalls = getTransactionCountMock.mock.calls.map( ([departmentId, date, dateTo]) => `${departmentId}:${date}:${dateTo}` ); expect(rangeCalls).toEqual(expect.arrayContaining(["10:2026-03-23:2026-03-24", "20:2026-03-23:2026-03-24"])); }); it("shows missing-opening-hours labels in the list summary", async () => { getTransactionCountMock.mockResolvedValueOnce({ data: { data: { earnings: 10, products: 2, quantity: 3, washes: 4, outside_hours: { total: 1, by_source: { orders: 0, xlvask: 1, selfserve: 0, }, has_missing_opening_hours: true, missing_department_ids: [20], }, }, }, }); const wrapper = mount(DepartmentDailyReportThisWeek, { props: { departments: [10], }, global: { mocks: { $t: (key, values = {}) => { if (key === "admin.daily_report.reports.night_washes.warning_missing_opening_hours") { return `Opening hours missing for: ${values.departments}`; } return key; }, }, stubs: { DepartmentDashboardOverviewNavigation: true, BSkeleton: true, }, }, }); await flushAll(); await flushAll(); expect(wrapper.find('[data-testid="overview-summary-night-washes-warning"]').text()).toContain( "Opening hours missing for: Dept 20" ); }); it("does not show missing-opening-hours labels in compact department rows", async () => { getTransactionCountMock.mockResolvedValueOnce({ data: { data: { earnings: 10, products: 2, quantity: 3, washes: 4, outside_hours: { total: 1, by_source: { orders: 0, xlvask: 1, selfserve: 0, }, has_missing_opening_hours: true, missing_department_ids: [20], }, }, }, }); const wrapper = mount(DepartmentDailyReportSmall, { props: { department_id: 20, }, global: { mocks: { $t: (key, values = {}) => { if (key === "admin.daily_report.reports.night_washes.warning_missing_opening_hours") { return `Opening hours missing for: ${values.departments}`; } return key; }, }, stubs: { BLoading: true, }, }, }); await flushAll(); await flushAll(); expect(wrapper.text()).toContain("Dept 20"); expect(wrapper.text()).toContain("1"); expect(wrapper.text()).not.toContain("Opening hours missing for:"); }); it("renders lane machine and Døgnvask toggles and warns when Døgnvask setup is incomplete", async () => { const laneWithCompleteSetup = { id: 21, department: 20, name: "T1", status: "AVAILABLE", machine_status_enabled: true, selfserve_enabled: true, relay_in_id: "in-1", relay_out_id: "out-1", relay_machine_id: "machine-1", relay_machine_program_picker_id: "picker-1", relay_machine_cleaner_id: "cleaner-1", dynamic_image_id: 1, machine_type_id: 1, dognvask_configured: true, dognvask_configuration_warnings: [], machine_status_audit: { modified_at: "2026-05-26T11:45:00+02:00", modified_by_name: "Jeppe", }, }; const laneWithMissingSetup = { id: 22, department: 20, name: "T2", status: "MAINTENANCE", machine_status_enabled: false, selfserve_enabled: true, relay_in_id: "in-2", relay_out_id: "", relay_machine_id: "machine-2", relay_machine_program_picker_id: "", relay_machine_cleaner_id: "cleaner-2", dynamic_image_id: null, machine_type_id: null, dognvask_configured: false, dognvask_configuration_warnings: [ { field: "relay_out_id", message: "Udgangsrelæ mangler" }, { field: "dynamic_image_id", message: "Maskinstatusbillede mangler" }, ], }; getLaneStatusTogglesMock.mockResolvedValueOnce({ data: { data: [laneWithCompleteSetup, laneWithMissingSetup], }, }); setMachineStatusEnabledMock.mockResolvedValueOnce({ data: { data: { id: 22, status: "AVAILABLE", machine_status_enabled: true, }, }, }); setLaneSelfServeEnabledMock .mockResolvedValueOnce({ data: { data: { lane: { ...laneWithCompleteSetup, selfserve_enabled: false, }, }, }, }) .mockResolvedValueOnce({ data: { data: { lane: { ...laneWithMissingSetup, selfserve_enabled: false, }, }, }, }); const wrapper = mount(DepartmentDailyReportSmall, { props: { department_id: 20, }, global: { mocks: { $t: (value) => value, }, stubs: { BLoading: true, }, }, }); await flushAll(); await flushAll(); expect(getLaneStatusTogglesMock).toHaveBeenCalledWith(20); expect(wrapper.text()).toContain("Maskinstatus"); expect(wrapper.text()).toContain("Sidst ændret"); expect(wrapper.text()).toContain("Ændret af"); expect(wrapper.text()).toContain("Jeppe"); expect(wrapper.text()).toContain("2026"); expect(wrapper.text()).toContain("Døgnvask"); expect(wrapper.find('[data-testid="overview-department-20-lane-21-dognvask-info"]').exists()).toBe(true); expect(wrapper.find('[data-testid="overview-department-20-lane-22-dognvask-warning"]').exists()).toBe(true); expect(wrapper.find("#dognvask-20-21").element.checked).toBe(true); expect(wrapper.find("#dognvask-20-22").element.checked).toBe(true); await wrapper.find("#machine-status-20-22").setValue(true); await flushAll(); expect(setMachineStatusEnabledMock).toHaveBeenCalledWith(22, true); await wrapper.find("#dognvask-20-21").setValue(false); await flushAll(); expect(setLaneSelfServeEnabledMock).toHaveBeenCalledWith(21, false); expect(wrapper.find("#dognvask-20-21").element.checked).toBe(false); await wrapper.find("#dognvask-20-22").setValue(false); await flushAll(); expect(setLaneSelfServeEnabledMock).toHaveBeenCalledWith(22, false); expect(setLaneSelfServeEnabledMock).toHaveBeenCalledTimes(2); expect(wrapper.find("#dognvask-20-22").element.checked).toBe(false); await wrapper.find("#dognvask-20-22").setValue(true); await flushAll(); expect(setLaneSelfServeEnabledMock).toHaveBeenCalledTimes(2); expect(wrapper.find("#dognvask-20-22").element.checked).toBe(false); }); it("disables lane mutation toggles for users without lane mutation permissions", async () => { hasPermissionMock.mockImplementation((permission) => permission === "list_department_daily_reports"); getLaneStatusTogglesMock.mockResolvedValueOnce({ data: { data: [ { id: 21, department: 20, name: "T1", status: "AVAILABLE", machine_status_enabled: true, selfserve_enabled: true, relay_in_id: "in-1", relay_out_id: "out-1", relay_machine_id: "machine-1", relay_machine_program_picker_id: "picker-1", relay_machine_cleaner_id: "cleaner-1", dynamic_image_id: 1, machine_type_id: 1, dognvask_configured: true, dognvask_configuration_warnings: [], }, ], }, }); const wrapper = mount(DepartmentDailyReportSmall, { props: { department_id: 20, }, global: { mocks: { $t: (value) => value, }, stubs: { BLoading: true, }, }, }); await flushAll(); await flushAll(); const machineToggle = wrapper.find("#machine-status-20-21"); const dognvaskToggle = wrapper.find("#dognvask-20-21"); expect(machineToggle.element.disabled).toBe(true); expect(dognvaskToggle.element.disabled).toBe(true); await machineToggle.trigger("change"); await dognvaskToggle.trigger("change"); await flushAll(); expect(setMachineStatusEnabledMock).not.toHaveBeenCalled(); expect(setLaneSelfServeEnabledMock).not.toHaveBeenCalled(); }); it("does not allow wash-lane list permission to mutate lane toggles", async () => { hasPermissionMock.mockImplementation((permission) => permission === "list_department_wash_lanes"); getLaneStatusTogglesMock.mockResolvedValueOnce({ data: { data: [ { id: 21, department: 20, name: "T1", status: "AVAILABLE", machine_status_enabled: true, selfserve_enabled: true, relay_in_id: "in-1", relay_out_id: "out-1", relay_machine_id: "machine-1", relay_machine_program_picker_id: "picker-1", relay_machine_cleaner_id: "cleaner-1", dynamic_image_id: 1, machine_type_id: 1, dognvask_configured: true, dognvask_configuration_warnings: [], }, ], }, }); const wrapper = mount(DepartmentDailyReportSmall, { props: { department_id: 20, }, global: { mocks: { $t: (value) => value, }, stubs: { BLoading: true, }, }, }); await flushAll(); await flushAll(); const machineToggle = wrapper.find("#machine-status-20-21"); const dognvaskToggle = wrapper.find("#dognvask-20-21"); expect(machineToggle.element.disabled).toBe(true); expect(dognvaskToggle.element.disabled).toBe(true); await machineToggle.trigger("change"); await dognvaskToggle.trigger("change"); await flushAll(); expect(setMachineStatusEnabledMock).not.toHaveBeenCalled(); expect(setLaneSelfServeEnabledMock).not.toHaveBeenCalled(); }); });