Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f671024ab | ||
|
|
605a7ca433 |
Generated
+29
-10
@@ -41,6 +41,7 @@
|
|||||||
"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",
|
||||||
@@ -5069,7 +5070,6 @@
|
|||||||
"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,7 +5085,6 @@
|
|||||||
"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"
|
||||||
@@ -5737,7 +5736,6 @@
|
|||||||
"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"
|
||||||
@@ -6425,7 +6423,6 @@
|
|||||||
"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"
|
||||||
@@ -8988,7 +8985,6 @@
|
|||||||
"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"
|
||||||
@@ -9571,7 +9567,6 @@
|
|||||||
"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": {
|
||||||
@@ -10109,7 +10104,6 @@
|
|||||||
"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": {
|
||||||
@@ -10171,6 +10165,34 @@
|
|||||||
"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",
|
||||||
@@ -10728,7 +10750,6 @@
|
|||||||
"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": {
|
||||||
@@ -11646,7 +11667,6 @@
|
|||||||
"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"
|
||||||
@@ -11918,7 +11938,6 @@
|
|||||||
"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,6 +86,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<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,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
import { BIcon, BMessage, BRadioButton } from "buefy";
|
import { BIcon, BMessage, BRadioButton } from "buefy";
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
lanes: Array<any>;
|
lanes: Array<any>;
|
||||||
selectedLaneId: number | string | null;
|
selectedLaneId: number | string | null;
|
||||||
washType: string;
|
washType: string;
|
||||||
@@ -14,6 +15,8 @@ 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>
|
||||||
@@ -92,16 +95,16 @@ const emit = defineEmits<{
|
|||||||
:model-value="washType"
|
:model-value="washType"
|
||||||
native-value="Machine"
|
native-value="Machine"
|
||||||
type="is-link"
|
type="is-link"
|
||||||
:disabled="!isMachineAvailable(selectedLaneId)"
|
:disabled="!isSelectedMachineAvailable"
|
||||||
:title="!isMachineAvailable(selectedLaneId) ? $t('self_wash.machine_unavailable_for_lane') : null"
|
:title="!isSelectedMachineAvailable ? $t('self_wash.unavailable') : null"
|
||||||
:aria-label="!isMachineAvailable(selectedLaneId) ? $t('self_wash.machine_unavailable_for_lane') : null"
|
:aria-label="!isSelectedMachineAvailable ? $t('self_wash.unavailable') : 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="!isMachineAvailable(selectedLaneId)">
|
<span v-if="!isSelectedMachineAvailable">
|
||||||
<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") }}
|
||||||
@@ -117,14 +120,6 @@ const emit = defineEmits<{
|
|||||||
</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 { ref, watch } from "vue";
|
import { BIcon } from "buefy";
|
||||||
import { BIcon, BSkeleton } from "buefy";
|
import SelfServeDynamicImageFrame from "@/components/displays/selfServe/SelfServeDynamicImageFrame.vue";
|
||||||
import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue";
|
import SelfServeTaskList from "@/components/displays/selfServe/SelfServeTaskList.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -18,12 +18,6 @@ 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);
|
||||||
};
|
};
|
||||||
@@ -32,12 +26,7 @@ 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>
|
||||||
@@ -58,30 +47,11 @@ const onDynamicImageError = () => {
|
|||||||
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
|
<span class="ml-2">{{ $t("self_wash.loading_data") }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<SelfServeDynamicImageFrame
|
||||||
v-if="dynamicImageUrl"
|
:src="dynamicImageUrl"
|
||||||
class="self-serve-dynamic-image-frame mb-4"
|
alt="Machine status"
|
||||||
:class="{ 'is-loading': isDynamicImageLoading }"
|
@error="onDynamicImageError"
|
||||||
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>
|
||||||
@@ -95,41 +65,3 @@ 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)">
|
<ActionSettingsWheelButton v-if="hasRowActions(subuser)" display-actions-directly>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<ActionSettingsWheelItem
|
<ActionSettingsWheelItem
|
||||||
v-if="canEditPermissions() && subuser.grant_id"
|
v-if="canEditPermissions() && subuser.grant_id"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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";
|
||||||
|
|
||||||
@@ -271,7 +272,26 @@ const mergeByNumericId = (existingItems, incomingItems) => {
|
|||||||
return merged;
|
return merged;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function useSelfServeLogic() {
|
let selfServeLogicStoreInstance = 0;
|
||||||
|
let fallbackSelfServePinia = null;
|
||||||
|
|
||||||
|
const ensureSelfServePinia = () => {
|
||||||
|
if (typeof document === "undefined") {
|
||||||
|
fallbackSelfServePinia ||= createPinia();
|
||||||
|
setActivePinia(fallbackSelfServePinia);
|
||||||
|
return fallbackSelfServePinia;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activePinia = getActivePinia();
|
||||||
|
if (activePinia) {
|
||||||
|
return activePinia;
|
||||||
|
}
|
||||||
|
fallbackSelfServePinia ||= createPinia();
|
||||||
|
setActivePinia(fallbackSelfServePinia);
|
||||||
|
return fallbackSelfServePinia;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSelfServeLogicStore = (storeId) => defineStore(storeId, () => {
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const requestError = ref(null);
|
const requestError = ref(null);
|
||||||
const preview = ref(null);
|
const preview = ref(null);
|
||||||
@@ -1075,4 +1095,50 @@ export function useSelfServeLogic() {
|
|||||||
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: true,
|
defer_relay_side_effects: false,
|
||||||
});
|
});
|
||||||
if (!startResponse) {
|
if (!startResponse) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -4595,7 +4595,6 @@
|
|||||||
"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,7 +4705,6 @@
|
|||||||
"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,7 +4429,6 @@
|
|||||||
"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,7 +4224,6 @@
|
|||||||
"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,7 +4706,6 @@
|
|||||||
"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,7 +4756,6 @@
|
|||||||
"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,7 +3663,6 @@
|
|||||||
"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,7 +3663,6 @@
|
|||||||
"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,7 +3662,6 @@
|
|||||||
"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,7 +3663,6 @@
|
|||||||
"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,7 +3663,6 @@
|
|||||||
"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,7 +18,6 @@
|
|||||||
"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,7 +18,6 @@
|
|||||||
"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,7 +18,6 @@
|
|||||||
"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,7 +21,6 @@
|
|||||||
"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,7 +18,6 @@
|
|||||||
"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,7 +18,6 @@
|
|||||||
"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,6 +10,7 @@ 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'
|
||||||
@@ -108,6 +109,7 @@ 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)
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
+67
-6
@@ -95,6 +95,25 @@ 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();
|
||||||
|
|
||||||
@@ -120,14 +139,48 @@ 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: Number.isInteger(washes) ? washes : Number(washes.toFixed(2)),
|
washes: formatMergedNumber(washes),
|
||||||
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: hours > 0 ? (washes / hours >= 1.3 ? "healthy" : washes / hours >= 1.0 ? "degraded" : "unhealthy") : "unknown",
|
status: getProductivityStatus(washes, hours),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const summarizeEntriesByHourOfDay = (entries) => {
|
||||||
|
const groupedByTime = new Map();
|
||||||
|
|
||||||
|
entries.forEach((entry) => {
|
||||||
|
const time = getNormalizedTime(entry);
|
||||||
|
if (!time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!groupedByTime.has(time)) {
|
||||||
|
groupedByTime.set(time, []);
|
||||||
|
}
|
||||||
|
groupedByTime.get(time).push(entry);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(groupedByTime.entries())
|
||||||
|
.sort(([timeA], [timeB]) => timeA.localeCompare(timeB))
|
||||||
|
.map(([time, hourEntries]) => {
|
||||||
|
const washes = hourEntries.reduce((sum, entry) => sum + toNumberOrZero(entry?.washes), 0);
|
||||||
|
const hours = hourEntries.reduce((sum, entry) => sum + toNumberOrZero(entry?.hours), 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: null,
|
||||||
|
test_id_date: "multi-day",
|
||||||
|
time,
|
||||||
|
current: hourEntries.some((entry) => entry?.current === true),
|
||||||
|
weather: getDominantWeather(hourEntries),
|
||||||
|
washes: formatMergedNumber(washes),
|
||||||
|
hours: Number(hours.toFixed(2)),
|
||||||
|
status: getProductivityStatus(washes, hours),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -136,6 +189,10 @@ 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));
|
||||||
});
|
});
|
||||||
@@ -144,6 +201,9 @@ 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;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -167,7 +227,9 @@ const getEntryKey = (entry, index) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getEntryTestId = (prefix, entry) => {
|
const getEntryTestId = (prefix, entry) => {
|
||||||
const date = isDateString(entry?.date) ? entry.date : "no-date";
|
const date = typeof entry?.test_id_date === "string"
|
||||||
|
? 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}`;
|
||||||
};
|
};
|
||||||
@@ -177,9 +239,8 @@ const getHourDetailsSlotKey = (entry) => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeValue = typeof entry?.time === "string" ? entry.time : "";
|
const normalizedTime = getNormalizedTime(entry);
|
||||||
const normalizedTime = timeValue.slice(0, 5);
|
if (!normalizedTime) {
|
||||||
if (!/^\d{2}:\d{2}$/.test(normalizedTime)) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+80
-101
@@ -1,5 +1,6 @@
|
|||||||
<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";
|
||||||
@@ -34,6 +35,7 @@ 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();
|
||||||
@@ -367,111 +369,88 @@ const AUTO_LAYOUT_RUNTIME_STAGE_ORDER = {
|
|||||||
finish: 2,
|
finish: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const flowNodes = ref([]);
|
const studioStore = useSelfServeStudioStore();
|
||||||
const flowEdges = ref([]);
|
const {
|
||||||
const graphPayload = ref(null);
|
flowNodes,
|
||||||
const lookups = ref({});
|
flowEdges,
|
||||||
const validation = ref({ valid: true, items: [], errors: [], warnings: [] });
|
graphPayload,
|
||||||
const gatewayWorkspace = ref({ gateways: [], relays: [], lanes: [], issues: [], actions: [] });
|
lookups,
|
||||||
const versions = ref([]);
|
validation,
|
||||||
const activePanel = ref(panelFromRoute() || "flow");
|
gatewayWorkspace,
|
||||||
const selectedNodeId = ref(null);
|
versions,
|
||||||
const inspectorForm = ref({});
|
activePanel,
|
||||||
const simulatorForm = ref({
|
selectedNodeId,
|
||||||
lane_id: null,
|
inspectorForm,
|
||||||
reg: "TEST123",
|
simulatorForm,
|
||||||
customer_number: null,
|
simulatorResult,
|
||||||
config_source: "draft",
|
simulatorAnswerOverrides,
|
||||||
include_hardware: true,
|
simulatorShowJson,
|
||||||
hardware_mode: "studio",
|
simulatorDynamicImageHidden,
|
||||||
});
|
simulatorDynamicImageError,
|
||||||
const simulatorResult = ref(null);
|
simulatorCompletedTasks,
|
||||||
const simulatorAnswerOverrides = ref({});
|
isSimulatorRunning,
|
||||||
const simulatorShowJson = ref(false);
|
simulatorRunReason,
|
||||||
const simulatorDynamicImageHidden = ref(false);
|
simulatorRequestSequence,
|
||||||
const simulatorDynamicImageError = ref("");
|
simulatorLastRequestKey,
|
||||||
const simulatorCompletedTasks = ref({});
|
simulatorLastError,
|
||||||
const isSimulatorRunning = ref(false);
|
isRestoringSimulatorStorage,
|
||||||
const simulatorRunReason = ref("");
|
pathOutcomes,
|
||||||
|
isLoadingPathOutcomes,
|
||||||
|
pathOutcomesError,
|
||||||
|
pathOutcomesRequestKey,
|
||||||
|
selectedPathOutcomeId,
|
||||||
|
selectedPathCaseSignature,
|
||||||
|
selectedPathCaseIndex,
|
||||||
|
pathCaseRunSignatures,
|
||||||
|
pathCaseConfirmingSignature,
|
||||||
|
pathEditorDirty,
|
||||||
|
isHydratingPathEditor,
|
||||||
|
pathEditorTaskTitleOverrides,
|
||||||
|
editingPathEditorTaskTitleKey,
|
||||||
|
editingPathEditorTaskTitle,
|
||||||
|
pathVerificationDynamicImageHidden,
|
||||||
|
pathVerificationDynamicImageError,
|
||||||
|
isPathVerificationDynamicImageLoading,
|
||||||
|
pathEditorQuestionDraftVisible,
|
||||||
|
pathEditorQuestionDraft,
|
||||||
|
pathEditorForm,
|
||||||
|
simulatorCustomerSearchQuery,
|
||||||
|
selectedSimulatorCustomer,
|
||||||
|
showSimulatorCustomerResults,
|
||||||
|
scopeGuideDraft,
|
||||||
|
isScopeGuideManuallyOpen,
|
||||||
|
isSyncingScopeFromRoute,
|
||||||
|
isWritingScopeToRoute,
|
||||||
|
virtualHardwareForm,
|
||||||
|
laneEditorMode,
|
||||||
|
selectedLaneManagementId,
|
||||||
|
laneForm,
|
||||||
|
lanePreviewHidden,
|
||||||
|
inspectorLanePreviewHidden,
|
||||||
|
inspectorTaskDynamicImageHidden,
|
||||||
|
inspectorTaskDynamicImageError,
|
||||||
|
searchTerm,
|
||||||
|
filters,
|
||||||
|
isLoading,
|
||||||
|
isSaving,
|
||||||
|
isUploadingTaskAttachment,
|
||||||
|
deletingTaskAttachmentId,
|
||||||
|
isStudioFullscreen,
|
||||||
|
hasNativeStudioFullscreen,
|
||||||
|
suppressLayoutSave,
|
||||||
|
exportText,
|
||||||
|
importText,
|
||||||
|
draggedTemplateKind,
|
||||||
|
unfilteredNodePositions,
|
||||||
|
} = storeToRefs(studioStore);
|
||||||
|
|
||||||
|
activePanel.value = panelFromRoute() || "flow";
|
||||||
|
studioStore.applyRouteScope(scopeFiltersFromRoute());
|
||||||
|
|
||||||
const 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,6 +55,7 @@ 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;
|
||||||
@@ -833,6 +834,7 @@ 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;
|
||||||
@@ -917,6 +919,7 @@ 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;
|
||||||
@@ -952,8 +955,14 @@ 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 })) {
|
||||||
@@ -964,6 +973,7 @@ 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);
|
||||||
@@ -1177,6 +1187,7 @@ 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;
|
||||||
@@ -1347,10 +1358,15 @@ 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1399,6 +1415,9 @@ watch(
|
|||||||
if (!restoring) {
|
if (!restoring) {
|
||||||
ensureEffectiveLaneSelection();
|
ensureEffectiveLaneSelection();
|
||||||
fetchSelfServeData();
|
fetchSelfServeData();
|
||||||
|
if (washInProgress.value) {
|
||||||
|
startActiveWashRefresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -1480,7 +1499,6 @@ 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-2026-03-23-06:00")).toHaveText("1");
|
await expect(page.getByTestId("department-weather-hours-multi-day-06:00")).toHaveText("1");
|
||||||
await expect(page.getByTestId("department-weather-hours-2026-03-23-19:00")).toHaveText("19");
|
await expect(page.getByTestId("department-weather-hours-multi-day-19:00")).toHaveText("19");
|
||||||
await expect(page.getByTestId("department-weather-hours-2026-03-23-20:00")).toHaveText("21");
|
await expect(page.getByTestId("department-weather-hours-multi-day-20:00")).toHaveText("21");
|
||||||
await expect(page.getByTestId("department-weather-productivity-2026-03-23-06:00")).toHaveClass(/red/);
|
await expect(page.getByTestId("department-weather-productivity-multi-day-06:00")).toHaveClass(/red/);
|
||||||
await expect(page.getByTestId("department-weather-productivity-2026-03-24-08:00")).toHaveClass(/gray/);
|
await expect(page.getByTestId("department-weather-productivity-multi-day-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-2026-03-23-12:00")).toHaveText("1");
|
await expect(page.getByTestId("department-weather-hours-multi-day-12:00")).toHaveText("1");
|
||||||
await expect(page.getByTestId("department-weather-hours-2026-03-24-13:00")).toHaveText("1");
|
await expect(page.getByTestId("department-weather-hours-multi-day-13:00")).toHaveText("1");
|
||||||
await expect(page.getByTestId("department-weather-hours-2026-03-24-14:00")).toHaveCount(0);
|
await expect(page.getByTestId("department-weather-hours-multi-day-14:00")).toHaveCount(0);
|
||||||
|
|
||||||
await page.getByTestId("department-weather-summary-toggle").click();
|
await page.getByTestId("department-weather-summary-toggle").click();
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ 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: true,
|
defer_relay_side_effects: false,
|
||||||
});
|
});
|
||||||
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,6 +64,22 @@ 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");
|
||||||
|
|
||||||
@@ -104,6 +120,52 @@ 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-2026-03-23-06:00").text()).toBe("1");
|
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-06:00").text()).toBe("1");
|
||||||
expect(getCellByTestId(wrapper, "department-weather-hours-2026-03-23-19:00").text()).toBe("19");
|
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-19:00").text()).toBe("19");
|
||||||
expect(getCellByTestId(wrapper, "department-weather-hours-2026-03-23-20:00").text()).toBe("21");
|
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-20:00").text()).toBe("21");
|
||||||
expect(getCellByTestId(wrapper, "department-weather-productivity-2026-03-23-06:00").classes()).toContain("red");
|
expect(getCellByTestId(wrapper, "department-weather-productivity-multi-day-06:00").classes()).toContain("red");
|
||||||
expect(getCellByTestId(wrapper, "department-weather-productivity-2026-03-24-08:00").classes()).toContain("gray");
|
expect(getCellByTestId(wrapper, "department-weather-productivity-multi-day-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,11 +325,11 @@ describe("DepartmentWeather", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts in hourly mode and lets users toggle to daily summary in multi-day mode", async () => {
|
it("merges matching hours into one column for multi-day hourly 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", weather: "clear", washes: 1, hours: 0.5, status: "unhealthy" }),
|
||||||
makeEntry({ date: "2026-03-23", time: "07:00", washes: 2, hours: 2 }),
|
makeEntry({ date: "2026-03-24", time: "06:00", weather: "rain", washes: 2, hours: 0.6, status: "healthy" }),
|
||||||
makeEntry({ date: "2026-03-24", time: "08:00", washes: 3, hours: 3 }),
|
makeEntry({ date: "2026-03-24", time: "07:00", weather: "rain", washes: 4, hours: 2, status: "healthy" }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const wrapper = mount(DepartmentWeather, {
|
const wrapper = mount(DepartmentWeather, {
|
||||||
@@ -341,7 +341,32 @@ describe("DepartmentWeather", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await flushAll();
|
await flushAll();
|
||||||
expect(getRenderedHours(wrapper)).toEqual(["06", "07", "08"]);
|
|
||||||
|
expect(getRenderedHours(wrapper)).toEqual(["06", "07"]);
|
||||||
|
expect(getRowValues(wrapper, 1)).toEqual(["3", "4"]);
|
||||||
|
expect(getCellByTestId(wrapper, "department-weather-hours-multi-day-06:00").text()).toBe("2");
|
||||||
|
expect(getCellByTestId(wrapper, "department-weather-productivity-multi-day-06:00").classes()).toContain("green");
|
||||||
|
expect(wrapper.find('[data-testid="department-weather-hours-2026-03-23-06:00"]').exists()).toBe(false);
|
||||||
|
expect(wrapper.find('[data-testid="department-weather-hours-2026-03-24-06:00"]').exists()).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts in hourly mode and lets users toggle to daily summary in multi-day mode", async () => {
|
||||||
|
getObjectsMock.mockResolvedValueOnce([
|
||||||
|
makeEntry({ date: "2026-03-23", time: "06:00", washes: 1, hours: 1 }),
|
||||||
|
makeEntry({ date: "2026-03-23", time: "07:00", washes: 2, hours: 2 }),
|
||||||
|
makeEntry({ date: "2026-03-24", time: "06:00", washes: 3, hours: 3 }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
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"]);
|
||||||
|
|
||||||
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();
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ 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,21 +78,16 @@ describe("SelfServeLaneStep", () => {
|
|||||||
expect(wrapper.text()).toContain("Tilgængelig");
|
expect(wrapper.text()).toContain("Tilgængelig");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows machine unavailable guidance for the selected lane", () => {
|
it("keeps machine wash disabled without rendering extra guidance", () => {
|
||||||
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.machine_unavailable_for_lane");
|
expect(machineButton.attributes("title")).toBe("self_wash.unavailable");
|
||||||
expect(guidance.text()).toContain("self_wash.machine_unavailable_for_lane");
|
expect(machineButton.text()).toContain("self_wash.unavailable");
|
||||||
expect(washTypeOptions.find('[data-testid="self-serve-machine-unavailable-guidance"]').exists()).toBe(false);
|
expect(wrapper.find('[data-testid="self-serve-machine-unavailable-guidance"]').exists()).toBe(false);
|
||||||
expect(Array.from(washTypeGroup.element.children).indexOf(washTypeOptions.element)).toBeLessThan(
|
expect(wrapper.text()).not.toContain("self_wash.machine_unavailable_for_lane");
|
||||||
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: true,
|
defer_relay_side_effects: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
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: true,
|
defer_relay_side_effects: false,
|
||||||
});
|
});
|
||||||
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: true,
|
defer_relay_side_effects: false,
|
||||||
});
|
});
|
||||||
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,6 +10,15 @@ 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