fix(self-serve): restrict Dognvask machine to single vehicle type (traekker) (TRU-84)

The Dognvask machine is a single-vehicle-type wash machine. When the
current lane resolves to a Dognvask machine type, the path editor and
scope guide now:

- restrict the vehicle type filter to the 'traekker' (tractor unit) entry
  in both the main toolbar and the scope guide
- show a visible 'Dognvask: trækker only' hint badge so operators can
  see why the options are limited
- auto-correct the vehicle type filter when the user switches to or from
  a Dognvask-configured lane (clearing the filter if no traekker type
  is configured, otherwise snapping to the only allowed option)

Detection compares the resolved selfserve_machine_types row name against
'dognvask' (case-insensitive). The allowed vehicle type is matched by
label/name against 'traekker' / 'tractor' / 'tractor unit' to tolerate
the various Danish/English renderings used across the system.

This follows the same pattern introduced in TRU-91 (path editor machine
filter), which scoped machine-button options to the current lane's
machine type.
This commit is contained in:
TRU-83 bugfix sub-agent
2026-08-17 18:30:52 +00:00
parent c11b164188
commit 12fe4745d7
2 changed files with 166 additions and 4 deletions
@@ -1434,6 +1434,42 @@ const pathEditorCurrentMachineType = computed(() => {
const pathEditorMachineIsAvailable = computed(() => Boolean(pathEditorCurrentMachineType.value));
// TRU-84: Dognvask machine is a single-vehicle-type machine. When the current
// lane is configured to use the Dognvask machine, the vehicle type toolbar and
// the path editor are restricted to the "traekker" (tractor unit) vehicle type.
const pathEditorCurrentMachineTypeName = computed(() => {
const machineType = pathEditorCurrentMachineType.value;
if (!machineType) {
return "";
}
return String(machineType.name || machineType.label || "").trim().toLowerCase();
});
const pathEditorIsDognvaskMachine = computed(
() => pathEditorCurrentMachineTypeName.value === "dognvask"
);
const isTraekkerVehicleType = (vehicleType) => {
if (!vehicleType) {
return false;
}
const candidate = String(
vehicleType.label || vehicleType.name || vehicleType.vehicleTypeId || ""
)
.trim()
.toLowerCase();
return candidate === "traekker" || candidate === "tractor" || candidate === "tractor unit";
};
const pathEditorAllowedVehicleTypes = computed(() => {
const rows = lookupRows("vehicle_types");
if (!pathEditorIsDognvaskMachine.value) {
return rows;
}
const traekker = rows.filter((row) => isTraekkerVehicleType(row));
return traekker;
});
const pathEditorMachineButtonOptions = computed(() => {
if (!pathEditorMachineIsAvailable.value) {
return [];
@@ -5477,6 +5513,35 @@ watch(
scheduleSimulatorRun("scope", 150);
}
);
// TRU-84: Dognvask machines are single-vehicle-type machines. When the
// selected lane resolves to a Dognvask machine, force the vehicle type filter
// to the traekker option and keep it in sync when the user switches lanes.
watch(
() => [filters.value.lane_id, pathEditorIsDognvaskMachine.value, pathEditorAllowedVehicleTypes.value],
() => {
if (!pathEditorIsDognvaskMachine.value) {
return;
}
const allowed = pathEditorAllowedVehicleTypes.value;
if (allowed.length === 0) {
if (filters.value.vehicle_type_id !== "") {
filters.value = { ...filters.value, vehicle_type_id: "" };
}
return;
}
const allowedIds = new Set(
allowed.map((row) => String(vehicleTypeFilterValue(row)))
);
const current = String(filters.value.vehicle_type_id ?? "");
if (!allowedIds.has(current)) {
filters.value = {
...filters.value,
vehicle_type_id: String(vehicleTypeFilterValue(allowed[0])),
};
}
},
{ immediate: true }
);
watch(simulatorDynamicImageUrl, () => {
simulatorDynamicImageHidden.value = false;
simulatorDynamicImageError.value = "";
@@ -5682,10 +5747,19 @@ onBeforeUnmount(() => {
<section class="studio-scope-guide-column">
<span class="studio-scope-guide-step">2</span>
<h3>Vehicle type</h3>
<h3>
Vehicle type
<span
v-if="pathEditorIsDognvaskMachine"
class="tag is-info is-light is-small studio-scope-guide-constraint"
data-testid="studio-scope-guide-dognvask-hint"
>
Dognvask: trækker only
</span>
</h3>
<div class="studio-scope-option-list">
<button
v-for="vehicleType in lookupRows('vehicle_types')"
v-for="vehicleType in pathEditorAllowedVehicleTypes"
:key="vehicleType.id"
type="button"
class="studio-scope-option"
@@ -5702,7 +5776,7 @@ onBeforeUnmount(() => {
<small>#{{ vehicleTypeFilterValue(vehicleType) }}</small>
</span>
</button>
<p v-if="lookupRows('vehicle_types').length === 0" class="studio-empty-inline">
<p v-if="pathEditorAllowedVehicleTypes.length === 0" class="studio-empty-inline">
No vehicle types are configured.
</p>
</div>
@@ -7194,13 +7268,20 @@ onBeforeUnmount(() => {
<select v-model="filters.vehicle_type_id" data-testid="studio-filter-vehicle-type">
<option value="" disabled>Select vehicle type</option>
<option
v-for="vehicleType in lookupRows('vehicle_types')"
v-for="vehicleType in pathEditorAllowedVehicleTypes"
:key="vehicleType.id"
:value="vehicleTypeFilterValue(vehicleType)"
>
{{ vehicleType.label }}
</option>
</select>
<span
v-if="pathEditorIsDognvaskMachine"
class="tag is-info is-light is-small studio-scope-dognvask-hint"
data-testid="studio-filter-dognvask-hint"
>
Dognvask: trækker only
</span>
</div>
<div class="select is-small">
<select v-model="filters.machine_type_id" data-testid="studio-filter-machine-type">
@@ -0,0 +1,81 @@
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
describe("self-serve studio Dognvask single vehicle type (TRU-84)", () => {
const studioSource = () =>
readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
it("defines a Dognvask detection helper tied to the current lane machine type", () => {
const source = studioSource();
expect(source).toContain("pathEditorCurrentMachineTypeName");
expect(source).toContain("pathEditorIsDognvaskMachine");
expect(source).toContain('pathEditorCurrentMachineTypeName.value === "dognvask"');
});
it("provides a path editor allowed vehicle types list that filters to traekker for Dognvask", () => {
const source = studioSource();
expect(source).toContain("pathEditorAllowedVehicleTypes");
expect(source).toContain("isTraekkerVehicleType");
expect(source).toMatch(
/pathEditorAllowedVehicleTypes[\s\S]{0,400}if\s*\(\s*!pathEditorIsDognvaskMachine\.value\s*\)\s*\{\s*return\s+rows;\s*\}/
);
expect(source).toMatch(
/pathEditorAllowedVehicleTypes[\s\S]{0,800}const\s+traekker\s*=\s*rows\.filter\(\(row\)\s*=>\s*isTraekkerVehicleType\(row\)\);/
);
});
it("recognises traekker/tractor/tractor unit as the Dognvask vehicle type", () => {
const source = studioSource();
expect(source).toMatch(
/isTraekkerVehicleType[\s\S]{0,400}return\s+candidate\s*===\s*"traekker"\s*\|\|\s*candidate\s*===\s*"tractor"\s*\|\|\s*candidate\s*===\s*"tractor unit";/
);
});
it("uses the allowed vehicle types list in the toolbar filter", () => {
const source = studioSource();
// Toolbar select sits above the v-for option; assert the data-testid and the v-for coexist.
const toolbarIndex = source.indexOf('data-testid="studio-filter-vehicle-type"');
expect(toolbarIndex).toBeGreaterThanOrEqual(0);
const toolbarSection = source.slice(toolbarIndex, toolbarIndex + 400);
expect(toolbarSection).toContain("v-for=\"vehicleType in pathEditorAllowedVehicleTypes\"");
});
it("uses the allowed vehicle types list in the scope guide", () => {
const source = studioSource();
// Scope guide uses :data-testid="`studio-scope-option-vehicle-${...}`" with v-for before it.
const scopeGuideIndex = source.indexOf("studio-scope-option-vehicle-${");
expect(scopeGuideIndex).toBeGreaterThanOrEqual(0);
const scopeGuideSection = source.slice(Math.max(0, scopeGuideIndex - 800), scopeGuideIndex);
expect(scopeGuideSection).toContain("v-for=\"vehicleType in pathEditorAllowedVehicleTypes\"");
});
it("auto-corrects the vehicle type filter when the current machine is Dognvask", () => {
const source = studioSource();
expect(source).toContain(
"() => [filters.value.lane_id, pathEditorIsDognvaskMachine.value, pathEditorAllowedVehicleTypes.value]"
);
expect(source).toMatch(
/if\s*\(\s*!pathEditorIsDognvaskMachine\.value\s*\)\s*\{\s*return;\s*\}/
);
expect(source).toContain('filters.value = { ...filters.value, vehicle_type_id: "" }');
expect(source).toMatch(
/filters\.value\s*=\s*\{[\s\S]{0,200}vehicle_type_id:\s*String\(vehicleTypeFilterValue\(allowed\[0\]\)\)/
);
});
it("shows a visible Dognvask hint badge in the toolbar and scope guide", () => {
const source = studioSource();
expect(source).toContain('data-testid="studio-filter-dognvask-hint"');
expect(source).toContain('data-testid="studio-scope-guide-dognvask-hint"');
});
});