From da0c699b8804384473146a032da07c48fd3ace4f Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Wed, 3 Jun 2026 22:07:47 +0200 Subject: [PATCH] Add self-serve session handling and lane selection normalization --- src/composables/useWashFlowState.js | 26 +++++++++++--- .../userDashboard/wash/MyWashStart.vue | 34 +++++++++++++++++-- tests/unit/my-wash-start-production.spec.js | 8 +++-- tests/unit/my-wash-start.spec.js | 25 ++++++++++++++ .../use-wash-flow-state-production.spec.js | 11 ++++++ tests/unit/use-wash-flow-state.spec.js | 17 ++++++++++ 6 files changed, 111 insertions(+), 10 deletions(-) diff --git a/src/composables/useWashFlowState.js b/src/composables/useWashFlowState.js index b6a495bb..4aca5101 100644 --- a/src/composables/useWashFlowState.js +++ b/src/composables/useWashFlowState.js @@ -7,6 +7,11 @@ export const WASH_STEPS = { COMPLETED: 5, }; +const normalizeLaneId = (value) => { + const normalized = parseInt(String(value ?? ""), 10); + return Number.isNaN(normalized) || normalized <= 0 ? null : normalized; +}; + export function useWashFlowState(options) { const { currentStep, @@ -35,6 +40,17 @@ export function useWashFlowState(options) { activeTasks.value.length > 0 ? steps.TASKS : steps.WASH_IN_PROGRESS ); + const getSelectedLaneId = () => normalizeLaneId(radioLaneOption.value); + + const getSelectedLane = () => { + const selectedLaneId = getSelectedLaneId(); + if (selectedLaneId === null) { + return null; + } + + return nearestDepartment.value?.lanes.find((lane) => normalizeLaneId(lane.id) === selectedLaneId) || null; + }; + const clickableSteps = { [steps.VEHICLE]: () => { if (washInProgress.value) { @@ -106,7 +122,7 @@ export function useWashFlowState(options) { } if (currentStep.value === steps.SELECT_LANE) { - const selectedLane = nearestDepartment.value?.lanes.find((lane) => lane.id === radioLaneOption.value); + const selectedLane = getSelectedLane(); if (!selectedLane || !isLaneAvailable(selectedLane)) { return true; } @@ -136,9 +152,10 @@ export function useWashFlowState(options) { const handleConfirmNext = async () => { if (currentStep.value === steps.QUESTIONS) { editAnswers.value = false; - if (radioLaneOption.value && radioLaneOption.value !== "Any") { + const selectedLaneId = getSelectedLaneId(); + if (selectedLaneId !== null) { try { - await updateLaneAllowedServices(radioLaneOption.value); + await updateLaneAllowedServices(selectedLaneId); } catch { return; } @@ -153,8 +170,9 @@ export function useWashFlowState(options) { return; } + const selectedLaneId = getSelectedLaneId() ?? radioLaneOption.value; await onStartWash( - radioLaneOption.value, + selectedLaneId, licensePlateInput.value, customerNumberInput.value, targetStepForStart() diff --git a/src/views/dashboards/userDashboard/wash/MyWashStart.vue b/src/views/dashboards/userDashboard/wash/MyWashStart.vue index 47aabf3d..5c5af089 100644 --- a/src/views/dashboards/userDashboard/wash/MyWashStart.vue +++ b/src/views/dashboards/userDashboard/wash/MyWashStart.vue @@ -121,6 +121,7 @@ const { answers, completedTasks, allowedServices, + session: selfServeSession, error: selfServeDataError, visibleQuestions, activeTasks, @@ -203,8 +204,33 @@ const isLaneSelfServeEnabled = (lane: any) => const isCurrentActiveWashLane = (lane: any) => washInProgress.value && normalizeLaneId(lane?.id) === normalizeLaneId(washLaneId.value); -const isCurrentAllowedSelfServeLane = (lane: any) => - selfServeAllowed.value === true && normalizeLaneId(lane?.id) === normalizeLaneId(selfServeLane.value?.id); +const isAllowedSelfServeValue = (value: any) => + value === true || + value === 1 || + value === "1" || + String(value ?? "") + .trim() + .toLowerCase() === "true"; + +const isSelfServeSessionAllowed = () => + isAllowedSelfServeValue(selfServeAllowed.value) || isAllowedSelfServeValue(selfServeSession.value?.allowed); + +const getAllowedSelfServeLaneIds = () => + new Set( + [ + selfServeLane.value?.id, + selfServeSession.value?.lane_id, + selfServeSession.value?.lane?.id, + selfServeSession.value?.metadata?.lane_id, + ] + .map(normalizeLaneId) + .filter((laneId): laneId is number => laneId !== null) + ); + +const isCurrentAllowedSelfServeLane = (lane: any) => { + const laneId = normalizeLaneId(lane?.id); + return laneId !== null && isSelfServeSessionAllowed() && getAllowedSelfServeLaneIds().has(laneId); +}; const isLaneAvailable = (lane: any) => (isCurrentActiveWashLane(lane) || isCurrentAllowedSelfServeLane(lane) || lane.status === "AVAILABLE") && @@ -219,7 +245,9 @@ const isMachineAvailable = (laneId: number | string | null) => { ); } - const selectedLane = nearestDepartment.value?.lanes.find((entry: { id: number }) => entry.id === laneId); + const selectedLane = nearestDepartment.value?.lanes.find( + (entry: { id: number }) => normalizeLaneId(entry.id) === normalizeLaneId(laneId) + ); if (!selectedLane) { return false; } diff --git a/tests/unit/my-wash-start-production.spec.js b/tests/unit/my-wash-start-production.spec.js index c2f23a32..dfedc06f 100644 --- a/tests/unit/my-wash-start-production.spec.js +++ b/tests/unit/my-wash-start-production.spec.js @@ -52,11 +52,13 @@ describe("MyWashStart.vue production recovery contracts", () => { }); it("does not treat the customer's active occupied lane as unavailable", () => { + expect(source).toContain("session: selfServeSession,"); expect(source).toContain("const isCurrentActiveWashLane = (lane: any) =>"); + expect(source).toContain("const isSelfServeSessionAllowed = () =>"); expect(source).toContain("const isCurrentAllowedSelfServeLane = (lane: any) =>"); - expect(source).toContain( - "selfServeAllowed.value === true && normalizeLaneId(lane?.id) === normalizeLaneId(selfServeLane.value?.id)" - ); + expect(source).toContain("isAllowedSelfServeValue(selfServeSession.value?.allowed)"); + expect(source).toContain("selfServeSession.value?.lane_id"); + expect(source).toContain("getAllowedSelfServeLaneIds().has(laneId)"); expect(source).toContain( '(isCurrentActiveWashLane(lane) || isCurrentAllowedSelfServeLane(lane) || lane.status === "AVAILABLE")' ); diff --git a/tests/unit/my-wash-start.spec.js b/tests/unit/my-wash-start.spec.js index 04d2adc3..6341e056 100644 --- a/tests/unit/my-wash-start.spec.js +++ b/tests/unit/my-wash-start.spec.js @@ -26,6 +26,7 @@ const mocks = vi.hoisted(() => { const allowedServices = { value: ["MACHINE"] }; const selfServeAllowed = { value: false }; const selfServeLane = { value: { id: 7 } }; + const selfServeSession = { value: null }; return { nearestDepartment, @@ -40,6 +41,7 @@ const mocks = vi.hoisted(() => { allowedServices, selfServeAllowed, selfServeLane, + selfServeSession, fetchDepartments: vi.fn(async () => guestDepartments.value), startAutoRefresh: vi.fn(), stopAutoRefresh: vi.fn(), @@ -176,6 +178,7 @@ vi.mock("@/composables/useSelfServeLogic", () => ({ completedTasks: mocks.completedTasks, allowedServices: mocks.allowedServices, allowed: mocks.selfServeAllowed, + session: mocks.selfServeSession, visibleQuestions: mocks.visibleQuestions, activeTasks: mocks.activeTasks, allVisibleQuestionsAnswered: { value: false }, @@ -376,6 +379,7 @@ describe("MyWashStart", () => { mocks.allowedServices.value = ["MACHINE"]; mocks.selfServeAllowed.value = false; mocks.selfServeLane.value = { id: 7 }; + mocks.selfServeSession.value = null; mocks.conditions.value = []; mocks.rules.value = []; mocks.sessionCustomerNumber.value = 12345679; @@ -517,6 +521,27 @@ describe("MyWashStart", () => { expect(wrapper.get('[data-testid="lane-7-available"]').text()).toBe("true"); }); + it("uses the allowed session lane when the lane payload is not populated yet", async () => { + mocks.nearestDepartment.value = { + ...mocks.nearestDepartment.value, + lanes: [{ id: 7, name: "7", status: "OCCUPIED", products: [2], machine_available: true }], + }; + mocks.guestDepartments.value = [mocks.nearestDepartment.value]; + mocks.selfServeLane.value = null; + mocks.selfServeAllowed.value = false; + mocks.selfServeSession.value = { lane_id: 7, allowed: true }; + + const wrapper = mountWithApp(MyWashStart, { + global: { + stubs: stubComponents, + }, + }); + + await flushPromises(); + + expect(wrapper.get('[data-testid="lane-7-available"]').text()).toBe("true"); + }); + it("hides machine tasks while manual wash is selected", async () => { mocks.activeTasks.value = [ { id: 31, task: "Machine checklist", services: ["MACHINE"] }, diff --git a/tests/unit/use-wash-flow-state-production.spec.js b/tests/unit/use-wash-flow-state-production.spec.js index 62b36666..3e447f3b 100644 --- a/tests/unit/use-wash-flow-state-production.spec.js +++ b/tests/unit/use-wash-flow-state-production.spec.js @@ -79,6 +79,17 @@ describe("useWashFlowState production transitions", () => { expect(flow.isNextButtonDisabled()).toBe(true); }); + it("keeps lane confirmation enabled when the selected lane id is a string", async () => { + const { state, flow } = createFlow({ radioLaneOption: ref("7") }); + state.currentStep.value = WASH_STEPS.SELECT_LANE; + + expect(flow.isNextButtonDisabled()).toBe(false); + + await flow.handleConfirmNext(); + + expect(state.onStartWash).toHaveBeenCalledWith(7, "AB12345", "12345679", WASH_STEPS.WASH_IN_PROGRESS); + }); + it("stays on questions when lane allowed-service refresh fails", async () => { const { state, flow } = createFlow({ updateLaneAllowedServices: vi.fn(async () => { diff --git a/tests/unit/use-wash-flow-state.spec.js b/tests/unit/use-wash-flow-state.spec.js index bdce9a52..9da57449 100644 --- a/tests/unit/use-wash-flow-state.spec.js +++ b/tests/unit/use-wash-flow-state.spec.js @@ -111,4 +111,21 @@ describe("useWashFlowState", () => { expect(state.onStartWash).toHaveBeenCalledWith(7, "AB12345", "12345", WASH_STEPS.TASKS); }); + + it("matches string lane selections against numeric lane ids", async () => { + const { flow, state } = createState({ + currentStep: ref(WASH_STEPS.QUESTIONS), + radioLaneOption: ref("7"), + }); + + await flow.handleConfirmNext(); + + expect(state.updateLaneAllowedServices).toHaveBeenCalledWith(7); + expect(state.currentStep.value).toBe(WASH_STEPS.SELECT_LANE); + expect(flow.isNextButtonDisabled()).toBe(false); + + await flow.handleConfirmNext(); + + expect(state.onStartWash).toHaveBeenCalledWith(7, "AB12345", "12345", WASH_STEPS.WASH_IN_PROGRESS); + }); });