Extend self-serve functionality with new machine statuses, config version tracking, and detailed session displays:

- Added `INCLUDED` and `BILLABLE` statuses to `MachineStatusType` for enhanced session tracking.
- Introduced `configVersionId` and `evaluationTrace` support in self-serve logic and UI.
- Enhanced `SelfServeMachineRelayControls` with billable session parsing and UI updates.
- Updated `SelfServeMachineStatus` to reflect new statuses with appropriate icons and labels.
- Improved session summary displays with detailed evaluation trace output.
This commit is contained in:
Jeppe Bundgaard
2026-03-25 16:15:19 +01:00
parent 5b181087ff
commit 2c22a548e1
5 changed files with 100 additions and 9 deletions
@@ -40,6 +40,8 @@ const {
questions,
answers,
allowedServices,
configVersionId,
evaluationTrace,
visibleQuestions,
activeTasks,
currentQuestion,
@@ -169,6 +171,9 @@ watch(() => [selectedLaneId.value, reg.value], async ([laneId, registration], [o
<span v-if="lane" class="tag is-light">
Lane: {{ lane.name || lane.id }}
</span>
<span v-if="configVersionId" class="tag is-dark">
Config version: #{{ configVersionId }}
</span>
</div>
<div v-if="allowedServices.length > 0" class="tags mt-2">
@@ -236,6 +241,14 @@ watch(() => [selectedLaneId.value, reg.value], async ([laneId, registration], [o
<hr />
<h5 class="subtitle is-6">Evaluation trace</h5>
<div v-if="evaluationTrace" class="content is-small">
<pre>{{ JSON.stringify(evaluationTrace, null, 2) }}</pre>
</div>
<p v-else class="is-italic">Ingen trace-data returneret.</p>
<hr />
<h5 class="subtitle is-6">Besvarede sporgsmal</h5>
<ul>
<li v-for="question in answeredQuestions" :key="question.id" class="mb-1">
+20
View File
@@ -42,6 +42,8 @@ const normalizeTask = (task) => ({
task: task?.task ?? "",
description: task?.description ?? "",
condition_id: task?.condition_id ?? null,
gate_type: task?.gate_type ?? null,
gate_ref_id: task?.gate_ref_id ?? null,
order_priority: parseInt(task?.order_priority ?? 0),
services: Array.isArray(task?.services) ? task.services : [],
buttons: Array.isArray(task?.buttons) ? task.buttons : [],
@@ -172,6 +174,8 @@ export function useSelfServeLogic() {
const answers = ref({});
const completedTasks = ref({});
const allowedServices = ref([]);
const configVersionId = ref(null);
const evaluationTrace = ref(null);
const lastPreviewContextKey = ref(null);
const summaryVisibleQuestionIds = ref([]);
const summaryQuestionOrder = ref({});
@@ -248,6 +252,12 @@ export function useSelfServeLogic() {
machineType.value = previewData?.machine_type || machineType.value;
vehicle.value = previewData?.vehicle || null;
session.value = previewData?.session || session.value;
if (Object.prototype.hasOwnProperty.call(previewData || {}, "config_version_id")) {
configVersionId.value = previewData?.config_version_id ?? null;
}
if (Object.prototype.hasOwnProperty.call(previewData || {}, "evaluation_trace")) {
evaluationTrace.value = previewData?.evaluation_trace ?? null;
}
const previewQuestionsRaw = Array.isArray(previewData?.questions) ? previewData.questions : [];
const normalizedQuestions = previewQuestionsRaw.length > 0
@@ -291,6 +301,12 @@ export function useSelfServeLogic() {
lane.value = summaryData?.lane || lane.value;
machineType.value = summaryData?.machine_type || machineType.value;
events.value = Array.isArray(summaryData?.events) ? summaryData.events : [];
if (Object.prototype.hasOwnProperty.call(summaryData || {}, "config_version_id")) {
configVersionId.value = summaryData?.config_version_id ?? null;
}
if (Object.prototype.hasOwnProperty.call(summaryData || {}, "evaluation_trace")) {
evaluationTrace.value = summaryData?.evaluation_trace ?? null;
}
if (Array.isArray(summaryData?.questions)) {
const summaryQuestionsRaw = summaryData.questions;
@@ -372,6 +388,8 @@ export function useSelfServeLogic() {
tasks.value = [];
allowedServices.value = [];
answers.value = {};
configVersionId.value = null;
evaluationTrace.value = null;
lastPreviewContextKey.value = null;
summaryVisibleQuestionIds.value = [];
summaryQuestionOrder.value = {};
@@ -634,6 +652,8 @@ export function useSelfServeLogic() {
answers,
completedTasks,
allowedServices,
configVersionId,
evaluationTrace,
visibleQuestions,
activeTasks,
activeTaskServices,
@@ -11,6 +11,7 @@ import {
} from "@/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue";
import type { Machine } from "@/views/dashboards/superUserDashboard/selfserve/types/MachineType.vue";
import {BTooltip} from "buefy";
import MachineStatusType from "@/views/dashboards/superUserDashboard/selfserve/types/MachineStatusType.vue";
const props = defineProps<{ machine: Machine }>();
@@ -84,6 +85,36 @@ const toggleRelay = async (relay: RelayKind, on: boolean) => {
}
};
const parseBillableStatus = (inProgressDetails: typeof connectivity.inProgressDetails.value) => {
if (!inProgressDetails?.in_progress) {
return MachineStatusType.OFF;
}
const includedTimeMs = (inProgressDetails.session.included_minutes ?? 0) * 60 * 1000;
const washStartedAt = new Date(inProgressDetails.session.wash_started_at).getTime();
if (Date.now() - washStartedAt >= includedTimeMs) {
return MachineStatusType.BILLABLE;
}
return MachineStatusType.INCLUDED;
};
const parseBillableMinuteAmount = (inProgressDetails: typeof connectivity.inProgressDetails.value) => {
if (!inProgressDetails?.in_progress) {
return "N/A"
}
// When there's included minutes, it should count down.
// When there's no included minutes, or they've all been used, it should count up from 0.
const includedMinutes = inProgressDetails.session.included_minutes ?? 0;
const washStartedAt = new Date(inProgressDetails.session.wash_started_at).getTime();
const elapsedMinutes = Math.floor((Date.now() - washStartedAt) / (60 * 1000));
if (includedMinutes > 0 && elapsedMinutes >= includedMinutes) {
// If there are included minutes, we show the remaining minutes.
return elapsedMinutes - includedMinutes;
} else if (includedMinutes > 0) {
// If there are no included minutes, we show the elapsed minutes.
return includedMinutes - elapsedMinutes;
}
}
const openGate = async (gate: LaneGate) => {
try {
await connectivity.openGate(gate);
@@ -210,14 +241,32 @@ onUnmounted(() => {
<p>Loading in-progress details...</p>
</template>
<template v-else-if="inProgressDetails?.in_progress">
<p>
<strong>Customer:</strong>
{{ customerDisplay }}
</p>
<p>
<strong>Vehicle:</strong>
{{ vehicleDisplay }}
</p>
<div class="columns is-mobile">
<div class="column is-half">
<p>
<b-tooltip multilined dashed>
<template v-slot:default>
<p>Duration</p>
</template>
<template v-slot:content>
The duration of the current wash, based on the machine's internal timer. This may not be perfectly accurate, but can provide an estimate of how long the current wash has been running.
</template>
</b-tooltip>
</p>
</div>
<div class="column is-half">
<div class="columns is-mobile">
<div class="column is-half">
<SelfServeMachineStatus :machine-status="parseBillableStatus(inProgressDetails)" />
</div>
<div class="column is-half">
<p class="label is-small">
{{ parseBillableMinuteAmount(inProgressDetails) }} min
</p>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<p>No wash in progress.</p>
@@ -10,6 +10,8 @@ const icon = {
MAINTENANCE: "tools",
ON: "check",
OFF: "close",
INCLUDED: "clock",
BILLABLE: "clock",
}
const type = {
ONLINE: "is-success",
@@ -17,6 +19,8 @@ const type = {
MAINTENANCE: "is-warning",
ON: "is-success",
OFF: "is-danger",
INCLUDED: "is-info",
BILLABLE: "is-info",
}
</script>
@@ -32,8 +36,11 @@ const type = {
size="is-small"
/>
</span>
<!-- If INCLUDED -->
<span v-if="machineStatus === 'INCLUDED'">PREPAID</span>
<span v-else-if="machineStatus === 'BILLABLE'">ADD</span>
<!-- If maintenance, show maintenance -->
<span v-if="machineStatus === 'MAINTENANCE'">N/A</span>
<span v-else-if="machineStatus === 'MAINTENANCE'">N/A</span>
<span v-else>{{ machineStatus }}</span>
</span>
</div>
@@ -5,6 +5,8 @@ const MachineStatus = {
MAINTENANCE: "MAINTENANCE",
ON: "ON",
OFF: "OFF",
INCLUDED: "INCLUDED",
BILLABLE: "BILLABLE",
} as const;
export type { MachineStatus };
export default MachineStatus;