Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18ab007f87 |
Generated
+10
-29
@@ -41,7 +41,6 @@
|
||||
"dayspan-vuetify": "^0.4.0",
|
||||
"flow": "^0.2.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"pinia": "^3.0.4",
|
||||
"qrcode": "^1.5.4",
|
||||
"sass": "^1.81.1",
|
||||
"sweetalert2": "11.22.4",
|
||||
@@ -5070,6 +5069,7 @@
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/devtools-shared": "^7.7.9",
|
||||
@@ -5085,6 +5085,7 @@
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"rfdc": "^1.4.1"
|
||||
@@ -5736,6 +5737,7 @@
|
||||
"version": "2.9.0",
|
||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/antfu"
|
||||
@@ -6423,6 +6425,7 @@
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-what": "^5.2.0"
|
||||
@@ -8985,6 +8988,7 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
||||
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
@@ -9567,6 +9571,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
@@ -10104,6 +10109,7 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/performance-now": {
|
||||
@@ -10165,34 +10171,6 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz",
|
||||
"integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^7.7.7"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/posva"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.5.0",
|
||||
"vue": "^3.5.11"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/@vue/devtools-api": {
|
||||
"version": "7.7.9",
|
||||
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.9.tgz",
|
||||
"integrity": "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-kit": "^7.7.9"
|
||||
}
|
||||
},
|
||||
"node_modules/pixelmatch": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
|
||||
@@ -10750,6 +10728,7 @@
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rgbcolor": {
|
||||
@@ -11667,6 +11646,7 @@
|
||||
"version": "14.0.1",
|
||||
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -11938,6 +11918,7 @@
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
||||
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-anything": "^4"
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
"dayspan-vuetify": "^0.4.0",
|
||||
"flow": "^0.2.3",
|
||||
"html2canvas": "^1.4.1",
|
||||
"pinia": "^3.0.4",
|
||||
"qrcode": "^1.5.4",
|
||||
"sass": "^1.81.1",
|
||||
"sweetalert2": "11.22.4",
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { BSkeleton } from "buefy";
|
||||
|
||||
const props = defineProps<{
|
||||
src: string | null;
|
||||
alt?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "error"): void;
|
||||
(e: "load"): void;
|
||||
}>();
|
||||
|
||||
const isLoading = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.src,
|
||||
(src) => {
|
||||
isLoading.value = !!src;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const onLoad = () => {
|
||||
isLoading.value = false;
|
||||
emit("load");
|
||||
};
|
||||
|
||||
const onError = () => {
|
||||
isLoading.value = false;
|
||||
emit("error");
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="src"
|
||||
class="self-serve-dynamic-image-frame mb-4"
|
||||
:class="{ 'is-loading': isLoading }"
|
||||
data-testid="self-serve-dynamic-image-frame"
|
||||
>
|
||||
<b-skeleton
|
||||
v-if="isLoading"
|
||||
class="self-serve-dynamic-image-skeleton"
|
||||
width="100%"
|
||||
height="100%"
|
||||
data-testid="self-serve-dynamic-image-skeleton"
|
||||
/>
|
||||
<img
|
||||
:key="src"
|
||||
:src="src"
|
||||
:alt="alt || 'Machine status'"
|
||||
data-testid="self-serve-dynamic-image"
|
||||
class="self-serve-dynamic-image"
|
||||
:class="{ 'is-loading': isLoading }"
|
||||
@load="onLoad"
|
||||
@error="onError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.self-serve-dynamic-image-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.self-serve-dynamic-image-frame {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
.self-serve-dynamic-image-skeleton {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.self-serve-dynamic-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.self-serve-dynamic-image.is-loading {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { BIcon, BMessage, BRadioButton } from "buefy";
|
||||
|
||||
const props = defineProps<{
|
||||
defineProps<{
|
||||
lanes: Array<any>;
|
||||
selectedLaneId: number | string | null;
|
||||
washType: string;
|
||||
@@ -15,8 +14,6 @@ const emit = defineEmits<{
|
||||
(e: "update:selectedLaneId", value: number): void;
|
||||
(e: "update:washType", value: string): void;
|
||||
}>();
|
||||
|
||||
const isSelectedMachineAvailable = computed(() => props.isMachineAvailable(props.selectedLaneId));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -95,16 +92,16 @@ const isSelectedMachineAvailable = computed(() => props.isMachineAvailable(props
|
||||
:model-value="washType"
|
||||
native-value="Machine"
|
||||
type="is-link"
|
||||
:disabled="!isSelectedMachineAvailable"
|
||||
:title="!isSelectedMachineAvailable ? $t('self_wash.unavailable') : null"
|
||||
:aria-label="!isSelectedMachineAvailable ? $t('self_wash.unavailable') : null"
|
||||
:disabled="!isMachineAvailable(selectedLaneId)"
|
||||
:title="!isMachineAvailable(selectedLaneId) ? $t('self_wash.machine_unavailable_for_lane') : null"
|
||||
:aria-label="!isMachineAvailable(selectedLaneId) ? $t('self_wash.machine_unavailable_for_lane') : null"
|
||||
data-testid="self-serve-wash-type-machine"
|
||||
@update:model-value="emit('update:washType', 'Machine')"
|
||||
@input="emit('update:washType', 'Machine')"
|
||||
>
|
||||
<span>
|
||||
<span>{{ $t("self_wash.machine") }}<br /></span>
|
||||
<span v-if="!isSelectedMachineAvailable">
|
||||
<span v-if="!isMachineAvailable(selectedLaneId)">
|
||||
<small>
|
||||
<b-icon icon="times-circle" type="is-danger" pack="fas" class="mr-1" />
|
||||
{{ $t("self_wash.unavailable") }}
|
||||
@@ -120,6 +117,14 @@ const isSelectedMachineAvailable = computed(() => props.isMachineAvailable(props
|
||||
</b-radio-button>
|
||||
</div>
|
||||
</div>
|
||||
<b-message
|
||||
v-if="!isMachineAvailable(selectedLaneId)"
|
||||
type="is-warning"
|
||||
aria-close-label="Luk besked"
|
||||
data-testid="self-serve-machine-unavailable-guidance"
|
||||
>
|
||||
{{ $t("self_wash.machine_unavailable_for_lane") }}
|
||||
</b-message>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { BIcon } from "buefy";
|
||||
import SelfServeDynamicImageFrame from "@/components/displays/selfServe/SelfServeDynamicImageFrame.vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { BIcon, BSkeleton } from "buefy";
|
||||
import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -18,6 +18,12 @@ const emit = defineEmits<{
|
||||
(e: "clear-dynamic-image"): void;
|
||||
}>();
|
||||
|
||||
const isDynamicImageLoading = ref(false);
|
||||
|
||||
watch(() => props.dynamicImageUrl, (dynamicImageUrl) => {
|
||||
isDynamicImageLoading.value = !!dynamicImageUrl;
|
||||
}, { immediate: true });
|
||||
|
||||
const emitToggleTask = (taskId: number, value: boolean) => {
|
||||
emit("toggle-task", taskId, value);
|
||||
};
|
||||
@@ -26,7 +32,12 @@ const emitDownloadAttachment = (taskId: number, attachmentId: number | undefined
|
||||
emit("download-attachment", taskId, attachmentId, attachment);
|
||||
};
|
||||
|
||||
const onDynamicImageLoad = () => {
|
||||
isDynamicImageLoading.value = false;
|
||||
};
|
||||
|
||||
const onDynamicImageError = () => {
|
||||
isDynamicImageLoading.value = false;
|
||||
emit("clear-dynamic-image");
|
||||
};
|
||||
</script>
|
||||
@@ -47,11 +58,30 @@ const onDynamicImageError = () => {
|
||||
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
|
||||
</div>
|
||||
|
||||
<SelfServeDynamicImageFrame
|
||||
:src="dynamicImageUrl"
|
||||
alt="Machine status"
|
||||
@error="onDynamicImageError"
|
||||
/>
|
||||
<div
|
||||
v-if="dynamicImageUrl"
|
||||
class="self-serve-dynamic-image-frame mb-4"
|
||||
:class="{ 'is-loading': isDynamicImageLoading }"
|
||||
data-testid="self-serve-dynamic-image-frame"
|
||||
>
|
||||
<b-skeleton
|
||||
v-if="isDynamicImageLoading"
|
||||
class="self-serve-dynamic-image-skeleton"
|
||||
width="100%"
|
||||
height="100%"
|
||||
data-testid="self-serve-dynamic-image-skeleton"
|
||||
/>
|
||||
<img
|
||||
:key="dynamicImageUrl"
|
||||
:src="dynamicImageUrl"
|
||||
alt="Machine status"
|
||||
data-testid="self-serve-dynamic-image"
|
||||
class="self-serve-dynamic-image"
|
||||
:class="{ 'is-loading': isDynamicImageLoading }"
|
||||
@load="onDynamicImageLoad"
|
||||
@error="onDynamicImageError"
|
||||
/>
|
||||
</div>
|
||||
<div v-show="allVisibleQuestionsAnswered && !editAnswers" class="notification is-info is-light mb-4">
|
||||
<h1 class="title has-text-centered mb-2" v-if="activeTasks.length > 0">{{ $t("self_wash.start_machine") }}</h1>
|
||||
<h1 class="title has-text-centered mb-2" v-else>{{ $t("self_wash.questions_answered") }}</h1>
|
||||
@@ -65,3 +95,41 @@ const onDynamicImageError = () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.self-serve-dynamic-image-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 769px) {
|
||||
.self-serve-dynamic-image-frame {
|
||||
max-width: 640px;
|
||||
}
|
||||
}
|
||||
|
||||
.self-serve-dynamic-image-skeleton {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.self-serve-dynamic-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.self-serve-dynamic-image.is-loading {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -252,7 +252,7 @@ const onResendInvite = async (subuser) => {
|
||||
|
||||
<td>
|
||||
<div class="buttons is-justify-content-flex-end action-buttons">
|
||||
<ActionSettingsWheelButton v-if="hasRowActions(subuser)" display-actions-directly>
|
||||
<ActionSettingsWheelButton v-if="hasRowActions(subuser)">
|
||||
<template #actions>
|
||||
<ActionSettingsWheelItem
|
||||
v-if="canEditPermissions() && subuser.grant_id"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { createPinia, defineStore, getActivePinia, setActivePinia, storeToRefs } from "pinia";
|
||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||
import { safeAttachmentDownloadLink } from "@/services/attachmentDownloadLinks.js";
|
||||
|
||||
@@ -272,26 +271,7 @@ const mergeByNumericId = (existingItems, incomingItems) => {
|
||||
return merged;
|
||||
};
|
||||
|
||||
let selfServeLogicStoreInstance = 0;
|
||||
let fallbackSelfServePinia = null;
|
||||
|
||||
const ensureSelfServePinia = () => {
|
||||
if (typeof document === "undefined") {
|
||||
fallbackSelfServePinia ||= createPinia();
|
||||
setActivePinia(fallbackSelfServePinia);
|
||||
return fallbackSelfServePinia;
|
||||
}
|
||||
|
||||
const activePinia = getActivePinia();
|
||||
if (activePinia) {
|
||||
return activePinia;
|
||||
}
|
||||
fallbackSelfServePinia ||= createPinia();
|
||||
setActivePinia(fallbackSelfServePinia);
|
||||
return fallbackSelfServePinia;
|
||||
};
|
||||
|
||||
const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
export function useSelfServeLogic() {
|
||||
const loading = ref(false);
|
||||
const requestError = ref(null);
|
||||
const preview = ref(null);
|
||||
@@ -317,6 +297,30 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
const summaryVisibleQuestionIds = ref([]);
|
||||
const summaryQuestionOrder = ref({});
|
||||
const latestFetchRequestId = ref(0);
|
||||
const latestSuccessfulFetchKey = ref(null);
|
||||
const inFlightFetchKey = ref(null);
|
||||
|
||||
const createFetchKey = (departmentId, vehicleTypeId, laneId, reg) => {
|
||||
const normalizedDepartmentId = parseInt(departmentId);
|
||||
const normalizedLaneId = parseInt(laneId);
|
||||
const normalizedReg = String(reg || "").trim().toUpperCase();
|
||||
const normalizedVehicleTypeId = parseInt(vehicleTypeId);
|
||||
const vehicleTypeKey = !Number.isNaN(normalizedVehicleTypeId) && normalizedVehicleTypeId > 0
|
||||
? normalizedVehicleTypeId
|
||||
: "";
|
||||
|
||||
if (
|
||||
Number.isNaN(normalizedDepartmentId)
|
||||
|| normalizedDepartmentId <= 0
|
||||
|| Number.isNaN(normalizedLaneId)
|
||||
|| normalizedLaneId <= 0
|
||||
|| normalizedReg.length < 2
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [normalizedDepartmentId, normalizedLaneId, normalizedReg, vehicleTypeKey].join("|");
|
||||
};
|
||||
|
||||
const beginFetchRequest = () => {
|
||||
latestFetchRequestId.value += 1;
|
||||
@@ -595,10 +599,17 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSelfServeData = async (_departmentId, _vehicleTypeId, laneId = null, reg = null) => {
|
||||
const fetchSelfServeData = async (_departmentId, _vehicleTypeId, laneId = null, reg = null, options = {}) => {
|
||||
const fetchKey = createFetchKey(_departmentId, _vehicleTypeId, laneId, reg);
|
||||
if (!options.force && fetchKey && (fetchKey === latestSuccessfulFetchKey.value || fetchKey === inFlightFetchKey.value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestId = beginFetchRequest();
|
||||
|
||||
if (!laneId || !reg || reg.trim().length < 2) {
|
||||
latestSuccessfulFetchKey.value = null;
|
||||
inFlightFetchKey.value = null;
|
||||
preview.value = null;
|
||||
summary.value = null;
|
||||
session.value = null;
|
||||
@@ -626,6 +637,7 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
|
||||
loading.value = true;
|
||||
requestError.value = null;
|
||||
inFlightFetchKey.value = fetchKey;
|
||||
try {
|
||||
const normalizedReg = reg.trim().toUpperCase();
|
||||
const normalizedVehicleTypeId = parseInt(_vehicleTypeId);
|
||||
@@ -707,6 +719,10 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
setSummaryVisibleQuestions(questions.value);
|
||||
}
|
||||
|
||||
if (isFetchRequestActive(requestId)) {
|
||||
latestSuccessfulFetchKey.value = fetchKey;
|
||||
}
|
||||
|
||||
return previewData;
|
||||
} catch (error) {
|
||||
console.error("Error fetching self-serve preview:", error);
|
||||
@@ -715,6 +731,9 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
if (inFlightFetchKey.value === fetchKey) {
|
||||
inFlightFetchKey.value = null;
|
||||
}
|
||||
if (isFetchRequestActive(requestId)) {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -769,7 +788,7 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
updateResolvedVehicleTypeId(responseSummary, responseSummary?.session);
|
||||
}
|
||||
|
||||
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg);
|
||||
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg, { force: true });
|
||||
answers.value = {
|
||||
...answers.value,
|
||||
[parseInt(questionId)]: value,
|
||||
@@ -834,7 +853,7 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
? normalizedRefreshVehicleTypeId
|
||||
: null;
|
||||
|
||||
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg);
|
||||
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg, { force: true });
|
||||
|
||||
return { deletedCount: conditionIdsToDelete.length };
|
||||
} catch (error) {
|
||||
@@ -1095,50 +1114,4 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||
answerQuestion,
|
||||
removeAnswer,
|
||||
};
|
||||
});
|
||||
|
||||
export function useSelfServeLogic(options = {}) {
|
||||
const pinia = ensureSelfServePinia();
|
||||
const explicitStoreId = typeof options === "string" ? options : options?.storeId;
|
||||
const storeId = explicitStoreId || `selfServeLogic:${++selfServeLogicStoreInstance}`;
|
||||
const store = createSelfServeLogicStore(storeId)(pinia);
|
||||
const refs = storeToRefs(store);
|
||||
const {
|
||||
fetchSelfServeData,
|
||||
fetchWashSummary,
|
||||
syncVehicleAnswer,
|
||||
clearVehicleAnswers,
|
||||
evaluateRule,
|
||||
evaluateCondition,
|
||||
isQuestionVisible,
|
||||
isTaskActive,
|
||||
isServiceAllowed,
|
||||
updateLaneAllowedServices,
|
||||
enableMachineRelay,
|
||||
isImage,
|
||||
downloadAttachment,
|
||||
reset,
|
||||
answerQuestion,
|
||||
removeAnswer,
|
||||
} = store;
|
||||
|
||||
return {
|
||||
...refs,
|
||||
fetchSelfServeData,
|
||||
fetchWashSummary,
|
||||
syncVehicleAnswer,
|
||||
clearVehicleAnswers,
|
||||
evaluateRule,
|
||||
evaluateCondition,
|
||||
isQuestionVisible,
|
||||
isTaskActive,
|
||||
isServiceAllowed,
|
||||
updateLaneAllowedServices,
|
||||
enableMachineRelay,
|
||||
isImage,
|
||||
downloadAttachment,
|
||||
reset,
|
||||
answerQuestion,
|
||||
removeAnswer,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ export function useWashSessionActions(options) {
|
||||
const startResponse = await executeSelfServeCommand(laneId, "START", {
|
||||
customer_number: parseInt(customerNumber),
|
||||
license_plate: licensePlate.trim().toUpperCase(),
|
||||
defer_relay_side_effects: false,
|
||||
defer_relay_side_effects: true,
|
||||
});
|
||||
if (!startResponse) {
|
||||
return false;
|
||||
|
||||
@@ -4595,6 +4595,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.indlæser'} @:{'words.generated.tilgængelige'} @:{'words.generated.køretøjstyper'}...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||
"machine": "@.capitalize:{'words.generated.maskine'}",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.",
|
||||
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||
"occupied": "Optaget",
|
||||
"open_property_access_gate": "@:{'words.generated.abn'} adgangsport",
|
||||
|
||||
@@ -4705,6 +4705,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.verf'}?@:{'words.generated.gbare'} @:{'words.generated.fahrzeugtypen'} @:{'words.generated.werden'} @:{'words.generated.geladen'}...",
|
||||
"lane_unavailable": "Waschspur nicht verfugbar",
|
||||
"machine": "@:{'words.generated.maschine'}",
|
||||
"machine_unavailable_for_lane": "Maschinenwasche ist fur die ausgewahlte Spur nicht verfugbar. Wahlen Sie manuelle Wasche oder eine andere freie Spur.",
|
||||
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||
"occupied": "Belegt",
|
||||
"open_property_access_gate": "Grundstueckszugangstor @:{'words.generated.oeffnen'}",
|
||||
|
||||
@@ -4429,6 +4429,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.loading'} @:{'words.generated.available'} @:{'words.generated.vehicle'} @:{'words.generated.types'}...",
|
||||
"lane_unavailable": "Lane unavailable",
|
||||
"machine": "@.capitalize:{'words.generated.machine'}",
|
||||
"machine_unavailable_for_lane": "Machine wash is unavailable for the selected lane. Choose manual wash or another available lane.",
|
||||
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||
"occupied": "Occupied",
|
||||
"open_property_access_gate": "@.capitalize:{'words.generated.open'} @:{'words.generated.property'} @:{'words.generated.access'} @:{'words.generated.gate'}",
|
||||
|
||||
@@ -4224,6 +4224,7 @@
|
||||
"loading_vehicle_types": "@:{'templates.generated.compat.self_wash.loading_vehicle_types'}",
|
||||
"lane_unavailable": "@:{'templates.generated.compat.self_wash.lane_unavailable'}",
|
||||
"machine": "@:{'templates.generated.compat.self_wash.machine'}",
|
||||
"machine_unavailable_for_lane": "@:{'templates.generated.compat.self_wash.machine_unavailable_for_lane'}",
|
||||
"no_departments_found": "@:{'templates.generated.compat.self_wash.no_departments_found'}",
|
||||
"no_vehicles_found": "@:vehicles.no_vehicles",
|
||||
"occupied": "@:{'templates.generated.compat.self_wash.occupied'}",
|
||||
|
||||
@@ -4706,6 +4706,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.laster'} @:{'words.generated.inn'} @:{'words.generated.tilgjengelige'} @:{'words.generated.kjøretøytyper'}...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
||||
"machine": "@.capitalize:{'words.generated.maskin'}",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgjengelig for valgt bane. Velg manuell vask eller en annen ledig bane.",
|
||||
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||
"occupied": "Okkupert",
|
||||
"open_property_access_gate": "@:{'words.generated.aapne'} @:{'words.generated.eiendommens'} inngangsport",
|
||||
|
||||
@@ -4756,6 +4756,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'words.generated.laser'} @:{'words.generated.in'} @:{'words.generated.tillgangliga'} @:{'words.generated.fordonstyper'}...",
|
||||
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
||||
"machine": "@.capitalize:{'words.generated.machine'}",
|
||||
"machine_unavailable_for_lane": "Maskintvatt ar inte tillganglig for vald bana. Valj manuell tvatt eller en annan ledig bana.",
|
||||
"no_departments_found": "@.capitalize:{'words.generated.inga'} @:{'words.generated.avdelningar'} @:{'words.generated.hittades'}",
|
||||
"occupied": "Upptagen",
|
||||
"open_property_access_gate": "@:{'words.generated.oeppna'} @:{'words.generated.fastighetens'} ingangsgrind",
|
||||
|
||||
@@ -3663,6 +3663,7 @@
|
||||
"loading_vehicle_types": "Indlæser tilgængelige køretøjstyper...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||
"machine": "Maskine",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.",
|
||||
"vehicle_step_missing_customer_number": "Indtast kundenummer for at fortsætte.",
|
||||
"vehicle_step_missing_department": "Vent på, at nærmeste afdeling er indlæst, før du fortsætter.",
|
||||
"vehicle_step_missing_registration": "Indtast registreringsnummer for at fortsætte.",
|
||||
|
||||
@@ -3663,6 +3663,7 @@
|
||||
"loading_vehicle_types": "Verf?gbare Fahrzeugtypen werden geladen...",
|
||||
"lane_unavailable": "Waschspur nicht verfugbar",
|
||||
"machine": "Maschine",
|
||||
"machine_unavailable_for_lane": "Maschinenwasche ist fur die ausgewahlte Spur nicht verfugbar. Wahlen Sie manuelle Wasche oder eine andere freie Spur.",
|
||||
"vehicle_step_missing_customer_number": "Geben Sie Ihre Kundennummer ein, um fortzufahren.",
|
||||
"vehicle_step_missing_department": "Warten Sie, bis die nachste Abteilung geladen ist, bevor Sie fortfahren.",
|
||||
"vehicle_step_missing_registration": "Geben Sie das Kennzeichen ein, um fortzufahren.",
|
||||
|
||||
@@ -3662,6 +3662,7 @@
|
||||
"loading_vehicle_types": "Loading available vehicle types...",
|
||||
"lane_unavailable": "Lane unavailable",
|
||||
"machine": "Machine",
|
||||
"machine_unavailable_for_lane": "Machine wash is unavailable for the selected lane. Choose manual wash or another available lane.",
|
||||
"vehicle_step_missing_customer_number": "Enter your customer number to continue.",
|
||||
"vehicle_step_missing_department": "Wait for the nearest department to load before continuing.",
|
||||
"vehicle_step_missing_registration": "Enter your registration number to continue.",
|
||||
|
||||
@@ -3663,6 +3663,7 @@
|
||||
"loading_vehicle_types": "Laster inn tilgjengelige kjøretøytyper...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
||||
"machine": "Maskin",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgjengelig for valgt bane. Velg manuell vask eller en annen ledig bane.",
|
||||
"vehicle_step_missing_customer_number": "Skriv inn kundenummer for a fortsette.",
|
||||
"vehicle_step_missing_department": "Vent til naermeste avdeling er lastet inn for du fortsetter.",
|
||||
"vehicle_step_missing_registration": "Skriv inn registreringsnummer for a fortsette.",
|
||||
|
||||
@@ -3663,6 +3663,7 @@
|
||||
"loading_vehicle_types": "Läser in tillgängliga fordonstyper...",
|
||||
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
||||
"machine": "Machine",
|
||||
"machine_unavailable_for_lane": "Maskintvatt ar inte tillganglig for vald bana. Valj manuell tvatt eller en annan ledig bana.",
|
||||
"vehicle_step_missing_customer_number": "Ange kundnummer for att fortsatta.",
|
||||
"vehicle_step_missing_department": "Vanta tills narmaste avdelning har laddats innan du fortsatter.",
|
||||
"vehicle_step_missing_registration": "Ange registreringsnummer for att fortsatta.",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'terms.glossary.indlæser'} @:{'terms.glossary.tilgængelige'} @:{'terms.glossary.køretøjstyper'}...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||
"machine": "@.capitalize:{'terms.glossary.maskine'}",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.",
|
||||
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||
"occupied": "Optaget",
|
||||
"open_property_access_gate": "@:{'terms.glossary.abn'} adgangsport",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'terms.glossary.verf'}?@:{'terms.glossary.gbare'} @:{'terms.glossary.fahrzeugtypen'} @:{'terms.glossary.werden'} @:{'terms.glossary.geladen'}...",
|
||||
"lane_unavailable": "Waschspur nicht verfugbar",
|
||||
"machine": "@:{'terms.glossary.maschine'}",
|
||||
"machine_unavailable_for_lane": "Maschinenwasche ist fur die ausgewahlte Spur nicht verfugbar. Wahlen Sie manuelle Wasche oder eine andere freie Spur.",
|
||||
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||
"occupied": "Belegt",
|
||||
"open_property_access_gate": "Grundstueckszugangstor @:{'terms.glossary.oeffnen'}",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'terms.glossary.loading'} @:{'terms.glossary.available'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.types'}...",
|
||||
"lane_unavailable": "Lane unavailable",
|
||||
"machine": "@.capitalize:{'terms.glossary.machine'}",
|
||||
"machine_unavailable_for_lane": "Machine wash is unavailable for the selected lane. Choose manual wash or another available lane.",
|
||||
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||
"occupied": "Occupied",
|
||||
"open_property_access_gate": "@.capitalize:{'terms.glossary.open'} @:{'terms.glossary.property'} @:{'terms.glossary.access'} @:{'terms.glossary.gate'}",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"loading_vehicle_types": "@:{'phrases.compat.self_wash.loading_vehicle_types'}",
|
||||
"lane_unavailable": "@:{'phrases.compat.self_wash.lane_unavailable'}",
|
||||
"machine": "@:{'phrases.compat.self_wash.machine'}",
|
||||
"machine_unavailable_for_lane": "@:{'phrases.compat.self_wash.machine_unavailable_for_lane'}",
|
||||
"no_departments_found": "@:{'phrases.compat.self_wash.no_departments_found'}",
|
||||
"no_vehicles_found": "@:vehicles.no_vehicles",
|
||||
"occupied": "@:{'phrases.compat.self_wash.occupied'}",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'terms.glossary.laster'} @:{'terms.glossary.inn'} @:{'terms.glossary.tilgjengelige'} @:{'terms.glossary.kjøretøytyper'}...",
|
||||
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
||||
"machine": "@.capitalize:{'terms.glossary.maskin'}",
|
||||
"machine_unavailable_for_lane": "Maskinvask er ikke tilgjengelig for valgt bane. Velg manuell vask eller en annen ledig bane.",
|
||||
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||
"occupied": "Okkupert",
|
||||
"open_property_access_gate": "@:{'terms.glossary.aapne'} @:{'terms.glossary.eiendommens'} inngangsport",
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"loading_vehicle_types": "@.capitalize:{'terms.glossary.laser'} @:{'terms.glossary.in'} @:{'terms.glossary.tillgangliga'} @:{'terms.glossary.fordonstyper'}...",
|
||||
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
||||
"machine": "@.capitalize:{'terms.glossary.machine'}",
|
||||
"machine_unavailable_for_lane": "Maskintvatt ar inte tillganglig for vald bana. Valj manuell tvatt eller en annan ledig bana.",
|
||||
"no_departments_found": "@.capitalize:{'terms.glossary.inga'} @:{'terms.glossary.avdelningar'} @:{'terms.glossary.hittades'}",
|
||||
"occupied": "Upptagen",
|
||||
"open_property_access_gate": "@:{'terms.glossary.oeppna'} @:{'terms.glossary.fastighetens'} ingangsgrind",
|
||||
|
||||
@@ -10,7 +10,6 @@ import applyMiddleware from '@/middleware/index';
|
||||
import store from './store/user.vue'
|
||||
import { createHead } from "@vueuse/head";
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import Router from "@/router";
|
||||
import App from "@/App.vue";
|
||||
import Buefy from 'buefy'
|
||||
@@ -109,7 +108,6 @@ applyMiddleware(Router)
|
||||
const app = createApp(App)
|
||||
.use(Router)
|
||||
.use(createHead())
|
||||
.use(createPinia())
|
||||
.use(store)
|
||||
.use(ToastPlugin)
|
||||
.use(Buefy)
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
const createValidationState = () => ({ valid: true, items: [], errors: [], warnings: [] });
|
||||
|
||||
const createGatewayWorkspaceState = () => ({ gateways: [], relays: [], lanes: [], issues: [], actions: [] });
|
||||
|
||||
const createSimulatorForm = () => ({
|
||||
lane_id: null,
|
||||
reg: "TEST123",
|
||||
customer_number: null,
|
||||
config_source: "draft",
|
||||
include_hardware: true,
|
||||
hardware_mode: "studio",
|
||||
});
|
||||
|
||||
const createPathEditorQuestionDraft = () => ({
|
||||
question: "New question",
|
||||
description: "",
|
||||
scope: "department_lane",
|
||||
vehicle_mode: "current",
|
||||
vehicle_type_values: [],
|
||||
});
|
||||
|
||||
const createPathEditorForm = () => ({
|
||||
path_key: "",
|
||||
previous_path_key: "",
|
||||
answers: {},
|
||||
machine_allowed: true,
|
||||
task: "Start machine",
|
||||
description: "",
|
||||
services: ["MACHINE"],
|
||||
program_button: "",
|
||||
press_reset: false,
|
||||
press_start: true,
|
||||
extra_buttons: [],
|
||||
dynamic_images_vehicle_type: "0",
|
||||
});
|
||||
|
||||
const createVirtualHardwareForm = () => ({
|
||||
gateway_key: "virtual-main",
|
||||
gateway_label: "Virtual Studio Gateway",
|
||||
relay_id: "",
|
||||
role: "MACHINE",
|
||||
label: "",
|
||||
});
|
||||
|
||||
const createScopeFilters = () => ({
|
||||
lane_id: "",
|
||||
vehicle_type_id: "",
|
||||
});
|
||||
|
||||
const createCanvasFilters = () => ({
|
||||
...createScopeFilters(),
|
||||
machine_type_id: "",
|
||||
dynamic_image_id: "",
|
||||
});
|
||||
|
||||
export const useSelfServeStudioStore = defineStore("selfServeStudio", {
|
||||
state: () => ({
|
||||
flowNodes: [],
|
||||
flowEdges: [],
|
||||
graphPayload: null,
|
||||
lookups: {},
|
||||
validation: createValidationState(),
|
||||
gatewayWorkspace: createGatewayWorkspaceState(),
|
||||
versions: [],
|
||||
activePanel: "flow",
|
||||
selectedNodeId: null,
|
||||
inspectorForm: {},
|
||||
simulatorForm: createSimulatorForm(),
|
||||
simulatorResult: null,
|
||||
simulatorAnswerOverrides: {},
|
||||
simulatorShowJson: false,
|
||||
simulatorDynamicImageHidden: false,
|
||||
simulatorDynamicImageError: "",
|
||||
simulatorCompletedTasks: {},
|
||||
isSimulatorRunning: false,
|
||||
simulatorRunReason: "",
|
||||
simulatorRequestSequence: 0,
|
||||
simulatorLastRequestKey: "",
|
||||
simulatorLastError: "",
|
||||
isRestoringSimulatorStorage: false,
|
||||
pathOutcomes: null,
|
||||
isLoadingPathOutcomes: false,
|
||||
pathOutcomesError: "",
|
||||
pathOutcomesRequestKey: "",
|
||||
selectedPathOutcomeId: null,
|
||||
selectedPathCaseSignature: "",
|
||||
selectedPathCaseIndex: 0,
|
||||
pathCaseRunSignatures: {},
|
||||
pathCaseConfirmingSignature: "",
|
||||
pathEditorDirty: false,
|
||||
isHydratingPathEditor: false,
|
||||
pathEditorTaskTitleOverrides: {},
|
||||
editingPathEditorTaskTitleKey: "",
|
||||
editingPathEditorTaskTitle: "",
|
||||
pathVerificationDynamicImageHidden: false,
|
||||
pathVerificationDynamicImageError: "",
|
||||
isPathVerificationDynamicImageLoading: false,
|
||||
pathEditorQuestionDraftVisible: false,
|
||||
pathEditorQuestionDraft: createPathEditorQuestionDraft(),
|
||||
pathEditorForm: createPathEditorForm(),
|
||||
simulatorCustomerSearchQuery: "",
|
||||
selectedSimulatorCustomer: null,
|
||||
showSimulatorCustomerResults: false,
|
||||
scopeGuideDraft: createScopeFilters(),
|
||||
isScopeGuideManuallyOpen: false,
|
||||
isSyncingScopeFromRoute: false,
|
||||
isWritingScopeToRoute: false,
|
||||
virtualHardwareForm: createVirtualHardwareForm(),
|
||||
laneEditorMode: "edit",
|
||||
selectedLaneManagementId: null,
|
||||
laneForm: {},
|
||||
lanePreviewHidden: false,
|
||||
inspectorLanePreviewHidden: false,
|
||||
inspectorTaskDynamicImageHidden: false,
|
||||
inspectorTaskDynamicImageError: "",
|
||||
searchTerm: "",
|
||||
filters: createCanvasFilters(),
|
||||
isLoading: false,
|
||||
isSaving: false,
|
||||
isUploadingTaskAttachment: false,
|
||||
deletingTaskAttachmentId: null,
|
||||
isStudioFullscreen: false,
|
||||
hasNativeStudioFullscreen: false,
|
||||
suppressLayoutSave: false,
|
||||
exportText: "",
|
||||
importText: "",
|
||||
draggedTemplateKind: null,
|
||||
unfilteredNodePositions: null,
|
||||
}),
|
||||
actions: {
|
||||
resetPathEditorQuestionDraft() {
|
||||
this.pathEditorQuestionDraft = createPathEditorQuestionDraft();
|
||||
},
|
||||
resetPathEditorForm() {
|
||||
this.pathEditorForm = createPathEditorForm();
|
||||
},
|
||||
applyRouteScope(scopeFilters = {}) {
|
||||
const nextScope = {
|
||||
...createScopeFilters(),
|
||||
...scopeFilters,
|
||||
};
|
||||
this.scopeGuideDraft = nextScope;
|
||||
this.filters = {
|
||||
...this.filters,
|
||||
...nextScope,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
+6
-67
@@ -95,25 +95,6 @@ const getDominantWeather = (entries) => {
|
||||
return dominant;
|
||||
};
|
||||
|
||||
const getProductivityStatus = (washes, hours) => {
|
||||
if (hours <= 0) {
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
const productivity = washes / hours;
|
||||
return productivity >= 1.3 ? "healthy" : productivity >= 1.0 ? "degraded" : "unhealthy";
|
||||
};
|
||||
|
||||
const getNormalizedTime = (entry) => {
|
||||
const timeValue = typeof entry?.time === "string" ? entry.time : "";
|
||||
const normalizedTime = timeValue.slice(0, 5);
|
||||
return /^\d{2}:\d{2}$/.test(normalizedTime) ? normalizedTime : null;
|
||||
};
|
||||
|
||||
const formatMergedNumber = (value) => {
|
||||
return Number.isInteger(value) ? value : Number(value.toFixed(2));
|
||||
};
|
||||
|
||||
const summarizeEntriesByDay = (entries) => {
|
||||
const groupedByDate = new Map();
|
||||
|
||||
@@ -139,48 +120,14 @@ const summarizeEntriesByDay = (entries) => {
|
||||
time: "00:00",
|
||||
current: dayEntries.some((entry) => entry?.current === true),
|
||||
weather: getDominantWeather(dayEntries),
|
||||
washes: formatMergedNumber(washes),
|
||||
washes: Number.isInteger(washes) ? washes : Number(washes.toFixed(2)),
|
||||
hours: Number(hours.toFixed(2)),
|
||||
/**
|
||||
* Below 1.0 cars/workhour = unhealthy (red)
|
||||
* 1.0-1.3 cars/workhour = degraded (yellow)
|
||||
* Above 1.3 cars/workhour = healthy (green)
|
||||
*/
|
||||
status: getProductivityStatus(washes, hours),
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const summarizeEntriesByHourOfDay = (entries) => {
|
||||
const groupedByTime = new Map();
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const time = getNormalizedTime(entry);
|
||||
if (!time) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!groupedByTime.has(time)) {
|
||||
groupedByTime.set(time, []);
|
||||
}
|
||||
groupedByTime.get(time).push(entry);
|
||||
});
|
||||
|
||||
return Array.from(groupedByTime.entries())
|
||||
.sort(([timeA], [timeB]) => timeA.localeCompare(timeB))
|
||||
.map(([time, hourEntries]) => {
|
||||
const washes = hourEntries.reduce((sum, entry) => sum + toNumberOrZero(entry?.washes), 0);
|
||||
const hours = hourEntries.reduce((sum, entry) => sum + toNumberOrZero(entry?.hours), 0);
|
||||
|
||||
return {
|
||||
date: null,
|
||||
test_id_date: "multi-day",
|
||||
time,
|
||||
current: hourEntries.some((entry) => entry?.current === true),
|
||||
weather: getDominantWeather(hourEntries),
|
||||
washes: formatMergedNumber(washes),
|
||||
hours: Number(hours.toFixed(2)),
|
||||
status: getProductivityStatus(washes, hours),
|
||||
status: hours > 0 ? (washes / hours >= 1.3 ? "healthy" : washes / hours >= 1.0 ? "degraded" : "unhealthy") : "unknown",
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -189,10 +136,6 @@ const hourlyDisplayedState = computed(() => {
|
||||
return state.value.filter((entry) => hasOperationalData(entry));
|
||||
});
|
||||
|
||||
const hourlyMergedDisplayedState = computed(() => {
|
||||
return summarizeEntriesByHourOfDay(hourlyDisplayedState.value).filter((entry) => hasOperationalData(entry));
|
||||
});
|
||||
|
||||
const dailyDisplayedState = computed(() => {
|
||||
return summarizeEntriesByDay(state.value).filter((entry) => hasOperationalData(entry));
|
||||
});
|
||||
@@ -201,9 +144,6 @@ const displayedState = computed(() => {
|
||||
if (summarizeDaily.value && isMultiDaySelection.value) {
|
||||
return dailyDisplayedState.value;
|
||||
}
|
||||
if (isMultiDaySelection.value) {
|
||||
return hourlyMergedDisplayedState.value;
|
||||
}
|
||||
return hourlyDisplayedState.value;
|
||||
});
|
||||
|
||||
@@ -227,9 +167,7 @@ const getEntryKey = (entry, index) => {
|
||||
};
|
||||
|
||||
const getEntryTestId = (prefix, entry) => {
|
||||
const date = typeof entry?.test_id_date === "string"
|
||||
? entry.test_id_date
|
||||
: isDateString(entry?.date) ? entry.date : "no-date";
|
||||
const date = isDateString(entry?.date) ? entry.date : "no-date";
|
||||
const time = typeof entry?.time === "string" && entry.time !== "" ? entry.time : "no-time";
|
||||
return `department-weather-${prefix}-${date}-${time}`;
|
||||
};
|
||||
@@ -239,8 +177,9 @@ const getHourDetailsSlotKey = (entry) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalizedTime = getNormalizedTime(entry);
|
||||
if (!normalizedTime) {
|
||||
const timeValue = typeof entry?.time === "string" ? entry.time : "";
|
||||
const normalizedTime = timeValue.slice(0, 5);
|
||||
if (!/^\d{2}:\d{2}$/.test(normalizedTime)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+101
-80
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { Handle, Position, VueFlow, useVueFlow } from "@vue-flow/core";
|
||||
import "@vue-flow/core/dist/style.css";
|
||||
@@ -35,7 +34,6 @@ import {
|
||||
formatSelfServeTaskButtons,
|
||||
normalizeSelfServeTaskButtons,
|
||||
} from "@/components/session/token/SessionUser/Objects/selfServeTaskButtons.js";
|
||||
import { useSelfServeStudioStore } from "@/stores/selfServeStudioStore.js";
|
||||
import { resolveAuditIssueNodeIds } from "./auditIssueNavigation.js";
|
||||
|
||||
const currentRoute = useRoute();
|
||||
@@ -369,88 +367,111 @@ const AUTO_LAYOUT_RUNTIME_STAGE_ORDER = {
|
||||
finish: 2,
|
||||
};
|
||||
|
||||
const studioStore = useSelfServeStudioStore();
|
||||
const {
|
||||
flowNodes,
|
||||
flowEdges,
|
||||
graphPayload,
|
||||
lookups,
|
||||
validation,
|
||||
gatewayWorkspace,
|
||||
versions,
|
||||
activePanel,
|
||||
selectedNodeId,
|
||||
inspectorForm,
|
||||
simulatorForm,
|
||||
simulatorResult,
|
||||
simulatorAnswerOverrides,
|
||||
simulatorShowJson,
|
||||
simulatorDynamicImageHidden,
|
||||
simulatorDynamicImageError,
|
||||
simulatorCompletedTasks,
|
||||
isSimulatorRunning,
|
||||
simulatorRunReason,
|
||||
simulatorRequestSequence,
|
||||
simulatorLastRequestKey,
|
||||
simulatorLastError,
|
||||
isRestoringSimulatorStorage,
|
||||
pathOutcomes,
|
||||
isLoadingPathOutcomes,
|
||||
pathOutcomesError,
|
||||
pathOutcomesRequestKey,
|
||||
selectedPathOutcomeId,
|
||||
selectedPathCaseSignature,
|
||||
selectedPathCaseIndex,
|
||||
pathCaseRunSignatures,
|
||||
pathCaseConfirmingSignature,
|
||||
pathEditorDirty,
|
||||
isHydratingPathEditor,
|
||||
pathEditorTaskTitleOverrides,
|
||||
editingPathEditorTaskTitleKey,
|
||||
editingPathEditorTaskTitle,
|
||||
pathVerificationDynamicImageHidden,
|
||||
pathVerificationDynamicImageError,
|
||||
isPathVerificationDynamicImageLoading,
|
||||
pathEditorQuestionDraftVisible,
|
||||
pathEditorQuestionDraft,
|
||||
pathEditorForm,
|
||||
simulatorCustomerSearchQuery,
|
||||
selectedSimulatorCustomer,
|
||||
showSimulatorCustomerResults,
|
||||
scopeGuideDraft,
|
||||
isScopeGuideManuallyOpen,
|
||||
isSyncingScopeFromRoute,
|
||||
isWritingScopeToRoute,
|
||||
virtualHardwareForm,
|
||||
laneEditorMode,
|
||||
selectedLaneManagementId,
|
||||
laneForm,
|
||||
lanePreviewHidden,
|
||||
inspectorLanePreviewHidden,
|
||||
inspectorTaskDynamicImageHidden,
|
||||
inspectorTaskDynamicImageError,
|
||||
searchTerm,
|
||||
filters,
|
||||
isLoading,
|
||||
isSaving,
|
||||
isUploadingTaskAttachment,
|
||||
deletingTaskAttachmentId,
|
||||
isStudioFullscreen,
|
||||
hasNativeStudioFullscreen,
|
||||
suppressLayoutSave,
|
||||
exportText,
|
||||
importText,
|
||||
draggedTemplateKind,
|
||||
unfilteredNodePositions,
|
||||
} = storeToRefs(studioStore);
|
||||
|
||||
activePanel.value = panelFromRoute() || "flow";
|
||||
studioStore.applyRouteScope(scopeFiltersFromRoute());
|
||||
|
||||
const flowNodes = ref([]);
|
||||
const flowEdges = ref([]);
|
||||
const graphPayload = ref(null);
|
||||
const lookups = ref({});
|
||||
const validation = ref({ valid: true, items: [], errors: [], warnings: [] });
|
||||
const gatewayWorkspace = ref({ gateways: [], relays: [], lanes: [], issues: [], actions: [] });
|
||||
const versions = ref([]);
|
||||
const activePanel = ref(panelFromRoute() || "flow");
|
||||
const selectedNodeId = ref(null);
|
||||
const inspectorForm = ref({});
|
||||
const simulatorForm = ref({
|
||||
lane_id: null,
|
||||
reg: "TEST123",
|
||||
customer_number: null,
|
||||
config_source: "draft",
|
||||
include_hardware: true,
|
||||
hardware_mode: "studio",
|
||||
});
|
||||
const simulatorResult = ref(null);
|
||||
const simulatorAnswerOverrides = ref({});
|
||||
const simulatorShowJson = ref(false);
|
||||
const simulatorDynamicImageHidden = ref(false);
|
||||
const simulatorDynamicImageError = ref("");
|
||||
const simulatorCompletedTasks = ref({});
|
||||
const isSimulatorRunning = ref(false);
|
||||
const simulatorRunReason = ref("");
|
||||
const simulatorRunTimer = ref(null);
|
||||
const simulatorRequestSequence = ref(0);
|
||||
const simulatorLastRequestKey = ref("");
|
||||
const simulatorLastError = ref("");
|
||||
const isRestoringSimulatorStorage = ref(false);
|
||||
const pathOutcomes = ref(null);
|
||||
const isLoadingPathOutcomes = ref(false);
|
||||
const pathOutcomesError = ref("");
|
||||
const pathOutcomesRequestKey = ref("");
|
||||
const pathOutcomesAbortController = ref(null);
|
||||
const selectedPathOutcomeId = ref(null);
|
||||
const selectedPathCaseSignature = ref("");
|
||||
const selectedPathCaseIndex = ref(0);
|
||||
const pathCaseRunSignatures = ref({});
|
||||
const pathCaseConfirmingSignature = ref("");
|
||||
const pathEditorDirty = ref(false);
|
||||
const isHydratingPathEditor = ref(false);
|
||||
const pathEditorTaskTitleOverrides = ref({});
|
||||
const editingPathEditorTaskTitleKey = ref("");
|
||||
const editingPathEditorTaskTitle = ref("");
|
||||
const pathVerificationDynamicImageHidden = ref(false);
|
||||
const pathVerificationDynamicImageError = ref("");
|
||||
const isPathVerificationDynamicImageLoading = ref(false);
|
||||
const pathEditorQuestionDraftVisible = ref(false);
|
||||
const pathEditorQuestionDraft = ref(emptyPathEditorQuestionDraft());
|
||||
const pathEditorForm = ref({
|
||||
path_key: "",
|
||||
previous_path_key: "",
|
||||
answers: {},
|
||||
machine_allowed: true,
|
||||
task: "Start machine",
|
||||
description: "",
|
||||
services: ["MACHINE"],
|
||||
program_button: "",
|
||||
press_reset: false,
|
||||
press_start: true,
|
||||
extra_buttons: [],
|
||||
dynamic_images_vehicle_type: "0",
|
||||
});
|
||||
const simulatorCustomerSearchQuery = ref("");
|
||||
const selectedSimulatorCustomer = ref(null);
|
||||
const showSimulatorCustomerResults = ref(false);
|
||||
const scopeGuideDraft = ref(scopeFiltersFromRoute());
|
||||
const isScopeGuideManuallyOpen = ref(false);
|
||||
const isSyncingScopeFromRoute = ref(false);
|
||||
const isWritingScopeToRoute = ref(false);
|
||||
const virtualHardwareForm = ref({
|
||||
gateway_key: "virtual-main",
|
||||
gateway_label: "Virtual Studio Gateway",
|
||||
relay_id: "",
|
||||
role: "MACHINE",
|
||||
label: "",
|
||||
});
|
||||
const laneEditorMode = ref("edit");
|
||||
const selectedLaneManagementId = ref(null);
|
||||
const laneForm = ref({});
|
||||
const lanePreviewHidden = ref(false);
|
||||
const inspectorLanePreviewHidden = ref(false);
|
||||
const inspectorTaskDynamicImageHidden = ref(false);
|
||||
const inspectorTaskDynamicImageError = ref("");
|
||||
const searchTerm = ref("");
|
||||
const filters = ref({
|
||||
...scopeFiltersFromRoute(),
|
||||
machine_type_id: "",
|
||||
dynamic_image_id: "",
|
||||
});
|
||||
const isLoading = ref(false);
|
||||
const isSaving = ref(false);
|
||||
const isUploadingTaskAttachment = ref(false);
|
||||
const deletingTaskAttachmentId = ref(null);
|
||||
const isStudioFullscreen = ref(false);
|
||||
const hasNativeStudioFullscreen = ref(false);
|
||||
const layoutSaveTimer = ref(null);
|
||||
const suppressLayoutSave = ref(false);
|
||||
const exportText = ref("");
|
||||
const importText = ref("");
|
||||
const draggedTemplateKind = ref(null);
|
||||
const studioRoot = ref(null);
|
||||
const unfilteredNodePositions = ref(null);
|
||||
|
||||
const graphStats = computed(() => {
|
||||
const counts = {};
|
||||
|
||||
@@ -55,12 +55,12 @@ const hasStartFormUserInput = ref(false);
|
||||
const shouldRestoreServerActiveWash = ref(false);
|
||||
const isRestoringServerActiveWash = ref(false);
|
||||
const isSyncingActiveWash = ref(false);
|
||||
const hasConfirmedActiveWashOnServer = 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 SELF_SERVE_FETCH_DEBOUNCE_MS = 300;
|
||||
|
||||
const normalizePositiveInteger = (value: any) => {
|
||||
const parsed = parseInt(String(value ?? ""), 10);
|
||||
@@ -638,24 +638,129 @@ const fetchVehicleTypes = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSelfServeData = async () => {
|
||||
if (isRestoring.value || !nearestDepartment.value) {
|
||||
return;
|
||||
type SelfServeFetchRequest = {
|
||||
key: string;
|
||||
departmentId: number;
|
||||
laneId: number;
|
||||
registration: string;
|
||||
vehicleTypeId: number | null;
|
||||
};
|
||||
|
||||
let selfServeFetchDebounceTimer: ReturnType<typeof window.setTimeout> | null = null;
|
||||
let selfServeFetchResolvers: Array<(value: any) => void> = [];
|
||||
let latestSuccessfulSelfServeFetchKey: string | null = null;
|
||||
let inFlightSelfServeFetchKey: string | null = null;
|
||||
let selfServeFetchSequence = 0;
|
||||
let isSelfServeFetchUnmounted = false;
|
||||
|
||||
const resolvePendingSelfServeFetches = (value: any = null) => {
|
||||
const resolvers = selfServeFetchResolvers;
|
||||
selfServeFetchResolvers = [];
|
||||
resolvers.forEach((resolve) => resolve(value));
|
||||
};
|
||||
|
||||
const clearScheduledSelfServeFetch = (resolveValue: any = null) => {
|
||||
if (selfServeFetchDebounceTimer) {
|
||||
window.clearTimeout(selfServeFetchDebounceTimer);
|
||||
selfServeFetchDebounceTimer = null;
|
||||
}
|
||||
|
||||
const normalizedReg = normalizeLicensePlate(licensePlateInput.value);
|
||||
if (normalizedReg.length < 2) {
|
||||
return;
|
||||
if (selfServeFetchResolvers.length > 0) {
|
||||
resolvePendingSelfServeFetches(resolveValue);
|
||||
}
|
||||
};
|
||||
|
||||
const createSelfServeFetchRequest = (): SelfServeFetchRequest | null => {
|
||||
if (isRestoring.value || !nearestDepartment.value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const departmentId = normalizePositiveInteger(nearestDepartment.value.id);
|
||||
if (!departmentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const registration = normalizeLicensePlate(licensePlateInput.value);
|
||||
if (registration.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const laneId = ensureEffectiveLaneSelection();
|
||||
if (!laneId) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const departmentId = nearestDepartment.value.id;
|
||||
const vehicleTypeId = normalizePositiveInteger(vehicleTypeSelect.value);
|
||||
const key = [departmentId, laneId, registration, vehicleTypeId ?? ""].join("|");
|
||||
|
||||
await fetchSelfServeDataInternal(departmentId, vehicleTypeSelect.value || null, laneId, normalizedReg);
|
||||
return {
|
||||
key,
|
||||
departmentId,
|
||||
laneId,
|
||||
registration,
|
||||
vehicleTypeId,
|
||||
};
|
||||
};
|
||||
|
||||
const executeSelfServeFetch = async (request: SelfServeFetchRequest | null = createSelfServeFetchRequest()) => {
|
||||
if (!request || isSelfServeFetchUnmounted) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (request.key === latestSuccessfulSelfServeFetchKey || request.key === inFlightSelfServeFetchKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestSequence = ++selfServeFetchSequence;
|
||||
inFlightSelfServeFetchKey = request.key;
|
||||
|
||||
try {
|
||||
const result = await fetchSelfServeDataInternal(
|
||||
request.departmentId,
|
||||
request.vehicleTypeId,
|
||||
request.laneId,
|
||||
request.registration
|
||||
);
|
||||
|
||||
if (!isSelfServeFetchUnmounted && requestSequence === selfServeFetchSequence) {
|
||||
latestSuccessfulSelfServeFetchKey = request.key;
|
||||
}
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
if (inFlightSelfServeFetchKey === request.key) {
|
||||
inFlightSelfServeFetchKey = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchSelfServeData = async (options: { immediate?: boolean } = {}) => {
|
||||
const request = createSelfServeFetchRequest();
|
||||
|
||||
if (options.immediate) {
|
||||
clearScheduledSelfServeFetch(null);
|
||||
return executeSelfServeFetch(request);
|
||||
}
|
||||
|
||||
if (!request || isSelfServeFetchUnmounted) {
|
||||
clearScheduledSelfServeFetch(null);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
selfServeFetchResolvers.push(resolve);
|
||||
|
||||
if (selfServeFetchDebounceTimer) {
|
||||
window.clearTimeout(selfServeFetchDebounceTimer);
|
||||
}
|
||||
|
||||
selfServeFetchDebounceTimer = window.setTimeout(async () => {
|
||||
selfServeFetchDebounceTimer = null;
|
||||
const latestRequest = createSelfServeFetchRequest();
|
||||
const result = await executeSelfServeFetch(latestRequest);
|
||||
resolvePendingSelfServeFetches(result);
|
||||
}, SELF_SERVE_FETCH_DEBOUNCE_MS);
|
||||
});
|
||||
};
|
||||
|
||||
const SERVER_ACTIVE_WASH_ENDPOINT = "/modules/self-serve/lane/wash/my-active-wash";
|
||||
@@ -834,7 +939,6 @@ const applyServerActiveWash = async (activeWash: any) => {
|
||||
}
|
||||
|
||||
washInProgress.value = true;
|
||||
hasConfirmedActiveWashOnServer.value = true;
|
||||
washLaneId.value = laneId;
|
||||
radioLaneOption.value = laneId;
|
||||
licensePlateInput.value = reg;
|
||||
@@ -919,7 +1023,6 @@ const clearLocalActiveWashFromServer = () => {
|
||||
completedDurationMs.value = washStartTime.value ? now.value - washStartTime.value : 0;
|
||||
currentStep.value = steps.COMPLETED;
|
||||
washInProgress.value = false;
|
||||
hasConfirmedActiveWashOnServer.value = false;
|
||||
washLaneId.value = null;
|
||||
washStartTime.value = null;
|
||||
shouldRestoreServerActiveWash.value = false;
|
||||
@@ -955,14 +1058,8 @@ const syncActiveWashWithServer = async () => {
|
||||
);
|
||||
const serverStillMatchesCurrentWash =
|
||||
!!details?.in_progress && (!customerNumber || serverCustomerNumber === customerNumber);
|
||||
const serverReportedNoActiveWash = details?.in_progress === false;
|
||||
const shouldPreserveUnconfirmedLocalWash = serverReportedNoActiveWash && !hasConfirmedActiveWashOnServer.value;
|
||||
|
||||
if (!serverStillMatchesCurrentWash || isRecentlyCompletedActiveWash({ details, laneId })) {
|
||||
if (shouldPreserveUnconfirmedLocalWash) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isWithinStartGracePeriod =
|
||||
washStartTime.value && Date.now() - washStartTime.value < WASH_START_SERVER_SYNC_GRACE_MS;
|
||||
if (isWithinStartGracePeriod && !isRecentlyCompletedActiveWash({ details, laneId })) {
|
||||
@@ -973,7 +1070,6 @@ const syncActiveWashWithServer = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
hasConfirmedActiveWashOnServer.value = true;
|
||||
const session = details?.session || {};
|
||||
const vehicle = details?.vehicle || {};
|
||||
const serverReg = normalizeLicensePlate(session?.reg ?? vehicle?.reg);
|
||||
@@ -1033,7 +1129,7 @@ const retrySelfServeData = async () => {
|
||||
|
||||
isSelfServeRetrying.value = true;
|
||||
try {
|
||||
await fetchSelfServeData();
|
||||
await fetchSelfServeData({ immediate: true });
|
||||
} finally {
|
||||
isSelfServeRetrying.value = false;
|
||||
}
|
||||
@@ -1103,7 +1199,7 @@ const onVehicleStepNext = async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await fetchSelfServeData();
|
||||
await fetchSelfServeData({ immediate: true });
|
||||
await nextTick();
|
||||
markQuestionReviewRequired();
|
||||
currentStep.value = visibleQuestions.value.length > 0 ? steps.QUESTIONS : steps.SELECT_LANE;
|
||||
@@ -1187,7 +1283,6 @@ const onConfirmNext = async () => {
|
||||
const onCloseCompleted = () => {
|
||||
stopElapsedTimer();
|
||||
washInProgress.value = false;
|
||||
hasConfirmedActiveWashOnServer.value = false;
|
||||
washLaneId.value = null;
|
||||
washStartTime.value = null;
|
||||
completedDurationMs.value = null;
|
||||
@@ -1227,6 +1322,9 @@ onUnmounted(() => {
|
||||
unregisterBeforeUnload.value();
|
||||
}
|
||||
|
||||
isSelfServeFetchUnmounted = true;
|
||||
clearScheduledSelfServeFetch(null);
|
||||
selfServeFetchSequence += 1;
|
||||
stopAutoRefresh();
|
||||
stopActiveWashRefresh();
|
||||
markDestroying();
|
||||
@@ -1358,15 +1456,10 @@ watch(
|
||||
|
||||
watch([() => washInProgress.value, () => washLaneId.value], ([inProgress]) => {
|
||||
if (inProgress) {
|
||||
if (isRestoring.value) {
|
||||
return;
|
||||
}
|
||||
hasConfirmedActiveWashOnServer.value = true;
|
||||
startActiveWashRefresh();
|
||||
return;
|
||||
}
|
||||
|
||||
hasConfirmedActiveWashOnServer.value = false;
|
||||
stopActiveWashRefresh();
|
||||
});
|
||||
|
||||
@@ -1415,9 +1508,6 @@ watch(
|
||||
if (!restoring) {
|
||||
ensureEffectiveLaneSelection();
|
||||
fetchSelfServeData();
|
||||
if (washInProgress.value) {
|
||||
startActiveWashRefresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1499,6 +1589,7 @@ watch(
|
||||
<b-message
|
||||
v-if="selfServeDataError"
|
||||
type="is-danger"
|
||||
has-icon
|
||||
:closable="false"
|
||||
data-testid="self-serve-runtime-error"
|
||||
>
|
||||
|
||||
@@ -265,11 +265,11 @@ test.describe("Admin daily report", () => {
|
||||
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-24");
|
||||
|
||||
await expect(page.getByTestId("daily-report-page")).toBeVisible();
|
||||
await expect(page.getByTestId("department-weather-hours-multi-day-06:00")).toHaveText("1");
|
||||
await expect(page.getByTestId("department-weather-hours-multi-day-19:00")).toHaveText("19");
|
||||
await expect(page.getByTestId("department-weather-hours-multi-day-20:00")).toHaveText("21");
|
||||
await expect(page.getByTestId("department-weather-productivity-multi-day-06:00")).toHaveClass(/red/);
|
||||
await expect(page.getByTestId("department-weather-productivity-multi-day-08:00")).toHaveClass(/gray/);
|
||||
await expect(page.getByTestId("department-weather-hours-2026-03-23-06:00")).toHaveText("1");
|
||||
await expect(page.getByTestId("department-weather-hours-2026-03-23-19:00")).toHaveText("19");
|
||||
await expect(page.getByTestId("department-weather-hours-2026-03-23-20:00")).toHaveText("21");
|
||||
await expect(page.getByTestId("department-weather-productivity-2026-03-23-06:00")).toHaveClass(/red/);
|
||||
await expect(page.getByTestId("department-weather-productivity-2026-03-24-08:00")).toHaveClass(/gray/);
|
||||
|
||||
await page.getByTestId("department-weather-summary-toggle").click();
|
||||
|
||||
@@ -319,9 +319,9 @@ test.describe("Admin daily report", () => {
|
||||
await page.goto("/admin/1/modules/daily-report?departmentIds=1&dateFrom=2026-03-23&dateTo=2026-03-24");
|
||||
|
||||
await expect(page.getByTestId("daily-report-page")).toBeVisible();
|
||||
await expect(page.getByTestId("department-weather-hours-multi-day-12:00")).toHaveText("1");
|
||||
await expect(page.getByTestId("department-weather-hours-multi-day-13:00")).toHaveText("1");
|
||||
await expect(page.getByTestId("department-weather-hours-multi-day-14:00")).toHaveCount(0);
|
||||
await expect(page.getByTestId("department-weather-hours-2026-03-23-12:00")).toHaveText("1");
|
||||
await expect(page.getByTestId("department-weather-hours-2026-03-24-13:00")).toHaveText("1");
|
||||
await expect(page.getByTestId("department-weather-hours-2026-03-24-14:00")).toHaveCount(0);
|
||||
|
||||
await page.getByTestId("department-weather-summary-toggle").click();
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ const expectedFuldfValues = {
|
||||
|
||||
const expectedSelfWashValues = {
|
||||
"self_wash.lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||
"self_wash.machine_unavailable_for_lane":
|
||||
"Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane.",
|
||||
"self_wash.vehicle_step_missing_customer_number": "Indtast kundenummer for at fortsætte.",
|
||||
"self_wash.vehicle_step_missing_department": "Vent på, at nærmeste afdeling er indlæst, før du fortsætter.",
|
||||
"self_wash.vehicle_step_missing_registration": "Indtast registreringsnummer for at fortsætte.",
|
||||
|
||||
@@ -726,7 +726,7 @@ test.describe("Self-serve wash", () => {
|
||||
expect(startCommandRequest.postDataJSON?.()).toMatchObject({
|
||||
command: "START",
|
||||
customer_number: 12345679,
|
||||
defer_relay_side_effects: false,
|
||||
defer_relay_side_effects: true,
|
||||
});
|
||||
await page.waitForTimeout(250);
|
||||
expect(orderFilters.some((filters) => filters.includes("customer_id:777"))).toBe(false);
|
||||
|
||||
@@ -64,22 +64,6 @@ vi.mock("@/components/pagination/departmentTabs.vue", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/components/page/wrappers/RestrictedPageWrapper.vue", () => ({
|
||||
default: { template: "<div><slot /></div>" },
|
||||
}));
|
||||
|
||||
vi.mock("@/components/page/wrappers/NotFoundFallBackPageWrapper.vue", () => ({
|
||||
default: { template: "<div><slot /></div>" },
|
||||
}));
|
||||
|
||||
vi.mock("@/components/displays/boxes/ElementTabsBox.vue", () => ({
|
||||
default: { template: "<div><slot name='form' /><slot name='history' /></div>" },
|
||||
}));
|
||||
|
||||
vi.mock("@/views/dashboards/departmentDashboard/DepartmentDashboardPageWrapper.vue", () => ({
|
||||
default: { template: "<div><slot /></div>" },
|
||||
}));
|
||||
|
||||
vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue", async () => {
|
||||
const { ref } = await import("vue");
|
||||
|
||||
@@ -120,52 +104,6 @@ vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentD
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentWeather.vue", () => ({
|
||||
default: { template: "<div />" },
|
||||
}));
|
||||
|
||||
vi.mock(
|
||||
"@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportCount.vue",
|
||||
() => ({
|
||||
default: { template: "<div />" },
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportComplaints.vue",
|
||||
() => ({
|
||||
default: { template: "<div />" },
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportHistory.vue",
|
||||
() => ({
|
||||
default: { template: "<div />" },
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportNavigation.vue",
|
||||
() => ({
|
||||
default: { template: "<div />" },
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportProductSales.vue",
|
||||
() => ({
|
||||
default: { template: "<div />" },
|
||||
})
|
||||
);
|
||||
|
||||
vi.mock(
|
||||
"@/views/dashboards/departmentDashboard/modules/daily-report/displays/DepartmentDashboardDailyReportTodayForm.vue",
|
||||
() => ({
|
||||
default: { template: "<div />" },
|
||||
})
|
||||
);
|
||||
|
||||
import DepartmentDailyReport from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReport.vue";
|
||||
|
||||
const flushAll = async () => {
|
||||
|
||||
@@ -278,11 +278,11 @@ describe("DepartmentWeather", () => {
|
||||
|
||||
await flushAll();
|
||||
|
||||
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-06:00").text()).toBe("1");
|
||||
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-19:00").text()).toBe("19");
|
||||
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-20:00").text()).toBe("21");
|
||||
expect(getCellByTestId(wrapper, "department-weather-productivity-multi-day-06:00").classes()).toContain("red");
|
||||
expect(getCellByTestId(wrapper, "department-weather-productivity-multi-day-08:00").classes()).toContain("gray");
|
||||
expect(getCellByTestId(wrapper, "department-weather-hours-2026-03-23-06:00").text()).toBe("1");
|
||||
expect(getCellByTestId(wrapper, "department-weather-hours-2026-03-23-19:00").text()).toBe("19");
|
||||
expect(getCellByTestId(wrapper, "department-weather-hours-2026-03-23-20:00").text()).toBe("21");
|
||||
expect(getCellByTestId(wrapper, "department-weather-productivity-2026-03-23-06:00").classes()).toContain("red");
|
||||
expect(getCellByTestId(wrapper, "department-weather-productivity-2026-03-24-08:00").classes()).toContain("gray");
|
||||
|
||||
await wrapper.get('[data-testid="department-weather-summary-toggle"]').trigger("click");
|
||||
await flushAll();
|
||||
@@ -325,36 +325,11 @@ describe("DepartmentWeather", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("merges matching hours into one column for multi-day hourly mode", async () => {
|
||||
getObjectsMock.mockResolvedValueOnce([
|
||||
makeEntry({ date: "2026-03-23", time: "06:00", weather: "clear", washes: 1, hours: 0.5, status: "unhealthy" }),
|
||||
makeEntry({ date: "2026-03-24", time: "06:00", weather: "rain", washes: 2, hours: 0.6, status: "healthy" }),
|
||||
makeEntry({ date: "2026-03-24", time: "07:00", weather: "rain", washes: 4, hours: 2, status: "healthy" }),
|
||||
]);
|
||||
|
||||
const wrapper = mount(DepartmentWeather, {
|
||||
props: {
|
||||
department_ids: [1],
|
||||
date_from: "2026-03-23",
|
||||
date_to: "2026-03-24",
|
||||
},
|
||||
});
|
||||
|
||||
await flushAll();
|
||||
|
||||
expect(getRenderedHours(wrapper)).toEqual(["06", "07"]);
|
||||
expect(getRowValues(wrapper, 1)).toEqual(["3", "4"]);
|
||||
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-06:00").text()).toBe("2");
|
||||
expect(getCellByTestId(wrapper, "department-weather-productivity-multi-day-06:00").classes()).toContain("green");
|
||||
expect(wrapper.find('[data-testid="department-weather-hours-2026-03-23-06:00"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="department-weather-hours-2026-03-24-06:00"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it("starts in hourly mode and lets users toggle to daily summary in multi-day mode", async () => {
|
||||
getObjectsMock.mockResolvedValueOnce([
|
||||
makeEntry({ date: "2026-03-23", time: "06:00", washes: 1, hours: 1 }),
|
||||
makeEntry({ date: "2026-03-23", time: "07:00", washes: 2, hours: 2 }),
|
||||
makeEntry({ date: "2026-03-24", time: "06:00", washes: 3, hours: 3 }),
|
||||
makeEntry({ date: "2026-03-24", time: "08:00", washes: 3, hours: 3 }),
|
||||
]);
|
||||
|
||||
const wrapper = mount(DepartmentWeather, {
|
||||
@@ -366,7 +341,7 @@ describe("DepartmentWeather", () => {
|
||||
});
|
||||
|
||||
await flushAll();
|
||||
expect(getRenderedHours(wrapper)).toEqual(["06", "07"]);
|
||||
expect(getRenderedHours(wrapper)).toEqual(["06", "07", "08"]);
|
||||
|
||||
await wrapper.get('[data-testid="department-weather-summary-toggle"]').trigger("click");
|
||||
await flushAll();
|
||||
|
||||
@@ -419,6 +419,8 @@ describe("MyWashStart", () => {
|
||||
});
|
||||
|
||||
it("wires child updates back into the self-serve runtime", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const wrapper = mountWithApp(MyWashStart, {
|
||||
global: {
|
||||
stubs: stubComponents,
|
||||
@@ -430,6 +432,7 @@ describe("MyWashStart", () => {
|
||||
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
||||
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
||||
await nextTick();
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
||||
@@ -449,6 +452,34 @@ describe("MyWashStart", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("debounces registration, lane, and vehicle type updates into one self-serve fetch", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const wrapper = mountWithApp(MyWashStart, {
|
||||
global: {
|
||||
stubs: stubComponents,
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
mocks.fetchSelfServeDataInternal.mockClear();
|
||||
|
||||
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
||||
await wrapper.get('[data-testid="emit-lane"]').trigger("click");
|
||||
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
||||
await nextTick();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(299);
|
||||
await flushPromises();
|
||||
expect(mocks.fetchSelfServeDataInternal).not.toHaveBeenCalled();
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
||||
});
|
||||
|
||||
it("hides machine tasks while manual wash is selected", async () => {
|
||||
mocks.activeTasks.value = [
|
||||
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
|
||||
@@ -597,6 +628,8 @@ describe("MyWashStart", () => {
|
||||
});
|
||||
|
||||
it("falls back to available lane when restored lane is stale for the selected department", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
mocks.restoredProgressPayload = {
|
||||
washInProgress: false,
|
||||
washLaneId: null,
|
||||
@@ -619,6 +652,8 @@ describe("MyWashStart", () => {
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(300);
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
||||
|
||||
@@ -11,6 +11,9 @@ describe("Danish self-serve labels", () => {
|
||||
|
||||
it("uses Danish self-serve validation and availability text", () => {
|
||||
expect(da.self_wash.lane_unavailable).toBe("Vaskebanen er ikke tilgængelig");
|
||||
expect(da.self_wash.machine_unavailable_for_lane).toBe(
|
||||
"Maskinvask er ikke tilgængelig for den valgte bane. Vælg manuel vask eller en anden ledig bane."
|
||||
);
|
||||
expect(da.self_wash.vehicle_step_missing_customer_number).toBe("Indtast kundenummer for at fortsætte.");
|
||||
expect(da.self_wash.vehicle_step_missing_department).toBe(
|
||||
"Vent på, at nærmeste afdeling er indlæst, før du fortsætter."
|
||||
|
||||
@@ -78,16 +78,21 @@ describe("SelfServeLaneStep", () => {
|
||||
expect(wrapper.text()).toContain("Tilgængelig");
|
||||
});
|
||||
|
||||
it("keeps machine wash disabled without rendering extra guidance", () => {
|
||||
it("shows machine unavailable guidance for the selected lane", () => {
|
||||
const wrapper = mountLaneStep({
|
||||
isMachineAvailable: () => false,
|
||||
});
|
||||
|
||||
const machineButton = wrapper.get('[data-testid="self-serve-wash-type-machine"]');
|
||||
const washTypeGroup = wrapper.get('[data-testid="self-serve-wash-type-group"]');
|
||||
const washTypeOptions = wrapper.get('[data-testid="self-serve-wash-type-options"]');
|
||||
const guidance = wrapper.get('[data-testid="self-serve-machine-unavailable-guidance"]');
|
||||
expect(machineButton.attributes("disabled")).toBeDefined();
|
||||
expect(machineButton.attributes("title")).toBe("self_wash.unavailable");
|
||||
expect(machineButton.text()).toContain("self_wash.unavailable");
|
||||
expect(wrapper.find('[data-testid="self-serve-machine-unavailable-guidance"]').exists()).toBe(false);
|
||||
expect(wrapper.text()).not.toContain("self_wash.machine_unavailable_for_lane");
|
||||
expect(machineButton.attributes("title")).toBe("self_wash.machine_unavailable_for_lane");
|
||||
expect(guidance.text()).toContain("self_wash.machine_unavailable_for_lane");
|
||||
expect(washTypeOptions.find('[data-testid="self-serve-machine-unavailable-guidance"]').exists()).toBe(false);
|
||||
expect(Array.from(washTypeGroup.element.children).indexOf(washTypeOptions.element)).toBeLessThan(
|
||||
Array.from(washTypeGroup.element.children).indexOf(guidance.element)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("useWashSessionActions property gate commands", () => {
|
||||
command: "START",
|
||||
customer_number: 12345679,
|
||||
license_plate: "AB12345",
|
||||
defer_relay_side_effects: false,
|
||||
defer_relay_side_effects: true,
|
||||
});
|
||||
|
||||
consoleErrorSpy.mockRestore();
|
||||
@@ -286,7 +286,7 @@ describe("useWashSessionActions property gate commands", () => {
|
||||
command: "START",
|
||||
customer_number: 12345679,
|
||||
license_plate: "AB12345",
|
||||
defer_relay_side_effects: false,
|
||||
defer_relay_side_effects: true,
|
||||
});
|
||||
expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", {
|
||||
lane_id: 7,
|
||||
@@ -346,7 +346,7 @@ describe("useWashSessionActions property gate commands", () => {
|
||||
command: "START",
|
||||
customer_number: 12345679,
|
||||
license_plate: "AB12345",
|
||||
defer_relay_side_effects: false,
|
||||
defer_relay_side_effects: true,
|
||||
});
|
||||
expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", {
|
||||
lane_id: 7,
|
||||
|
||||
@@ -10,15 +10,6 @@ export default defineConfig({
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
api: "modern-compiler",
|
||||
quietDeps: true,
|
||||
silenceDeprecations: ["import", "global-builtin", "legacy-js-api", "if-function"],
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["tests/unit/**/*.spec.js"],
|
||||
|
||||
Reference in New Issue
Block a user