From d354ce773715ac1bd831821092c27e476ae85891 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Thu, 26 Mar 2026 14:13:15 +0100 Subject: [PATCH] Enhance self-serve functionality with dynamic images, gate handling, and machine-scoped vehicle type filtering: - Added dynamic image previews with query parameter handling and error recovery in `SelfServeTryModal.vue`. - Introduced outside gate controls (`Entrance` and `Exit`) in `SelfServeMachineRelayControls.vue` with corresponding command handling logic. - Enhanced vehicle type scoping logic in `SelfServeTasksPagination.vue` and `SelfServeConditionsPagination.vue` to ensure machine-type filtering consistency. - Updated related unit tests to verify new dynamic image logic, gate controls, and scoping behaviors. --- .../department/tables/SelfServeTryModal.vue | 92 +++++++++++++++++++ .../SelfServeConditionsPagination.vue | 10 +- .../SelfServeTasksPagination.vue | 10 +- src/composables/useWashSessionActions.js | 2 +- .../self-serve/DepartmentSelfServeStudio.vue | 5 +- .../components/SelfServeConnectivityRelay.vue | 2 +- .../SelfServeMachineConnectivity.vue | 30 ++++++ .../machine/SelfServeMachineRelayControls.vue | 27 +++++- .../self-serve-machine-relay-controls.spec.js | 38 ++++++++ ...elf-serve-pagination-machine-scope.spec.js | 27 ++++++ ...lf-serve-studio-vehicle-type-scope.spec.js | 3 + tests/unit/self-serve-try-modal.spec.js | 81 ++++++++++++++++ 12 files changed, 318 insertions(+), 9 deletions(-) create mode 100644 tests/unit/self-serve-pagination-machine-scope.spec.js diff --git a/src/components/displays/department/tables/SelfServeTryModal.vue b/src/components/displays/department/tables/SelfServeTryModal.vue index 0f495ff7..33a5da08 100644 --- a/src/components/displays/department/tables/SelfServeTryModal.vue +++ b/src/components/displays/department/tables/SelfServeTryModal.vue @@ -6,6 +6,7 @@ import { SessionUser } from "@/components/session/token/SessionUser.vue"; import { useSelfServeLogic } from "@/composables/useSelfServeLogic"; import SelfServeQuestionCards from "@/components/displays/selfServe/SelfServeQuestionCards.vue"; import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue"; +import { API_URL } from "@/config"; const { t: $t } = useI18n(); @@ -133,6 +134,79 @@ const canClearAnswers = computed(() => ( && normalizedReg.value.length >= 2 && !loading.value )); +const hideDynamicImage = ref(false); + +const parseNonNegativeInt = (value) => { + const parsed = parseInt(value); + if (Number.isNaN(parsed) || parsed < 0) { + return null; + } + return parsed; +}; + +const dynamicImageButtons = computed(() => { + const buttonIds = new Set(); + + activeTasks.value.forEach((task) => { + if (!Array.isArray(task?.buttons)) { + return; + } + + task.buttons.forEach((button) => { + const buttonId = parseInt(button); + if (!Number.isNaN(buttonId) && buttonId >= 0 && buttonId <= 11) { + buttonIds.add(buttonId); + } + }); + }); + + return [...buttonIds]; +}); + +const dynamicImageThumbPosition = computed(() => { + for (const task of activeTasks.value) { + const parsedPosition = parseNonNegativeInt(task?.dynamic_images_vehicle_type); + if (parsedPosition !== null) { + return parsedPosition; + } + } + + return null; +}); + +const hasDynamicImageContext = computed(() => ( + dynamicImageButtons.value.length > 0 || dynamicImageThumbPosition.value !== null +)); + +const dynamicImageUrl = computed(() => { + const departmentId = parseInt(props.departmentId); + const laneId = parseInt(selectedLaneId.value); + if (Number.isNaN(departmentId) || departmentId <= 0 || Number.isNaN(laneId) || laneId <= 0 || !hasDynamicImageContext.value) { + return null; + } + + const params = new URLSearchParams({ + department: String(departmentId), + lane: String(laneId), + current_step: "0", + buttons: JSON.stringify(dynamicImageButtons.value), + }); + + const selectedVehicleType = parseInt(selectedVehicleTypeId.value); + if (!Number.isNaN(selectedVehicleType) && selectedVehicleType > 0) { + params.set("vehicle_type", String(selectedVehicleType)); + } + + if (dynamicImageThumbPosition.value !== null) { + params.set("thumb_position", String(dynamicImageThumbPosition.value)); + } + + return `${API_URL}/department/lanes/dynamic-image?${params.toString()}`; +}); + +const displayedDynamicImageUrl = computed(() => ( + hideDynamicImage.value ? null : dynamicImageUrl.value +)); const loadLanes = async () => { const lanes = await SessionUser.objects.department_lanes.get.all(); @@ -215,6 +289,10 @@ const closeModal = () => { emit("closeModal"); }; +const onDynamicImageError = () => { + hideDynamicImage.value = true; +}; + onMounted(async () => { await loadLanes(); await loadVehicleTypes(); @@ -232,6 +310,10 @@ watch(() => [selectedLaneId.value, reg.value, normalizeVehicleTypeId(selectedVeh await refresh(); }); + +watch(dynamicImageUrl, () => { + hideDynamicImage.value = false; +});