Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18ab007f87 |
Generated
+10
-29
@@ -41,7 +41,6 @@
|
|||||||
"dayspan-vuetify": "^0.4.0",
|
"dayspan-vuetify": "^0.4.0",
|
||||||
"flow": "^0.2.3",
|
"flow": "^0.2.3",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"pinia": "^3.0.4",
|
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sass": "^1.81.1",
|
"sass": "^1.81.1",
|
||||||
"sweetalert2": "11.22.4",
|
"sweetalert2": "11.22.4",
|
||||||
@@ -5070,6 +5069,7 @@
|
|||||||
"version": "7.7.9",
|
"version": "7.7.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz",
|
||||||
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
"integrity": "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/devtools-shared": "^7.7.9",
|
"@vue/devtools-shared": "^7.7.9",
|
||||||
@@ -5085,6 +5085,7 @@
|
|||||||
"version": "7.7.9",
|
"version": "7.7.9",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz",
|
||||||
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
"integrity": "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"rfdc": "^1.4.1"
|
"rfdc": "^1.4.1"
|
||||||
@@ -5736,6 +5737,7 @@
|
|||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz",
|
||||||
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
"integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/antfu"
|
"url": "https://github.com/sponsors/antfu"
|
||||||
@@ -6423,6 +6425,7 @@
|
|||||||
"version": "4.0.5",
|
"version": "4.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||||
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-what": "^5.2.0"
|
"is-what": "^5.2.0"
|
||||||
@@ -8985,6 +8988,7 @@
|
|||||||
"version": "5.5.0",
|
"version": "5.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
||||||
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -9567,6 +9571,7 @@
|
|||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mkdirp": {
|
"node_modules/mkdirp": {
|
||||||
@@ -10104,6 +10109,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
|
||||||
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/performance-now": {
|
"node_modules/performance-now": {
|
||||||
@@ -10165,34 +10171,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"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": {
|
"node_modules/pixelmatch": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz",
|
||||||
@@ -10750,6 +10728,7 @@
|
|||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
|
||||||
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/rgbcolor": {
|
"node_modules/rgbcolor": {
|
||||||
@@ -11667,6 +11646,7 @@
|
|||||||
"version": "14.0.1",
|
"version": "14.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
|
||||||
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -11938,6 +11918,7 @@
|
|||||||
"version": "2.2.6",
|
"version": "2.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
||||||
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"copy-anything": "^4"
|
"copy-anything": "^4"
|
||||||
|
|||||||
@@ -86,7 +86,6 @@
|
|||||||
"dayspan-vuetify": "^0.4.0",
|
"dayspan-vuetify": "^0.4.0",
|
||||||
"flow": "^0.2.3",
|
"flow": "^0.2.3",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"pinia": "^3.0.4",
|
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"sass": "^1.81.1",
|
"sass": "^1.81.1",
|
||||||
"sweetalert2": "11.22.4",
|
"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">
|
<script setup lang="ts">
|
||||||
import { computed } from "vue";
|
|
||||||
import { BIcon, BMessage, BRadioButton } from "buefy";
|
import { BIcon, BMessage, BRadioButton } from "buefy";
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
lanes: Array<any>;
|
lanes: Array<any>;
|
||||||
selectedLaneId: number | string | null;
|
selectedLaneId: number | string | null;
|
||||||
washType: string;
|
washType: string;
|
||||||
@@ -15,8 +14,6 @@ const emit = defineEmits<{
|
|||||||
(e: "update:selectedLaneId", value: number): void;
|
(e: "update:selectedLaneId", value: number): void;
|
||||||
(e: "update:washType", value: string): void;
|
(e: "update:washType", value: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isSelectedMachineAvailable = computed(() => props.isMachineAvailable(props.selectedLaneId));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -95,16 +92,16 @@ const isSelectedMachineAvailable = computed(() => props.isMachineAvailable(props
|
|||||||
:model-value="washType"
|
:model-value="washType"
|
||||||
native-value="Machine"
|
native-value="Machine"
|
||||||
type="is-link"
|
type="is-link"
|
||||||
:disabled="!isSelectedMachineAvailable"
|
:disabled="!isMachineAvailable(selectedLaneId)"
|
||||||
:title="!isSelectedMachineAvailable ? $t('self_wash.unavailable') : null"
|
:title="!isMachineAvailable(selectedLaneId) ? $t('self_wash.machine_unavailable_for_lane') : null"
|
||||||
:aria-label="!isSelectedMachineAvailable ? $t('self_wash.unavailable') : null"
|
:aria-label="!isMachineAvailable(selectedLaneId) ? $t('self_wash.machine_unavailable_for_lane') : null"
|
||||||
data-testid="self-serve-wash-type-machine"
|
data-testid="self-serve-wash-type-machine"
|
||||||
@update:model-value="emit('update:washType', 'Machine')"
|
@update:model-value="emit('update:washType', 'Machine')"
|
||||||
@input="emit('update:washType', 'Machine')"
|
@input="emit('update:washType', 'Machine')"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
<span>{{ $t("self_wash.machine") }}<br /></span>
|
<span>{{ $t("self_wash.machine") }}<br /></span>
|
||||||
<span v-if="!isSelectedMachineAvailable">
|
<span v-if="!isMachineAvailable(selectedLaneId)">
|
||||||
<small>
|
<small>
|
||||||
<b-icon icon="times-circle" type="is-danger" pack="fas" class="mr-1" />
|
<b-icon icon="times-circle" type="is-danger" pack="fas" class="mr-1" />
|
||||||
{{ $t("self_wash.unavailable") }}
|
{{ $t("self_wash.unavailable") }}
|
||||||
@@ -120,6 +117,14 @@ const isSelectedMachineAvailable = computed(() => props.isMachineAvailable(props
|
|||||||
</b-radio-button>
|
</b-radio-button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { BIcon } from "buefy";
|
import { ref, watch } from "vue";
|
||||||
import SelfServeDynamicImageFrame from "@/components/displays/selfServe/SelfServeDynamicImageFrame.vue";
|
import { BIcon, BSkeleton } from "buefy";
|
||||||
import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue";
|
import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -18,6 +18,12 @@ const emit = defineEmits<{
|
|||||||
(e: "clear-dynamic-image"): void;
|
(e: "clear-dynamic-image"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const isDynamicImageLoading = ref(false);
|
||||||
|
|
||||||
|
watch(() => props.dynamicImageUrl, (dynamicImageUrl) => {
|
||||||
|
isDynamicImageLoading.value = !!dynamicImageUrl;
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const emitToggleTask = (taskId: number, value: boolean) => {
|
const emitToggleTask = (taskId: number, value: boolean) => {
|
||||||
emit("toggle-task", taskId, value);
|
emit("toggle-task", taskId, value);
|
||||||
};
|
};
|
||||||
@@ -26,7 +32,12 @@ const emitDownloadAttachment = (taskId: number, attachmentId: number | undefined
|
|||||||
emit("download-attachment", taskId, attachmentId, attachment);
|
emit("download-attachment", taskId, attachmentId, attachment);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDynamicImageLoad = () => {
|
||||||
|
isDynamicImageLoading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const onDynamicImageError = () => {
|
const onDynamicImageError = () => {
|
||||||
|
isDynamicImageLoading.value = false;
|
||||||
emit("clear-dynamic-image");
|
emit("clear-dynamic-image");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -47,11 +58,30 @@ const onDynamicImageError = () => {
|
|||||||
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
|
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SelfServeDynamicImageFrame
|
<div
|
||||||
:src="dynamicImageUrl"
|
v-if="dynamicImageUrl"
|
||||||
alt="Machine status"
|
class="self-serve-dynamic-image-frame mb-4"
|
||||||
@error="onDynamicImageError"
|
: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">
|
<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-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>
|
<h1 class="title has-text-centered mb-2" v-else>{{ $t("self_wash.questions_answered") }}</h1>
|
||||||
@@ -65,3 +95,41 @@ const onDynamicImageError = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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>
|
<td>
|
||||||
<div class="buttons is-justify-content-flex-end action-buttons">
|
<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>
|
<template #actions>
|
||||||
<ActionSettingsWheelItem
|
<ActionSettingsWheelItem
|
||||||
v-if="canEditPermissions() && subuser.grant_id"
|
v-if="canEditPermissions() && subuser.grant_id"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { createPinia, defineStore, getActivePinia, setActivePinia, storeToRefs } from "pinia";
|
|
||||||
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
import { SessionUser } from "@/components/session/token/SessionUser.vue";
|
||||||
import { safeAttachmentDownloadLink } from "@/services/attachmentDownloadLinks.js";
|
import { safeAttachmentDownloadLink } from "@/services/attachmentDownloadLinks.js";
|
||||||
|
|
||||||
@@ -272,26 +271,7 @@ const mergeByNumericId = (existingItems, incomingItems) => {
|
|||||||
return merged;
|
return merged;
|
||||||
};
|
};
|
||||||
|
|
||||||
let selfServeLogicStoreInstance = 0;
|
export function useSelfServeLogic() {
|
||||||
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, () => {
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const requestError = ref(null);
|
const requestError = ref(null);
|
||||||
const preview = ref(null);
|
const preview = ref(null);
|
||||||
@@ -317,6 +297,30 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
|||||||
const summaryVisibleQuestionIds = ref([]);
|
const summaryVisibleQuestionIds = ref([]);
|
||||||
const summaryQuestionOrder = ref({});
|
const summaryQuestionOrder = ref({});
|
||||||
const latestFetchRequestId = ref(0);
|
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 = () => {
|
const beginFetchRequest = () => {
|
||||||
latestFetchRequestId.value += 1;
|
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();
|
const requestId = beginFetchRequest();
|
||||||
|
|
||||||
if (!laneId || !reg || reg.trim().length < 2) {
|
if (!laneId || !reg || reg.trim().length < 2) {
|
||||||
|
latestSuccessfulFetchKey.value = null;
|
||||||
|
inFlightFetchKey.value = null;
|
||||||
preview.value = null;
|
preview.value = null;
|
||||||
summary.value = null;
|
summary.value = null;
|
||||||
session.value = null;
|
session.value = null;
|
||||||
@@ -626,6 +637,7 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
|||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
requestError.value = null;
|
requestError.value = null;
|
||||||
|
inFlightFetchKey.value = fetchKey;
|
||||||
try {
|
try {
|
||||||
const normalizedReg = reg.trim().toUpperCase();
|
const normalizedReg = reg.trim().toUpperCase();
|
||||||
const normalizedVehicleTypeId = parseInt(_vehicleTypeId);
|
const normalizedVehicleTypeId = parseInt(_vehicleTypeId);
|
||||||
@@ -707,6 +719,10 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
|||||||
setSummaryVisibleQuestions(questions.value);
|
setSummaryVisibleQuestions(questions.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFetchRequestActive(requestId)) {
|
||||||
|
latestSuccessfulFetchKey.value = fetchKey;
|
||||||
|
}
|
||||||
|
|
||||||
return previewData;
|
return previewData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching self-serve preview:", error);
|
console.error("Error fetching self-serve preview:", error);
|
||||||
@@ -715,6 +731,9 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
} finally {
|
} finally {
|
||||||
|
if (inFlightFetchKey.value === fetchKey) {
|
||||||
|
inFlightFetchKey.value = null;
|
||||||
|
}
|
||||||
if (isFetchRequestActive(requestId)) {
|
if (isFetchRequestActive(requestId)) {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -769,7 +788,7 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
|||||||
updateResolvedVehicleTypeId(responseSummary, responseSummary?.session);
|
updateResolvedVehicleTypeId(responseSummary, responseSummary?.session);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg);
|
await fetchSelfServeData(departmentId, refreshVehicleTypeId, laneId, normalizedReg, { force: true });
|
||||||
answers.value = {
|
answers.value = {
|
||||||
...answers.value,
|
...answers.value,
|
||||||
[parseInt(questionId)]: value,
|
[parseInt(questionId)]: value,
|
||||||
@@ -834,7 +853,7 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
|||||||
? normalizedRefreshVehicleTypeId
|
? normalizedRefreshVehicleTypeId
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg);
|
await fetchSelfServeData(departmentId, refreshVehicleTypeId, parseInt(laneId), normalizedReg, { force: true });
|
||||||
|
|
||||||
return { deletedCount: conditionIdsToDelete.length };
|
return { deletedCount: conditionIdsToDelete.length };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1095,50 +1114,4 @@ const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
|||||||
answerQuestion,
|
answerQuestion,
|
||||||
removeAnswer,
|
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", {
|
const startResponse = await executeSelfServeCommand(laneId, "START", {
|
||||||
customer_number: parseInt(customerNumber),
|
customer_number: parseInt(customerNumber),
|
||||||
license_plate: licensePlate.trim().toUpperCase(),
|
license_plate: licensePlate.trim().toUpperCase(),
|
||||||
defer_relay_side_effects: false,
|
defer_relay_side_effects: true,
|
||||||
});
|
});
|
||||||
if (!startResponse) {
|
if (!startResponse) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -4595,6 +4595,7 @@
|
|||||||
"loading_vehicle_types": "@.capitalize:{'words.generated.indlæser'} @:{'words.generated.tilgængelige'} @:{'words.generated.køretøjstyper'}...",
|
"loading_vehicle_types": "@.capitalize:{'words.generated.indlæser'} @:{'words.generated.tilgængelige'} @:{'words.generated.køretøjstyper'}...",
|
||||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||||
"machine": "@.capitalize:{'words.generated.maskine'}",
|
"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'}",
|
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||||
"occupied": "Optaget",
|
"occupied": "Optaget",
|
||||||
"open_property_access_gate": "@:{'words.generated.abn'} adgangsport",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'words.generated.verf'}?@:{'words.generated.gbare'} @:{'words.generated.fahrzeugtypen'} @:{'words.generated.werden'} @:{'words.generated.geladen'}...",
|
||||||
"lane_unavailable": "Waschspur nicht verfugbar",
|
"lane_unavailable": "Waschspur nicht verfugbar",
|
||||||
"machine": "@:{'words.generated.maschine'}",
|
"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'}",
|
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||||
"occupied": "Belegt",
|
"occupied": "Belegt",
|
||||||
"open_property_access_gate": "Grundstueckszugangstor @:{'words.generated.oeffnen'}",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'words.generated.loading'} @:{'words.generated.available'} @:{'words.generated.vehicle'} @:{'words.generated.types'}...",
|
||||||
"lane_unavailable": "Lane unavailable",
|
"lane_unavailable": "Lane unavailable",
|
||||||
"machine": "@.capitalize:{'words.generated.machine'}",
|
"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'}",
|
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||||
"occupied": "Occupied",
|
"occupied": "Occupied",
|
||||||
"open_property_access_gate": "@.capitalize:{'words.generated.open'} @:{'words.generated.property'} @:{'words.generated.access'} @:{'words.generated.gate'}",
|
"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'}",
|
"loading_vehicle_types": "@:{'templates.generated.compat.self_wash.loading_vehicle_types'}",
|
||||||
"lane_unavailable": "@:{'templates.generated.compat.self_wash.lane_unavailable'}",
|
"lane_unavailable": "@:{'templates.generated.compat.self_wash.lane_unavailable'}",
|
||||||
"machine": "@:{'templates.generated.compat.self_wash.machine'}",
|
"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_departments_found": "@:{'templates.generated.compat.self_wash.no_departments_found'}",
|
||||||
"no_vehicles_found": "@:vehicles.no_vehicles",
|
"no_vehicles_found": "@:vehicles.no_vehicles",
|
||||||
"occupied": "@:{'templates.generated.compat.self_wash.occupied'}",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'words.generated.laster'} @:{'words.generated.inn'} @:{'words.generated.tilgjengelige'} @:{'words.generated.kjøretøytyper'}...",
|
||||||
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
||||||
"machine": "@.capitalize:{'words.generated.maskin'}",
|
"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'}",
|
"no_departments_found": "@:{'templates.generated.compat.departments.no_departments'}",
|
||||||
"occupied": "Okkupert",
|
"occupied": "Okkupert",
|
||||||
"open_property_access_gate": "@:{'words.generated.aapne'} @:{'words.generated.eiendommens'} inngangsport",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'words.generated.laser'} @:{'words.generated.in'} @:{'words.generated.tillgangliga'} @:{'words.generated.fordonstyper'}...",
|
||||||
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
||||||
"machine": "@.capitalize:{'words.generated.machine'}",
|
"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'}",
|
"no_departments_found": "@.capitalize:{'words.generated.inga'} @:{'words.generated.avdelningar'} @:{'words.generated.hittades'}",
|
||||||
"occupied": "Upptagen",
|
"occupied": "Upptagen",
|
||||||
"open_property_access_gate": "@:{'words.generated.oeppna'} @:{'words.generated.fastighetens'} ingangsgrind",
|
"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...",
|
"loading_vehicle_types": "Indlæser tilgængelige køretøjstyper...",
|
||||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||||
"machine": "Maskine",
|
"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_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_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.",
|
"vehicle_step_missing_registration": "Indtast registreringsnummer for at fortsætte.",
|
||||||
|
|||||||
@@ -3663,6 +3663,7 @@
|
|||||||
"loading_vehicle_types": "Verf?gbare Fahrzeugtypen werden geladen...",
|
"loading_vehicle_types": "Verf?gbare Fahrzeugtypen werden geladen...",
|
||||||
"lane_unavailable": "Waschspur nicht verfugbar",
|
"lane_unavailable": "Waschspur nicht verfugbar",
|
||||||
"machine": "Maschine",
|
"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_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_department": "Warten Sie, bis die nachste Abteilung geladen ist, bevor Sie fortfahren.",
|
||||||
"vehicle_step_missing_registration": "Geben Sie das Kennzeichen ein, um fortzufahren.",
|
"vehicle_step_missing_registration": "Geben Sie das Kennzeichen ein, um fortzufahren.",
|
||||||
|
|||||||
@@ -3662,6 +3662,7 @@
|
|||||||
"loading_vehicle_types": "Loading available vehicle types...",
|
"loading_vehicle_types": "Loading available vehicle types...",
|
||||||
"lane_unavailable": "Lane unavailable",
|
"lane_unavailable": "Lane unavailable",
|
||||||
"machine": "Machine",
|
"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_customer_number": "Enter your customer number to continue.",
|
||||||
"vehicle_step_missing_department": "Wait for the nearest department to load before continuing.",
|
"vehicle_step_missing_department": "Wait for the nearest department to load before continuing.",
|
||||||
"vehicle_step_missing_registration": "Enter your registration number to continue.",
|
"vehicle_step_missing_registration": "Enter your registration number to continue.",
|
||||||
|
|||||||
@@ -3663,6 +3663,7 @@
|
|||||||
"loading_vehicle_types": "Laster inn tilgjengelige kjøretøytyper...",
|
"loading_vehicle_types": "Laster inn tilgjengelige kjøretøytyper...",
|
||||||
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
||||||
"machine": "Maskin",
|
"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_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_department": "Vent til naermeste avdeling er lastet inn for du fortsetter.",
|
||||||
"vehicle_step_missing_registration": "Skriv inn registreringsnummer for a fortsette.",
|
"vehicle_step_missing_registration": "Skriv inn registreringsnummer for a fortsette.",
|
||||||
|
|||||||
@@ -3663,6 +3663,7 @@
|
|||||||
"loading_vehicle_types": "Läser in tillgängliga fordonstyper...",
|
"loading_vehicle_types": "Läser in tillgängliga fordonstyper...",
|
||||||
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
||||||
"machine": "Machine",
|
"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_customer_number": "Ange kundnummer for att fortsatta.",
|
||||||
"vehicle_step_missing_department": "Vanta tills narmaste avdelning har laddats innan du fortsatter.",
|
"vehicle_step_missing_department": "Vanta tills narmaste avdelning har laddats innan du fortsatter.",
|
||||||
"vehicle_step_missing_registration": "Ange registreringsnummer for att fortsatta.",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'terms.glossary.indlæser'} @:{'terms.glossary.tilgængelige'} @:{'terms.glossary.køretøjstyper'}...",
|
||||||
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
"lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
||||||
"machine": "@.capitalize:{'terms.glossary.maskine'}",
|
"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'}",
|
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||||
"occupied": "Optaget",
|
"occupied": "Optaget",
|
||||||
"open_property_access_gate": "@:{'terms.glossary.abn'} adgangsport",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'terms.glossary.verf'}?@:{'terms.glossary.gbare'} @:{'terms.glossary.fahrzeugtypen'} @:{'terms.glossary.werden'} @:{'terms.glossary.geladen'}...",
|
||||||
"lane_unavailable": "Waschspur nicht verfugbar",
|
"lane_unavailable": "Waschspur nicht verfugbar",
|
||||||
"machine": "@:{'terms.glossary.maschine'}",
|
"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'}",
|
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||||
"occupied": "Belegt",
|
"occupied": "Belegt",
|
||||||
"open_property_access_gate": "Grundstueckszugangstor @:{'terms.glossary.oeffnen'}",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'terms.glossary.loading'} @:{'terms.glossary.available'} @:{'terms.glossary.vehicle'} @:{'terms.glossary.types'}...",
|
||||||
"lane_unavailable": "Lane unavailable",
|
"lane_unavailable": "Lane unavailable",
|
||||||
"machine": "@.capitalize:{'terms.glossary.machine'}",
|
"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'}",
|
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||||
"occupied": "Occupied",
|
"occupied": "Occupied",
|
||||||
"open_property_access_gate": "@.capitalize:{'terms.glossary.open'} @:{'terms.glossary.property'} @:{'terms.glossary.access'} @:{'terms.glossary.gate'}",
|
"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'}",
|
"loading_vehicle_types": "@:{'phrases.compat.self_wash.loading_vehicle_types'}",
|
||||||
"lane_unavailable": "@:{'phrases.compat.self_wash.lane_unavailable'}",
|
"lane_unavailable": "@:{'phrases.compat.self_wash.lane_unavailable'}",
|
||||||
"machine": "@:{'phrases.compat.self_wash.machine'}",
|
"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_departments_found": "@:{'phrases.compat.self_wash.no_departments_found'}",
|
||||||
"no_vehicles_found": "@:vehicles.no_vehicles",
|
"no_vehicles_found": "@:vehicles.no_vehicles",
|
||||||
"occupied": "@:{'phrases.compat.self_wash.occupied'}",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'terms.glossary.laster'} @:{'terms.glossary.inn'} @:{'terms.glossary.tilgjengelige'} @:{'terms.glossary.kjøretøytyper'}...",
|
||||||
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
"lane_unavailable": "Vaskebanen er ikke tilgjengelig",
|
||||||
"machine": "@.capitalize:{'terms.glossary.maskin'}",
|
"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'}",
|
"no_departments_found": "@:{'phrases.compat.departments.no_departments'}",
|
||||||
"occupied": "Okkupert",
|
"occupied": "Okkupert",
|
||||||
"open_property_access_gate": "@:{'terms.glossary.aapne'} @:{'terms.glossary.eiendommens'} inngangsport",
|
"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'}...",
|
"loading_vehicle_types": "@.capitalize:{'terms.glossary.laser'} @:{'terms.glossary.in'} @:{'terms.glossary.tillgangliga'} @:{'terms.glossary.fordonstyper'}...",
|
||||||
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
"lane_unavailable": "Tvattbanan ar inte tillganglig",
|
||||||
"machine": "@.capitalize:{'terms.glossary.machine'}",
|
"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'}",
|
"no_departments_found": "@.capitalize:{'terms.glossary.inga'} @:{'terms.glossary.avdelningar'} @:{'terms.glossary.hittades'}",
|
||||||
"occupied": "Upptagen",
|
"occupied": "Upptagen",
|
||||||
"open_property_access_gate": "@:{'terms.glossary.oeppna'} @:{'terms.glossary.fastighetens'} ingangsgrind",
|
"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 store from './store/user.vue'
|
||||||
import { createHead } from "@vueuse/head";
|
import { createHead } from "@vueuse/head";
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { createPinia } from 'pinia'
|
|
||||||
import Router from "@/router";
|
import Router from "@/router";
|
||||||
import App from "@/App.vue";
|
import App from "@/App.vue";
|
||||||
import Buefy from 'buefy'
|
import Buefy from 'buefy'
|
||||||
@@ -109,7 +108,6 @@ applyMiddleware(Router)
|
|||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
.use(Router)
|
.use(Router)
|
||||||
.use(createHead())
|
.use(createHead())
|
||||||
.use(createPinia())
|
|
||||||
.use(store)
|
.use(store)
|
||||||
.use(ToastPlugin)
|
.use(ToastPlugin)
|
||||||
.use(Buefy)
|
.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;
|
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 summarizeEntriesByDay = (entries) => {
|
||||||
const groupedByDate = new Map();
|
const groupedByDate = new Map();
|
||||||
|
|
||||||
@@ -139,48 +120,14 @@ const summarizeEntriesByDay = (entries) => {
|
|||||||
time: "00:00",
|
time: "00:00",
|
||||||
current: dayEntries.some((entry) => entry?.current === true),
|
current: dayEntries.some((entry) => entry?.current === true),
|
||||||
weather: getDominantWeather(dayEntries),
|
weather: getDominantWeather(dayEntries),
|
||||||
washes: formatMergedNumber(washes),
|
washes: Number.isInteger(washes) ? washes : Number(washes.toFixed(2)),
|
||||||
hours: Number(hours.toFixed(2)),
|
hours: Number(hours.toFixed(2)),
|
||||||
/**
|
/**
|
||||||
* Below 1.0 cars/workhour = unhealthy (red)
|
* Below 1.0 cars/workhour = unhealthy (red)
|
||||||
* 1.0-1.3 cars/workhour = degraded (yellow)
|
* 1.0-1.3 cars/workhour = degraded (yellow)
|
||||||
* Above 1.3 cars/workhour = healthy (green)
|
* Above 1.3 cars/workhour = healthy (green)
|
||||||
*/
|
*/
|
||||||
status: getProductivityStatus(washes, hours),
|
status: hours > 0 ? (washes / hours >= 1.3 ? "healthy" : washes / hours >= 1.0 ? "degraded" : "unhealthy") : "unknown",
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -189,10 +136,6 @@ const hourlyDisplayedState = computed(() => {
|
|||||||
return state.value.filter((entry) => hasOperationalData(entry));
|
return state.value.filter((entry) => hasOperationalData(entry));
|
||||||
});
|
});
|
||||||
|
|
||||||
const hourlyMergedDisplayedState = computed(() => {
|
|
||||||
return summarizeEntriesByHourOfDay(hourlyDisplayedState.value).filter((entry) => hasOperationalData(entry));
|
|
||||||
});
|
|
||||||
|
|
||||||
const dailyDisplayedState = computed(() => {
|
const dailyDisplayedState = computed(() => {
|
||||||
return summarizeEntriesByDay(state.value).filter((entry) => hasOperationalData(entry));
|
return summarizeEntriesByDay(state.value).filter((entry) => hasOperationalData(entry));
|
||||||
});
|
});
|
||||||
@@ -201,9 +144,6 @@ const displayedState = computed(() => {
|
|||||||
if (summarizeDaily.value && isMultiDaySelection.value) {
|
if (summarizeDaily.value && isMultiDaySelection.value) {
|
||||||
return dailyDisplayedState.value;
|
return dailyDisplayedState.value;
|
||||||
}
|
}
|
||||||
if (isMultiDaySelection.value) {
|
|
||||||
return hourlyMergedDisplayedState.value;
|
|
||||||
}
|
|
||||||
return hourlyDisplayedState.value;
|
return hourlyDisplayedState.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,9 +167,7 @@ const getEntryKey = (entry, index) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getEntryTestId = (prefix, entry) => {
|
const getEntryTestId = (prefix, entry) => {
|
||||||
const date = typeof entry?.test_id_date === "string"
|
const date = isDateString(entry?.date) ? entry.date : "no-date";
|
||||||
? entry.test_id_date
|
|
||||||
: isDateString(entry?.date) ? entry.date : "no-date";
|
|
||||||
const time = typeof entry?.time === "string" && entry.time !== "" ? entry.time : "no-time";
|
const time = typeof entry?.time === "string" && entry.time !== "" ? entry.time : "no-time";
|
||||||
return `department-weather-${prefix}-${date}-${time}`;
|
return `department-weather-${prefix}-${date}-${time}`;
|
||||||
};
|
};
|
||||||
@@ -239,8 +177,9 @@ const getHourDetailsSlotKey = (entry) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedTime = getNormalizedTime(entry);
|
const timeValue = typeof entry?.time === "string" ? entry.time : "";
|
||||||
if (!normalizedTime) {
|
const normalizedTime = timeValue.slice(0, 5);
|
||||||
|
if (!/^\d{2}:\d{2}$/.test(normalizedTime)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+101
-80
@@ -1,6 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import { storeToRefs } from "pinia";
|
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { Handle, Position, VueFlow, useVueFlow } from "@vue-flow/core";
|
import { Handle, Position, VueFlow, useVueFlow } from "@vue-flow/core";
|
||||||
import "@vue-flow/core/dist/style.css";
|
import "@vue-flow/core/dist/style.css";
|
||||||
@@ -35,7 +34,6 @@ import {
|
|||||||
formatSelfServeTaskButtons,
|
formatSelfServeTaskButtons,
|
||||||
normalizeSelfServeTaskButtons,
|
normalizeSelfServeTaskButtons,
|
||||||
} from "@/components/session/token/SessionUser/Objects/selfServeTaskButtons.js";
|
} from "@/components/session/token/SessionUser/Objects/selfServeTaskButtons.js";
|
||||||
import { useSelfServeStudioStore } from "@/stores/selfServeStudioStore.js";
|
|
||||||
import { resolveAuditIssueNodeIds } from "./auditIssueNavigation.js";
|
import { resolveAuditIssueNodeIds } from "./auditIssueNavigation.js";
|
||||||
|
|
||||||
const currentRoute = useRoute();
|
const currentRoute = useRoute();
|
||||||
@@ -369,88 +367,111 @@ const AUTO_LAYOUT_RUNTIME_STAGE_ORDER = {
|
|||||||
finish: 2,
|
finish: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const studioStore = useSelfServeStudioStore();
|
const flowNodes = ref([]);
|
||||||
const {
|
const flowEdges = ref([]);
|
||||||
flowNodes,
|
const graphPayload = ref(null);
|
||||||
flowEdges,
|
const lookups = ref({});
|
||||||
graphPayload,
|
const validation = ref({ valid: true, items: [], errors: [], warnings: [] });
|
||||||
lookups,
|
const gatewayWorkspace = ref({ gateways: [], relays: [], lanes: [], issues: [], actions: [] });
|
||||||
validation,
|
const versions = ref([]);
|
||||||
gatewayWorkspace,
|
const activePanel = ref(panelFromRoute() || "flow");
|
||||||
versions,
|
const selectedNodeId = ref(null);
|
||||||
activePanel,
|
const inspectorForm = ref({});
|
||||||
selectedNodeId,
|
const simulatorForm = ref({
|
||||||
inspectorForm,
|
lane_id: null,
|
||||||
simulatorForm,
|
reg: "TEST123",
|
||||||
simulatorResult,
|
customer_number: null,
|
||||||
simulatorAnswerOverrides,
|
config_source: "draft",
|
||||||
simulatorShowJson,
|
include_hardware: true,
|
||||||
simulatorDynamicImageHidden,
|
hardware_mode: "studio",
|
||||||
simulatorDynamicImageError,
|
});
|
||||||
simulatorCompletedTasks,
|
const simulatorResult = ref(null);
|
||||||
isSimulatorRunning,
|
const simulatorAnswerOverrides = ref({});
|
||||||
simulatorRunReason,
|
const simulatorShowJson = ref(false);
|
||||||
simulatorRequestSequence,
|
const simulatorDynamicImageHidden = ref(false);
|
||||||
simulatorLastRequestKey,
|
const simulatorDynamicImageError = ref("");
|
||||||
simulatorLastError,
|
const simulatorCompletedTasks = ref({});
|
||||||
isRestoringSimulatorStorage,
|
const isSimulatorRunning = ref(false);
|
||||||
pathOutcomes,
|
const simulatorRunReason = ref("");
|
||||||
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 simulatorRunTimer = ref(null);
|
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 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 layoutSaveTimer = ref(null);
|
||||||
|
const suppressLayoutSave = ref(false);
|
||||||
|
const exportText = ref("");
|
||||||
|
const importText = ref("");
|
||||||
|
const draggedTemplateKind = ref(null);
|
||||||
const studioRoot = ref(null);
|
const studioRoot = ref(null);
|
||||||
|
const unfilteredNodePositions = ref(null);
|
||||||
|
|
||||||
const graphStats = computed(() => {
|
const graphStats = computed(() => {
|
||||||
const counts = {};
|
const counts = {};
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ const hasStartFormUserInput = ref(false);
|
|||||||
const shouldRestoreServerActiveWash = ref(false);
|
const shouldRestoreServerActiveWash = ref(false);
|
||||||
const isRestoringServerActiveWash = ref(false);
|
const isRestoringServerActiveWash = ref(false);
|
||||||
const isSyncingActiveWash = ref(false);
|
const isSyncingActiveWash = ref(false);
|
||||||
const hasConfirmedActiveWashOnServer = ref(false);
|
|
||||||
const MIN_FINISHING_SCREEN_MS = 250;
|
const MIN_FINISHING_SCREEN_MS = 250;
|
||||||
const RECENT_COMPLETED_WASH_KEY = "mywash_recent_completed_v1";
|
const RECENT_COMPLETED_WASH_KEY = "mywash_recent_completed_v1";
|
||||||
const RECENT_COMPLETED_WASH_SUPPRESSION_MS = 10 * 60 * 1000;
|
const RECENT_COMPLETED_WASH_SUPPRESSION_MS = 10 * 60 * 1000;
|
||||||
const ACTIVE_WASH_REFRESH_MS = 5 * 1000;
|
const ACTIVE_WASH_REFRESH_MS = 5 * 1000;
|
||||||
const WASH_START_SERVER_SYNC_GRACE_MS = 10 * 1000;
|
const WASH_START_SERVER_SYNC_GRACE_MS = 10 * 1000;
|
||||||
|
const SELF_SERVE_FETCH_DEBOUNCE_MS = 300;
|
||||||
|
|
||||||
const normalizePositiveInteger = (value: any) => {
|
const normalizePositiveInteger = (value: any) => {
|
||||||
const parsed = parseInt(String(value ?? ""), 10);
|
const parsed = parseInt(String(value ?? ""), 10);
|
||||||
@@ -638,24 +638,129 @@ const fetchVehicleTypes = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchSelfServeData = async () => {
|
type SelfServeFetchRequest = {
|
||||||
if (isRestoring.value || !nearestDepartment.value) {
|
key: string;
|
||||||
return;
|
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 (selfServeFetchResolvers.length > 0) {
|
||||||
if (normalizedReg.length < 2) {
|
resolvePendingSelfServeFetches(resolveValue);
|
||||||
return;
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
const laneId = ensureEffectiveLaneSelection();
|
||||||
if (!laneId) {
|
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";
|
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;
|
washInProgress.value = true;
|
||||||
hasConfirmedActiveWashOnServer.value = true;
|
|
||||||
washLaneId.value = laneId;
|
washLaneId.value = laneId;
|
||||||
radioLaneOption.value = laneId;
|
radioLaneOption.value = laneId;
|
||||||
licensePlateInput.value = reg;
|
licensePlateInput.value = reg;
|
||||||
@@ -919,7 +1023,6 @@ const clearLocalActiveWashFromServer = () => {
|
|||||||
completedDurationMs.value = washStartTime.value ? now.value - washStartTime.value : 0;
|
completedDurationMs.value = washStartTime.value ? now.value - washStartTime.value : 0;
|
||||||
currentStep.value = steps.COMPLETED;
|
currentStep.value = steps.COMPLETED;
|
||||||
washInProgress.value = false;
|
washInProgress.value = false;
|
||||||
hasConfirmedActiveWashOnServer.value = false;
|
|
||||||
washLaneId.value = null;
|
washLaneId.value = null;
|
||||||
washStartTime.value = null;
|
washStartTime.value = null;
|
||||||
shouldRestoreServerActiveWash.value = false;
|
shouldRestoreServerActiveWash.value = false;
|
||||||
@@ -955,14 +1058,8 @@ const syncActiveWashWithServer = async () => {
|
|||||||
);
|
);
|
||||||
const serverStillMatchesCurrentWash =
|
const serverStillMatchesCurrentWash =
|
||||||
!!details?.in_progress && (!customerNumber || serverCustomerNumber === customerNumber);
|
!!details?.in_progress && (!customerNumber || serverCustomerNumber === customerNumber);
|
||||||
const serverReportedNoActiveWash = details?.in_progress === false;
|
|
||||||
const shouldPreserveUnconfirmedLocalWash = serverReportedNoActiveWash && !hasConfirmedActiveWashOnServer.value;
|
|
||||||
|
|
||||||
if (!serverStillMatchesCurrentWash || isRecentlyCompletedActiveWash({ details, laneId })) {
|
if (!serverStillMatchesCurrentWash || isRecentlyCompletedActiveWash({ details, laneId })) {
|
||||||
if (shouldPreserveUnconfirmedLocalWash) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isWithinStartGracePeriod =
|
const isWithinStartGracePeriod =
|
||||||
washStartTime.value && Date.now() - washStartTime.value < WASH_START_SERVER_SYNC_GRACE_MS;
|
washStartTime.value && Date.now() - washStartTime.value < WASH_START_SERVER_SYNC_GRACE_MS;
|
||||||
if (isWithinStartGracePeriod && !isRecentlyCompletedActiveWash({ details, laneId })) {
|
if (isWithinStartGracePeriod && !isRecentlyCompletedActiveWash({ details, laneId })) {
|
||||||
@@ -973,7 +1070,6 @@ const syncActiveWashWithServer = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasConfirmedActiveWashOnServer.value = true;
|
|
||||||
const session = details?.session || {};
|
const session = details?.session || {};
|
||||||
const vehicle = details?.vehicle || {};
|
const vehicle = details?.vehicle || {};
|
||||||
const serverReg = normalizeLicensePlate(session?.reg ?? vehicle?.reg);
|
const serverReg = normalizeLicensePlate(session?.reg ?? vehicle?.reg);
|
||||||
@@ -1033,7 +1129,7 @@ const retrySelfServeData = async () => {
|
|||||||
|
|
||||||
isSelfServeRetrying.value = true;
|
isSelfServeRetrying.value = true;
|
||||||
try {
|
try {
|
||||||
await fetchSelfServeData();
|
await fetchSelfServeData({ immediate: true });
|
||||||
} finally {
|
} finally {
|
||||||
isSelfServeRetrying.value = false;
|
isSelfServeRetrying.value = false;
|
||||||
}
|
}
|
||||||
@@ -1103,7 +1199,7 @@ const onVehicleStepNext = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await fetchSelfServeData();
|
await fetchSelfServeData({ immediate: true });
|
||||||
await nextTick();
|
await nextTick();
|
||||||
markQuestionReviewRequired();
|
markQuestionReviewRequired();
|
||||||
currentStep.value = visibleQuestions.value.length > 0 ? steps.QUESTIONS : steps.SELECT_LANE;
|
currentStep.value = visibleQuestions.value.length > 0 ? steps.QUESTIONS : steps.SELECT_LANE;
|
||||||
@@ -1187,7 +1283,6 @@ const onConfirmNext = async () => {
|
|||||||
const onCloseCompleted = () => {
|
const onCloseCompleted = () => {
|
||||||
stopElapsedTimer();
|
stopElapsedTimer();
|
||||||
washInProgress.value = false;
|
washInProgress.value = false;
|
||||||
hasConfirmedActiveWashOnServer.value = false;
|
|
||||||
washLaneId.value = null;
|
washLaneId.value = null;
|
||||||
washStartTime.value = null;
|
washStartTime.value = null;
|
||||||
completedDurationMs.value = null;
|
completedDurationMs.value = null;
|
||||||
@@ -1227,6 +1322,9 @@ onUnmounted(() => {
|
|||||||
unregisterBeforeUnload.value();
|
unregisterBeforeUnload.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSelfServeFetchUnmounted = true;
|
||||||
|
clearScheduledSelfServeFetch(null);
|
||||||
|
selfServeFetchSequence += 1;
|
||||||
stopAutoRefresh();
|
stopAutoRefresh();
|
||||||
stopActiveWashRefresh();
|
stopActiveWashRefresh();
|
||||||
markDestroying();
|
markDestroying();
|
||||||
@@ -1358,15 +1456,10 @@ watch(
|
|||||||
|
|
||||||
watch([() => washInProgress.value, () => washLaneId.value], ([inProgress]) => {
|
watch([() => washInProgress.value, () => washLaneId.value], ([inProgress]) => {
|
||||||
if (inProgress) {
|
if (inProgress) {
|
||||||
if (isRestoring.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hasConfirmedActiveWashOnServer.value = true;
|
|
||||||
startActiveWashRefresh();
|
startActiveWashRefresh();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasConfirmedActiveWashOnServer.value = false;
|
|
||||||
stopActiveWashRefresh();
|
stopActiveWashRefresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1415,9 +1508,6 @@ watch(
|
|||||||
if (!restoring) {
|
if (!restoring) {
|
||||||
ensureEffectiveLaneSelection();
|
ensureEffectiveLaneSelection();
|
||||||
fetchSelfServeData();
|
fetchSelfServeData();
|
||||||
if (washInProgress.value) {
|
|
||||||
startActiveWashRefresh();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1499,6 +1589,7 @@ watch(
|
|||||||
<b-message
|
<b-message
|
||||||
v-if="selfServeDataError"
|
v-if="selfServeDataError"
|
||||||
type="is-danger"
|
type="is-danger"
|
||||||
|
has-icon
|
||||||
:closable="false"
|
:closable="false"
|
||||||
data-testid="self-serve-runtime-error"
|
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 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("daily-report-page")).toBeVisible();
|
||||||
await expect(page.getByTestId("department-weather-hours-multi-day-06:00")).toHaveText("1");
|
await expect(page.getByTestId("department-weather-hours-2026-03-23-06:00")).toHaveText("1");
|
||||||
await expect(page.getByTestId("department-weather-hours-multi-day-19:00")).toHaveText("19");
|
await expect(page.getByTestId("department-weather-hours-2026-03-23-19:00")).toHaveText("19");
|
||||||
await expect(page.getByTestId("department-weather-hours-multi-day-20:00")).toHaveText("21");
|
await expect(page.getByTestId("department-weather-hours-2026-03-23-20:00")).toHaveText("21");
|
||||||
await expect(page.getByTestId("department-weather-productivity-multi-day-06:00")).toHaveClass(/red/);
|
await expect(page.getByTestId("department-weather-productivity-2026-03-23-06:00")).toHaveClass(/red/);
|
||||||
await expect(page.getByTestId("department-weather-productivity-multi-day-08:00")).toHaveClass(/gray/);
|
await expect(page.getByTestId("department-weather-productivity-2026-03-24-08:00")).toHaveClass(/gray/);
|
||||||
|
|
||||||
await page.getByTestId("department-weather-summary-toggle").click();
|
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 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("daily-report-page")).toBeVisible();
|
||||||
await expect(page.getByTestId("department-weather-hours-multi-day-12:00")).toHaveText("1");
|
await expect(page.getByTestId("department-weather-hours-2026-03-23-12:00")).toHaveText("1");
|
||||||
await expect(page.getByTestId("department-weather-hours-multi-day-13:00")).toHaveText("1");
|
await expect(page.getByTestId("department-weather-hours-2026-03-24-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-24-14:00")).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByTestId("department-weather-summary-toggle").click();
|
await page.getByTestId("department-weather-summary-toggle").click();
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ const expectedFuldfValues = {
|
|||||||
|
|
||||||
const expectedSelfWashValues = {
|
const expectedSelfWashValues = {
|
||||||
"self_wash.lane_unavailable": "Vaskebanen er ikke tilgængelig",
|
"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_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_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.",
|
"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({
|
expect(startCommandRequest.postDataJSON?.()).toMatchObject({
|
||||||
command: "START",
|
command: "START",
|
||||||
customer_number: 12345679,
|
customer_number: 12345679,
|
||||||
defer_relay_side_effects: false,
|
defer_relay_side_effects: true,
|
||||||
});
|
});
|
||||||
await page.waitForTimeout(250);
|
await page.waitForTimeout(250);
|
||||||
expect(orderFilters.some((filters) => filters.includes("customer_id:777"))).toBe(false);
|
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 () => {
|
vi.mock("@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReportObject.vue", async () => {
|
||||||
const { ref } = await import("vue");
|
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";
|
import DepartmentDailyReport from "@/views/dashboards/departmentDashboard/modules/daily-report/DepartmentDailyReport.vue";
|
||||||
|
|
||||||
const flushAll = async () => {
|
const flushAll = async () => {
|
||||||
|
|||||||
@@ -278,11 +278,11 @@ describe("DepartmentWeather", () => {
|
|||||||
|
|
||||||
await flushAll();
|
await flushAll();
|
||||||
|
|
||||||
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-06:00").text()).toBe("1");
|
expect(getCellByTestId(wrapper, "department-weather-hours-2026-03-23-06:00").text()).toBe("1");
|
||||||
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-19:00").text()).toBe("19");
|
expect(getCellByTestId(wrapper, "department-weather-hours-2026-03-23-19:00").text()).toBe("19");
|
||||||
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-20:00").text()).toBe("21");
|
expect(getCellByTestId(wrapper, "department-weather-hours-2026-03-23-20:00").text()).toBe("21");
|
||||||
expect(getCellByTestId(wrapper, "department-weather-productivity-multi-day-06:00").classes()).toContain("red");
|
expect(getCellByTestId(wrapper, "department-weather-productivity-2026-03-23-06:00").classes()).toContain("red");
|
||||||
expect(getCellByTestId(wrapper, "department-weather-productivity-multi-day-08:00").classes()).toContain("gray");
|
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 wrapper.get('[data-testid="department-weather-summary-toggle"]').trigger("click");
|
||||||
await flushAll();
|
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 () => {
|
it("starts in hourly mode and lets users toggle to daily summary in multi-day mode", async () => {
|
||||||
getObjectsMock.mockResolvedValueOnce([
|
getObjectsMock.mockResolvedValueOnce([
|
||||||
makeEntry({ date: "2026-03-23", time: "06:00", washes: 1, hours: 1 }),
|
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-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, {
|
const wrapper = mount(DepartmentWeather, {
|
||||||
@@ -366,7 +341,7 @@ describe("DepartmentWeather", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await flushAll();
|
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 wrapper.get('[data-testid="department-weather-summary-toggle"]').trigger("click");
|
||||||
await flushAll();
|
await flushAll();
|
||||||
|
|||||||
@@ -419,6 +419,8 @@ describe("MyWashStart", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("wires child updates back into the self-serve runtime", async () => {
|
it("wires child updates back into the self-serve runtime", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
const wrapper = mountWithApp(MyWashStart, {
|
const wrapper = mountWithApp(MyWashStart, {
|
||||||
global: {
|
global: {
|
||||||
stubs: stubComponents,
|
stubs: stubComponents,
|
||||||
@@ -430,6 +432,7 @@ describe("MyWashStart", () => {
|
|||||||
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
await wrapper.get('[data-testid="emit-registration"]').trigger("click");
|
||||||
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
await wrapper.get('[data-testid="emit-vehicle-type"]').trigger("click");
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
await vi.advanceTimersByTimeAsync(300);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
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 () => {
|
it("hides machine tasks while manual wash is selected", async () => {
|
||||||
mocks.activeTasks.value = [
|
mocks.activeTasks.value = [
|
||||||
{ id: 31, task: "Machine checklist", services: ["MACHINE"] },
|
{ 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 () => {
|
it("falls back to available lane when restored lane is stale for the selected department", async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
mocks.restoredProgressPayload = {
|
mocks.restoredProgressPayload = {
|
||||||
washInProgress: false,
|
washInProgress: false,
|
||||||
washLaneId: null,
|
washLaneId: null,
|
||||||
@@ -619,6 +652,8 @@ describe("MyWashStart", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await vi.advanceTimersByTimeAsync(300);
|
||||||
await flushPromises();
|
await flushPromises();
|
||||||
|
|
||||||
expect(mocks.fetchSelfServeDataInternal).toHaveBeenCalledWith(6, 2, 7, "AB12345");
|
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", () => {
|
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.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_customer_number).toBe("Indtast kundenummer for at fortsætte.");
|
||||||
expect(da.self_wash.vehicle_step_missing_department).toBe(
|
expect(da.self_wash.vehicle_step_missing_department).toBe(
|
||||||
"Vent på, at nærmeste afdeling er indlæst, før du fortsætter."
|
"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");
|
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({
|
const wrapper = mountLaneStep({
|
||||||
isMachineAvailable: () => false,
|
isMachineAvailable: () => false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const machineButton = wrapper.get('[data-testid="self-serve-wash-type-machine"]');
|
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("disabled")).toBeDefined();
|
||||||
expect(machineButton.attributes("title")).toBe("self_wash.unavailable");
|
expect(machineButton.attributes("title")).toBe("self_wash.machine_unavailable_for_lane");
|
||||||
expect(machineButton.text()).toContain("self_wash.unavailable");
|
expect(guidance.text()).toContain("self_wash.machine_unavailable_for_lane");
|
||||||
expect(wrapper.find('[data-testid="self-serve-machine-unavailable-guidance"]').exists()).toBe(false);
|
expect(washTypeOptions.find('[data-testid="self-serve-machine-unavailable-guidance"]').exists()).toBe(false);
|
||||||
expect(wrapper.text()).not.toContain("self_wash.machine_unavailable_for_lane");
|
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",
|
command: "START",
|
||||||
customer_number: 12345679,
|
customer_number: 12345679,
|
||||||
license_plate: "AB12345",
|
license_plate: "AB12345",
|
||||||
defer_relay_side_effects: false,
|
defer_relay_side_effects: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
consoleErrorSpy.mockRestore();
|
consoleErrorSpy.mockRestore();
|
||||||
@@ -286,7 +286,7 @@ describe("useWashSessionActions property gate commands", () => {
|
|||||||
command: "START",
|
command: "START",
|
||||||
customer_number: 12345679,
|
customer_number: 12345679,
|
||||||
license_plate: "AB12345",
|
license_plate: "AB12345",
|
||||||
defer_relay_side_effects: false,
|
defer_relay_side_effects: true,
|
||||||
});
|
});
|
||||||
expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", {
|
expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", {
|
||||||
lane_id: 7,
|
lane_id: 7,
|
||||||
@@ -346,7 +346,7 @@ describe("useWashSessionActions property gate commands", () => {
|
|||||||
command: "START",
|
command: "START",
|
||||||
customer_number: 12345679,
|
customer_number: 12345679,
|
||||||
license_plate: "AB12345",
|
license_plate: "AB12345",
|
||||||
defer_relay_side_effects: false,
|
defer_relay_side_effects: true,
|
||||||
});
|
});
|
||||||
expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", {
|
expect(request).toHaveBeenCalledWith("/modules/self-serve/lane/command", "post", {
|
||||||
lane_id: 7,
|
lane_id: 7,
|
||||||
|
|||||||
@@ -10,15 +10,6 @@ export default defineConfig({
|
|||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": 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: {
|
test: {
|
||||||
environment: "node",
|
environment: "node",
|
||||||
include: ["tests/unit/**/*.spec.js"],
|
include: ["tests/unit/**/*.spec.js"],
|
||||||
|
|||||||
Reference in New Issue
Block a user