Merge pull request #57 from copenhagentruckwash/fix-bugs-in-start-wash-flow
Harden customer wash start flow
This commit is contained in:
@@ -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<null | (() => void)>(null);
|
||||
const activeWashRefreshInterval = ref<ReturnType<typeof window.setInterval> | 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<string, any> = 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") }}
|
||||
</b-button>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user