diff --git a/src/views/dashboards/userDashboard/wash/MyWashStart.vue b/src/views/dashboards/userDashboard/wash/MyWashStart.vue index 45fea378..2092804f 100644 --- a/src/views/dashboards/userDashboard/wash/MyWashStart.vue +++ b/src/views/dashboards/userDashboard/wash/MyWashStart.vue @@ -54,9 +54,12 @@ const isVehicleStepNextLoading = ref(false); const hasStartFormUserInput = ref(false); const shouldRestoreServerActiveWash = ref(false); const isRestoringServerActiveWash = ref(false); +const isSyncingActiveWash = ref(false); const MIN_FINISHING_SCREEN_MS = 250; const RECENT_COMPLETED_WASH_KEY = "mywash_recent_completed_v1"; const RECENT_COMPLETED_WASH_SUPPRESSION_MS = 10 * 60 * 1000; +const ACTIVE_WASH_REFRESH_MS = 5 * 1000; +const WASH_START_SERVER_SYNC_GRACE_MS = 10 * 1000; const normalizePositiveInteger = (value: any) => { const parsed = parseInt(String(value ?? ""), 10); @@ -374,6 +377,7 @@ const completeGuidedWash = async () => { }; const unregisterBeforeUnload = ref void)>(null); +const activeWashRefreshInterval = ref | null>(null); const registrationOptions = computed(() => customerVehicles.value @@ -505,12 +509,7 @@ const questionReviewStateKey = computed(() => { return ""; } - return [ - normalizeLicensePlate(licensePlateInput.value), - normalizeLaneId(radioLaneOption.value) ?? "", - vehicleTypeSelect.value ?? "", - visibleQuestionIds, - ].join("|"); + return [normalizeLicensePlate(licensePlateInput.value), vehicleTypeSelect.value ?? "", visibleQuestionIds].join("|"); }); const hasConfirmedCurrentQuestionReview = computed( @@ -893,6 +892,111 @@ const scheduleServerActiveWashRestore = (delayMs = 0) => { }, delayMs); }; +const clearLocalActiveWashFromServer = () => { + const completedLaneId = washLaneId.value; + markRecentlyCompletedWash(completedLaneId, licensePlateInput.value, getNumericCustomerNumber()); + completedDurationMs.value = washStartTime.value ? now.value - washStartTime.value : 0; + currentStep.value = steps.COMPLETED; + washInProgress.value = false; + washLaneId.value = null; + washStartTime.value = null; + shouldRestoreServerActiveWash.value = false; + stopElapsedTimer(); + clearProgress(); + resetGuidedWashStep(); +}; + +const syncActiveWashWithServer = async () => { + if (isSyncingActiveWash.value || !washInProgress.value || !washLaneId.value) { + return; + } + + const laneId = normalizeLaneId(washLaneId.value); + if (!laneId) { + clearLocalActiveWashFromServer(); + return; + } + + isSyncingActiveWash.value = true; + try { + const response = await SessionUser.request("/modules/self-serve/lane/wash/in-progress", "GET", { + lane_id: laneId, + }); + const details = unwrapApiData(response); + if (!Object.prototype.hasOwnProperty.call(details || {}, "in_progress")) { + return; + } + + const customerNumber = getNumericCustomerNumber(); + const serverCustomerNumber = normalizePositiveInteger( + details?.session?.customer_number ?? details?.customer?.customer_number + ); + const serverStillMatchesCurrentWash = + !!details?.in_progress && (!customerNumber || !serverCustomerNumber || serverCustomerNumber === customerNumber); + + if (!serverStillMatchesCurrentWash || isRecentlyCompletedActiveWash({ details, laneId })) { + const isWithinStartGracePeriod = + washStartTime.value && Date.now() - washStartTime.value < WASH_START_SERVER_SYNC_GRACE_MS; + if (isWithinStartGracePeriod && !isRecentlyCompletedActiveWash({ details, laneId })) { + return; + } + + clearLocalActiveWashFromServer(); + return; + } + + const session = details?.session || {}; + const vehicle = details?.vehicle || {}; + const serverReg = normalizeLicensePlate(session?.reg ?? vehicle?.reg); + if (serverReg) { + licensePlateInput.value = serverReg; + } + + applyEffectiveCustomerNumberInput(session?.customer_number ?? details?.customer?.customer_number ?? customerNumber); + vehicleTypeSelect.value = + normalizePositiveInteger(session?.vehicle_type_id ?? vehicle?.type) ?? vehicleTypeSelect.value; + radioWashType.value = session?.machine_relay_enabled ? "Machine" : radioWashType.value; + washStartTime.value = + parseServerDateTimeMs(session?.wash_started_at) ?? + parseServerDateTimeMs(session?.machine_start_triggered_at) ?? + parseServerDateTimeMs(session?.machine_relay_enabled_at) ?? + washStartTime.value; + + const summaryParams: Record = session?.id + ? { session_id: session.id } + : { lane_id: laneId, reg: normalizeLicensePlate(licensePlateInput.value) }; + const selectedVehicleTypeId = normalizePositiveInteger(vehicleTypeSelect.value); + if (selectedVehicleTypeId) { + summaryParams.vehicle_type = selectedVehicleTypeId; + } + await fetchWashSummary(summaryParams, false); + saveProgress("serverActiveWashRefresh"); + } catch (error) { + console.warn("Failed to refresh active self-serve wash:", error); + } finally { + isSyncingActiveWash.value = false; + } +}; + +const stopActiveWashRefresh = () => { + if (activeWashRefreshInterval.value) { + window.clearInterval(activeWashRefreshInterval.value); + activeWashRefreshInterval.value = null; + } +}; + +const startActiveWashRefresh = () => { + stopActiveWashRefresh(); + if (!washInProgress.value || !washLaneId.value) { + return; + } + + syncActiveWashWithServer(); + activeWashRefreshInterval.value = window.setInterval(() => { + syncActiveWashWithServer(); + }, ACTIVE_WASH_REFRESH_MS); +}; + const retrySelfServeData = async () => { if (isSelfServeRetrying.value) { return; @@ -979,6 +1083,29 @@ const onVehicleStepNext = async () => { } }; +const onPreviousStep = (fallbackPrevious: { action?: () => void } | null = null) => { + if (washInProgress.value || currentStep.value === steps.COMPLETED) { + return; + } + + if (currentStep.value === steps.QUESTIONS) { + currentStep.value = steps.VEHICLE; + return; + } + + if (currentStep.value === steps.SELECT_LANE) { + currentStep.value = visibleQuestions.value.length > 0 ? steps.QUESTIONS : steps.VEHICLE; + return; + } + + if (currentStep.value === steps.TASKS) { + currentStep.value = steps.SELECT_LANE; + return; + } + + fallbackPrevious?.action?.(); +}; + const onToggleTask = (taskId: number, value: boolean) => { completedTasks.value = { ...completedTasks.value, @@ -1071,6 +1198,7 @@ onUnmounted(() => { } stopAutoRefresh(); + stopActiveWashRefresh(); markDestroying(); setShowFooterInContent(true); }); @@ -1198,6 +1326,15 @@ watch( { deep: true } ); +watch([() => washInProgress.value, () => washLaneId.value], ([inProgress]) => { + if (inProgress) { + startActiveWashRefresh(); + return; + } + + stopActiveWashRefresh(); +}); + watch( () => nearestDepartment.value, (newValue) => { @@ -1219,11 +1356,7 @@ watch( (newLaneId) => { if (newLaneId && newLaneId !== "Any" && !isRestoring.value) { fetchSelfServeData().then(() => { - if ( - (!allVisibleQuestionsAnswered.value || shouldReturnToQuestionsForReview.value) && - !washInProgress.value && - currentStep.value > steps.QUESTIONS - ) { + if (!allVisibleQuestionsAnswered.value && !washInProgress.value && currentStep.value > steps.QUESTIONS) { currentStep.value = steps.QUESTIONS; } }); @@ -1513,7 +1646,7 @@ watch( icon-left="arrow-left" data-testid="self-serve-nav-previous" :disabled="previous.disabled || currentStep === steps.COMPLETED || washInProgress" - @click.prevent="previous.action" + @click.prevent="onPreviousStep(previous)" > {{ $t("common.previous") }} diff --git a/tests/unit/my-wash-start.spec.js b/tests/unit/my-wash-start.spec.js index 16710447..3b72d38a 100644 --- a/tests/unit/my-wash-start.spec.js +++ b/tests/unit/my-wash-start.spec.js @@ -1280,4 +1280,61 @@ describe("MyWashStart", () => { "display: none" ); }); + + it("clears restored in-progress state when active wash refresh says the lane is no longer in progress", async () => { + mocks.restoredProgressPayload = { + washInProgress: true, + washLaneId: 7, + washStartTime: Date.now() - 30_000, + licensePlateInput: "AB12345", + vehicleTypeSelect: 2, + radioWashType: "Manual", + radioLaneOption: 7, + customerNumberInput: 12345679, + isForcingNearestDepartment: false, + forceNearestDepartmentEvaluationId: 0, + answers: {}, + completedTasks: {}, + currentStep: 4, + }; + mocks.sessionRequest.mockImplementation(async (path, method, payload) => { + if (path === "/modules/self-serve/lane/wash/in-progress" && method === "GET" && payload?.lane_id === 7) { + return { + data: { + data: { + lane_id: 7, + in_progress: false, + session: null, + customer: null, + vehicle: null, + }, + }, + }; + } + + return undefined; + }); + + const wrapper = mountWithApp(MyWashStart, { + global: { + stubs: stubComponents, + }, + }); + + await flushPromises(); + await nextTick(); + await flushPromises(); + + expect(mocks.sessionRequest).toHaveBeenCalledWith("/modules/self-serve/lane/wash/in-progress", "GET", { + lane_id: 7, + }); + expect(mocks.stopElapsedTimer).toHaveBeenCalled(); + expect(mocks.clearProgress).toHaveBeenCalled(); + expect(mocks.fetchWashSummary).not.toHaveBeenCalled(); + expect(wrapper.get('[data-testid="self-serve-bottom-actions"]').attributes("style") || "").toContain( + "display: none" + ); + + wrapper.unmount(); + }); });