Add relay machine controls and program picker integration:

- Added `relay_machine_program_picker_id` and `relay_machine_cleaner_id` fields across components, views, and localization files.
- Updated relay control logic to include program picker and cleaner relays.
- Extended e2e and unit tests to validate new relay functionalities.
- Refactored related components to handle expanded relay configurations.
This commit is contained in:
Jeppe Bundgaard
2026-03-25 11:42:02 +01:00
parent c6531076ae
commit 97bcf73db8
16 changed files with 188 additions and 14 deletions
@@ -55,6 +55,8 @@ watch(() => props.objects, () => {
<th>{{ SessionUser.objects.department_lanes.columns.relay_in_id.label }}</th>
<th>{{ SessionUser.objects.department_lanes.columns.relay_out_id.label }}</th>
<th>{{ SessionUser.objects.department_lanes.columns.relay_machine_id.label }}</th>
<th>{{ SessionUser.objects.department_lanes.columns.relay_machine_program_picker_id.label }}</th>
<th>{{ SessionUser.objects.department_lanes.columns.relay_machine_cleaner_id.label }}</th>
<th>{{ SessionUser.objects.department_lanes.columns.dynamic_image_id.label }}</th>
<th>{{ SessionUser.objects.department_lanes.columns.machine_type_id.label }}</th>
</template>
@@ -116,6 +118,18 @@ watch(() => props.objects, () => {
column="relay_machine_id"
:edit-function="SessionUser.objects.department_lanes.showEditObjectFieldForm"
/>
<EditableTableColumn
:object="object"
:loadList="loadList"
column="relay_machine_program_picker_id"
:edit-function="SessionUser.objects.department_lanes.showEditObjectFieldForm"
/>
<EditableTableColumn
:object="object"
:loadList="loadList"
column="relay_machine_cleaner_id"
:edit-function="SessionUser.objects.department_lanes.showEditObjectFieldForm"
/>
<EditableTableColumn
:object="object"
:loadList="loadList"
@@ -75,6 +75,22 @@ const t = (key) => i18n.global.t(key);
required: false
}
},
relay_machine_program_picker_id: {
label: t('objects.columns.relay_machine_program_picker_id'),
type: "string",
sortable: true,
creation: {
required: false
}
},
relay_machine_cleaner_id: {
label: t('objects.columns.relay_machine_cleaner_id'),
type: "string",
sortable: true,
creation: {
required: false
}
},
dynamic_image_id: {
label: t('objects.columns.dynamic_image_id'),
type: "number",
@@ -103,7 +119,17 @@ const t = (key) => i18n.global.t(key);
}
};
},
add: async (department, name, relay_in_id = null, relay_out_id = null, relay_machine_id = null, dynamic_image_id = null, machine_type_id = null) => {
add: async (
department,
name,
relay_in_id = null,
relay_out_id = null,
relay_machine_id = null,
relay_machine_program_picker_id = null,
relay_machine_cleaner_id = null,
dynamic_image_id = null,
machine_type_id = null
) => {
return ObjectsGlobal.add.object(
DepartmentLanes.meta.endpoint,
{
@@ -112,6 +138,8 @@ const t = (key) => i18n.global.t(key);
relay_in_id: relay_in_id,
relay_out_id: relay_out_id,
relay_machine_id: relay_machine_id,
relay_machine_program_picker_id: relay_machine_program_picker_id,
relay_machine_cleaner_id: relay_machine_cleaner_id,
dynamic_image_id: dynamic_image_id ? parseInt(dynamic_image_id) : null,
machine_type_id: machine_type_id ? parseInt(machine_type_id) : null
}
@@ -158,6 +186,22 @@ const t = (key) => i18n.global.t(key);
relay_machine_id
)
},
relay_machine_program_picker_id: async (id, relay_machine_program_picker_id) => {
return ObjectsGlobal.set.column(
DepartmentLanes.meta.endpoint,
id,
"relay_machine_program_picker_id",
relay_machine_program_picker_id
)
},
relay_machine_cleaner_id: async (id, relay_machine_cleaner_id) => {
return ObjectsGlobal.set.column(
DepartmentLanes.meta.endpoint,
id,
"relay_machine_cleaner_id",
relay_machine_cleaner_id
)
},
dynamic_image_id: async (id, dynamic_image_id) => {
return ObjectsGlobal.set.column(
DepartmentLanes.meta.endpoint,
+2
View File
@@ -2206,6 +2206,8 @@
"reference": "Referanse",
"relay_in_id": "Indgangs relæ ID",
"relay_machine_id": "Maskine relæ ID",
"relay_machine_program_picker_id": "Machine program picker relay ID",
"relay_machine_cleaner_id": "Machine cleaner relay ID",
"relay_out_id": "Udgangs relæ ID",
"role_id": "Rolle ID",
"rule": "Regel",
+2
View File
@@ -2206,6 +2206,8 @@
"reference": "Referenz",
"relay_in_id": "Indgangs relæ ID",
"relay_machine_id": "Maskine relæ ID",
"relay_machine_program_picker_id": "Machine program picker relay ID",
"relay_machine_cleaner_id": "Machine cleaner relay ID",
"relay_out_id": "Udgangs relæ ID",
"role_id": "Rollen-ID",
"rule": "Regel",
+2
View File
@@ -2206,6 +2206,8 @@
"reference": "Reference",
"relay_in_id": "Input relay ID",
"relay_machine_id": "Machine relay ID",
"relay_machine_program_picker_id": "Machine program picker relay ID",
"relay_machine_cleaner_id": "Machine cleaner relay ID",
"relay_out_id": "Output relay ID",
"role_id": "Role ID",
"rule": "Rule",
+2
View File
@@ -2201,6 +2201,8 @@
"reference": "Referanse",
"relay_in_id": "Inndatarelé-ID",
"relay_machine_id": "Maskinrelé-ID",
"relay_machine_program_picker_id": "Machine program picker relay ID",
"relay_machine_cleaner_id": "Machine cleaner relay ID",
"relay_out_id": "Utgangsrelé-ID",
"role_id": "Rolle-ID",
"rule": "Regel",
+2
View File
@@ -2201,6 +2201,8 @@
"reference": "Referens",
"relay_in_id": "Ingångsrelä-ID",
"relay_machine_id": "Maskinrelä-ID",
"relay_machine_program_picker_id": "Machine program picker relay ID",
"relay_machine_cleaner_id": "Machine cleaner relay ID",
"relay_out_id": "Utgångsrelä-ID",
"role_id": "Role ID",
"rule": "Rule",
+2
View File
@@ -2092,6 +2092,8 @@
"reference": "Referens",
"relay_in_id": "Indgangs relæ ID",
"relay_machine_id": "Maskine relæ ID",
"relay_machine_program_picker_id": "Machine program picker relay ID",
"relay_machine_cleaner_id": "Machine cleaner relay ID",
"relay_out_id": "Udgangs relæ ID",
"role_id": "Role ID",
"rule": "Rule",
@@ -136,6 +136,28 @@ onMounted(() => {
</span>
</td>
</tr>
<tr>
<th>{{ SessionUser.objects.department_lanes.columns.relay_machine_program_picker_id.label }}</th>
<td>
<span class="icon" :class="{ 'has-text-warning': !lane.relay_machine_program_picker_id, 'has-text-success': lane.relay_machine_program_picker_id }">
<i :class="!lane.relay_machine_program_picker_id ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle'"></i>
</span>
<span>
{{ lane.relay_machine_program_picker_id || SessionUser.objects.global.language.not_configured }}
</span>
</td>
</tr>
<tr>
<th>{{ SessionUser.objects.department_lanes.columns.relay_machine_cleaner_id.label }}</th>
<td>
<span class="icon" :class="{ 'has-text-warning': !lane.relay_machine_cleaner_id, 'has-text-success': lane.relay_machine_cleaner_id }">
<i :class="!lane.relay_machine_cleaner_id ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle'"></i>
</span>
<span>
{{ lane.relay_machine_cleaner_id || SessionUser.objects.global.language.not_configured }}
</span>
</td>
</tr>
<tr>
<th>{{ SessionUser.objects.department_lanes.columns.dynamic_image_id.label }}</th>
<td>
@@ -172,6 +172,28 @@ onMounted(() => {
</span>
</td>
</tr>
<tr>
<th>{{ SessionUser.objects.department_lanes.columns.relay_machine_program_picker_id.label }}</th>
<td>
<span class="icon" :class="{ 'has-text-warning': !lane.relay_machine_program_picker_id, 'has-text-success': lane.relay_machine_program_picker_id }">
<i :class="!lane.relay_machine_program_picker_id ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle'"></i>
</span>
<span>
{{ lane.relay_machine_program_picker_id || SessionUser.objects.global.language.not_configured }}
</span>
</td>
</tr>
<tr>
<th>{{ SessionUser.objects.department_lanes.columns.relay_machine_cleaner_id.label }}</th>
<td>
<span class="icon" :class="{ 'has-text-warning': !lane.relay_machine_cleaner_id, 'has-text-success': lane.relay_machine_cleaner_id }">
<i :class="!lane.relay_machine_cleaner_id ? 'fas fa-exclamation-triangle' : 'fas fa-check-circle'"></i>
</span>
<span>
{{ lane.relay_machine_cleaner_id || SessionUser.objects.global.language.not_configured }}
</span>
</td>
</tr>
<tr>
<th>{{ SessionUser.objects.department_lanes.columns.dynamic_image_id.label }}</th>
<td>
@@ -54,7 +54,7 @@ export const relays = {
on: on
}
),
MACHINE_PROGRAM_PICKER: (laneId: number, on: boolean) => SessionUser.request(
PROGRAM_PICKER: (laneId: number, on: boolean) => SessionUser.request(
"/modules/self-serve/lane/relay/machine_program_picker/set",
"POST",
{
@@ -62,7 +62,7 @@ export const relays = {
on: on
}
),
MACHINE_CLEANER: (laneId: number, on: boolean) => SessionUser.request(
CLEANER: (laneId: number, on: boolean) => SessionUser.request(
"/modules/self-serve/lane/relay/machine_cleaner/set",
"POST",
{
@@ -1,30 +1,69 @@
<script setup lang="ts">
import {defineProps, defineEmits} from 'vue';
import {BButton} from "buefy";
import {defineProps, onMounted, ref} from 'vue';
import {BButton, BSwitch} from "buefy";
import { relays } from "@/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue";
import SelfServeMachineStatus
from "@/views/dashboards/superUserDashboard/selfserve/displays/machine/SelfServeMachineStatus.vue";
defineProps<{machine: any}>();
import type MachineType from "@/views/dashboards/superUserDashboard/selfserve/types/MachineType.vue";
const props = defineProps<{machine: MachineType}>();
const machineRelayStatus = ref(null);
const programPickerRelayStatus = ref(null);
const cleanerRelayStatus = ref(null);
// Get status on mount
onMounted(() => {
relays.get.status.MACHINE(props.machine.id).then((response) => {
/**
* {
* "lane_id": 3,
* "relay": "MACHINE",
* "relay_id": "e4b3231cce40",
* "online": true,
* "on": true
* }
*/
machineRelayStatus.value = response.data.data;
})
relays.get.status.PROGRAM_PICKER(props.machine.id).then((response) => {
programPickerRelayStatus.value = response.data.data;
})
relays.get.status.CLEANER(props.machine.id).then((response) => {
cleanerRelayStatus.value = response.data.data;
})
});
const parseStatus = (status: any) => {
if (!status) return "MAINTENANCE";
if (!status.online) return "OFFLINE";
return status.on ? "ON" : "OFF";
}
</script>
<template>
<div>
<p>Relay Controls</p>
<div class="columns is-multiline is-mobile">
<div class="column is-half">
<p>Machine Relay</p>
</div>
<div class="column is-half">
<SelfServeMachineStatus :machine-status="parseStatus(machineRelayStatus)" />
<b-button @click="relays.get.status.MACHINE(props.machine.id)" size="is-small" type="is-info">Get Status</b-button>
</div>
<div class="column is-half">
<p>Program Picker Relay</p>
</div>
<div class="column is-half">
<SelfServeMachineStatus machine-status="OFFLINE" />
<b-button @click="relays.get.status.PROGRAM_PICKER(machine.id)" size="is-small" type="is-info">Get Status</b-button>
<SelfServeMachineStatus :machine-status="parseStatus(programPickerRelayStatus)" />
<b-button @click="relays.get.status.PROGRAM_PICKER(props.machine.id)" size="is-small" type="is-info">Get Status</b-button>
</div>
<div class="column is-half">
<p>Cleaner Relay</p>
</div>
<div class="column is-half">
<SelfServeMachineStatus machine-status="OFFLINE" />
<b-button @click="relays.get.status.CLEANER(machine.id)" size="is-small" type="is-info">Get Status</b-button>
<SelfServeMachineStatus :machine-status="parseStatus(cleanerRelayStatus)" />
<b-button @click="relays.get.status.CLEANER(props.machine.id)" size="is-small" type="is-info">Get Status</b-button>
</div>
</div>
</div>
@@ -8,11 +8,15 @@ const icon = {
ONLINE: "check",
OFFLINE: "close",
MAINTENANCE: "tools",
ON: "check",
OFF: "close",
}
const type = {
ONLINE: "is-success",
OFFLINE: "is-danger",
MAINTENANCE: "is-warning",
ON: "is-success",
OFF: "is-danger",
}
</script>
@@ -3,6 +3,8 @@ const MachineStatus = {
ONLINE: "ONLINE",
OFFLINE: "OFFLINE",
MAINTENANCE: "MAINTENANCE",
ON: "ON",
OFF: "OFF",
} as const;
export type { MachineStatus };
export default MachineStatus;
+8
View File
@@ -71,6 +71,8 @@ function createSelfServeFixture(overrides = {}) {
relay_in_id: "IN-7",
relay_out_id: "OUT-7",
relay_machine_id: "M-7",
relay_machine_program_picker_id: "MPP-7",
relay_machine_cleaner_id: "MC-7",
dynamic_image_id: 77,
machine_type_id: null,
},
@@ -84,6 +86,8 @@ function createSelfServeFixture(overrides = {}) {
relay_in_id: "IN-8",
relay_out_id: "OUT-8",
relay_machine_id: "M-8",
relay_machine_program_picker_id: "MPP-8",
relay_machine_cleaner_id: "MC-8",
dynamic_image_id: 78,
machine_type_id: null,
},
@@ -107,6 +111,8 @@ function createSelfServeFixture(overrides = {}) {
relay_in_id: "IN-9",
relay_out_id: "OUT-9",
relay_machine_id: "M-9",
relay_machine_program_picker_id: "MPP-9",
relay_machine_cleaner_id: "MC-9",
dynamic_image_id: 79,
machine_type_id: null,
},
@@ -130,6 +136,8 @@ function createSelfServeFixture(overrides = {}) {
relay_in_id: "IN-10",
relay_out_id: "OUT-10",
relay_machine_id: "M-10",
relay_machine_program_picker_id: "MPP-10",
relay_machine_cleaner_id: "MC-10",
dynamic_image_id: 80,
machine_type_id: null,
},
+10 -3
View File
@@ -5,16 +5,23 @@ import { describe, expect, it } from "vitest";
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
describe("self-serve alignment", () => {
it("registers reusable machine types and lane machine_type_id wiring", () => {
it("registers reusable machine types and lane relay/machine_type wiring", () => {
const sessionUserSource = readSource("src/components/session/token/SessionUser.vue");
const laneSource = readSource("src/components/session/token/SessionUser/Objects/DepartmentLanes.vue");
const laneViewSource = readSource("src/views/dashboards/departmentDashboard/modules/wash-lanes/DepartmentWashLane.vue");
const laneAdminViewSource = readSource("src/views/dashboards/departmentDashboard/modules/wash-lanes/DepartmentWashLane.vue");
const laneSuperUserViewSource = readSource("src/views/dashboards/superUserDashboard/department/lanes/DepartmentLane.vue");
expect(sessionUserSource).toMatch(/self_serve_machine_types\s*:\s*SelfServeMachineTypes\s*,/);
expect(sessionUserSource).not.toMatch(/get\s+self_serve_machine_types\s*\(/);
expect(laneSource).toContain("relay_machine_program_picker_id:");
expect(laneSource).toContain("relay_machine_cleaner_id:");
expect(laneSource).toContain("machine_type_id:");
expect(laneSource).toContain("SessionUser.objects.self_serve_machine_types.get.all()");
expect(laneViewSource).toContain(":set-machine-type-filter=\"lane.machine_type_id || null\"");
expect(laneAdminViewSource).toContain(":set-machine-type-filter=\"lane.machine_type_id || null\"");
expect(laneAdminViewSource).toContain("lane.relay_machine_program_picker_id");
expect(laneAdminViewSource).toContain("lane.relay_machine_cleaner_id");
expect(laneSuperUserViewSource).toContain("lane.relay_machine_program_picker_id");
expect(laneSuperUserViewSource).toContain("lane.relay_machine_cleaner_id");
});
it("normalizes self-serve config reads and submits merged config payloads", () => {