Compare commits

...
39 changed files with 657 additions and 263 deletions
+29 -10
View File
@@ -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"
+1
View File
@@ -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"
class="self-serve-dynamic-image-frame mb-4"
:class="{ 'is-loading': isDynamicImageLoading }"
data-testid="self-serve-dynamic-image-frame"
>
<b-skeleton
v-if="isDynamicImageLoading"
class="self-serve-dynamic-image-skeleton"
width="100%"
height="100%"
data-testid="self-serve-dynamic-image-skeleton"
/>
<img
:key="dynamicImageUrl"
:src="dynamicImageUrl" :src="dynamicImageUrl"
alt="Machine status" alt="Machine status"
data-testid="self-serve-dynamic-image"
class="self-serve-dynamic-image"
:class="{ 'is-loading': isDynamicImageLoading }"
@load="onDynamicImageLoad"
@error="onDynamicImageError" @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"
+67 -1
View File
@@ -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,
};
} }
+1 -1
View File
@@ -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;
-1
View File
@@ -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",
-1
View File
@@ -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'}",
-1
View File
@@ -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'}",
-1
View File
@@ -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'}",
-1
View File
@@ -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",
-1
View File
@@ -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",
-1
View File
@@ -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.",
-1
View File
@@ -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.",
-1
View File
@@ -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.",
-1
View File
@@ -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.",
-1
View File
@@ -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",
+2
View File
@@ -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)
+152
View File
@@ -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,
};
},
},
});
@@ -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;
} }
@@ -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"
> >
+8 -8
View File
@@ -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();
-2
View File
@@ -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.",
+1 -1
View File
@@ -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 () => {
+35 -10
View File
@@ -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."
+5 -10
View File
@@ -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)
);
}); });
}); });
+3 -3
View File
@@ -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,
+9
View File
@@ -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"],