613 lines
18 KiB
JavaScript
613 lines
18 KiB
JavaScript
// @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: "<div class='overview-navigation-stub' />",
|
|
},
|
|
}));
|
|
|
|
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();
|
|
});
|
|
});
|