Add task priority, gate type normalization, and vehicle type logic to self-serve functionalities:
- Introduced task order priority, dynamic image thumb position, and improved task gate type handling in the self-serve studio. - Enhanced task payload normalization with nullable fields and parameter verification. - Updated UI with order priority controls, dynamic images configuration, and machine type-specific scoping options. - Added and refined unit tests for task sorting, form controls, and validation rules.
This commit is contained in:
@@ -49,19 +49,41 @@ const buildScopedTaskName = async (task) => {
|
||||
return `${departmentName}-${task.lane}-${productName}: ${task.task}`;
|
||||
};
|
||||
|
||||
const normalizePayload = (payload) => ({
|
||||
department: parseInt(payload.department || 0),
|
||||
lane: parseInt(payload.lane || 0),
|
||||
product: parseInt(payload.product || 0),
|
||||
machine_type_id: payload.machine_type_id && parseInt(payload.machine_type_id) !== 0 ? parseInt(payload.machine_type_id) : null,
|
||||
condition_id: payload.condition_id && parseInt(payload.condition_id) !== 0 ? parseInt(payload.condition_id) : null,
|
||||
task: payload.task,
|
||||
description: payload.description,
|
||||
order_priority: parseInt(payload.order_priority || 0),
|
||||
services: Array.isArray(payload.services) ? payload.services : [],
|
||||
buttons: Array.isArray(payload.buttons) ? payload.buttons.map((button) => parseInt(button)) : [],
|
||||
dynamic_images_vehicle_type: payload.dynamic_images_vehicle_type ? parseInt(payload.dynamic_images_vehicle_type) : null,
|
||||
});
|
||||
const parseIntOrZero = (value) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
};
|
||||
|
||||
const parseNullablePositiveInt = (value) => {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
return Number.isNaN(parsed) || parsed <= 0 ? null : parsed;
|
||||
};
|
||||
|
||||
const normalizePayload = (payload = {}) => {
|
||||
const gateTypeCandidate = String(payload.gate_type || "").toUpperCase();
|
||||
const normalizedGateType = ["ALWAYS", "CONDITION", "QUESTION"].includes(gateTypeCandidate)
|
||||
? gateTypeCandidate
|
||||
: (parseNullablePositiveInt(payload.gate_ref_id ?? payload.condition_id) ? "CONDITION" : "ALWAYS");
|
||||
const normalizedGateRefId = normalizedGateType === "ALWAYS"
|
||||
? null
|
||||
: parseNullablePositiveInt(payload.gate_ref_id ?? payload.condition_id);
|
||||
|
||||
return {
|
||||
department: parseIntOrZero(payload.department),
|
||||
lane: parseIntOrZero(payload.lane),
|
||||
product: parseIntOrZero(payload.product),
|
||||
machine_type_id: parseNullablePositiveInt(payload.machine_type_id),
|
||||
condition_id: normalizedGateRefId,
|
||||
gate_type: normalizedGateType,
|
||||
gate_ref_id: normalizedGateRefId,
|
||||
task: payload.task,
|
||||
description: payload.description,
|
||||
order_priority: parseIntOrZero(payload.order_priority),
|
||||
services: Array.isArray(payload.services) ? payload.services : [],
|
||||
buttons: Array.isArray(payload.buttons) ? payload.buttons.map((button) => parseIntOrZero(button)) : [],
|
||||
dynamic_images_vehicle_type: parseNullablePositiveInt(payload.dynamic_images_vehicle_type),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The SelfServeTasks object
|
||||
|
||||
@@ -22,6 +22,11 @@ const parseIntOrZero = (value) => {
|
||||
return Number.isNaN(parsed) ? 0 : parsed;
|
||||
};
|
||||
|
||||
const parseNullablePositiveInt = (value, fallback = null) => {
|
||||
const parsed = parseNullableInt(value, fallback);
|
||||
return parsed && parsed > 0 ? parsed : fallback;
|
||||
};
|
||||
|
||||
const buildOptionHtml = (options, selectedValue = null) =>
|
||||
options.map((option) => {
|
||||
const optionValue = option?.id ?? "";
|
||||
@@ -376,11 +381,14 @@ export const showSelfServeTaskForm = async ({ object, record = null, initialData
|
||||
await updateConditionOptions();
|
||||
},
|
||||
preConfirm: async () => {
|
||||
const selectedConditionId = parseNullablePositiveInt(getFieldValue("condition_id"));
|
||||
const payload = {
|
||||
task: getFieldValue("task").trim(),
|
||||
description: getFieldValue("description").trim(),
|
||||
order_priority: parseIntOrZero(getFieldValue("order_priority")),
|
||||
condition_id: parseNullableInt(getFieldValue("condition_id")),
|
||||
condition_id: selectedConditionId,
|
||||
gate_type: selectedConditionId ? "CONDITION" : "ALWAYS",
|
||||
gate_ref_id: selectedConditionId,
|
||||
services: getCheckedValues("services"),
|
||||
buttons: getCheckedValues("buttons").map((value) => parseIntOrZero(value)),
|
||||
dynamic_images_vehicle_type: parseNullableInt(getFieldValue("dynamic_images_vehicle_type")),
|
||||
|
||||
@@ -61,6 +61,19 @@ export function useWashSessionActions(options) {
|
||||
return totalButtonsCompleted;
|
||||
};
|
||||
|
||||
const getThumbPosition = () => {
|
||||
// Get the position of the thumb based on the session state
|
||||
let defaultThumbPosition = 0;
|
||||
// Check if any of the tasks has the "thumb_position" property and use it as the default position
|
||||
for (const task of activeTasks.value) {
|
||||
if (task.dynamic_images_vehicle_type !== null) {
|
||||
defaultThumbPosition = task.dynamic_images_vehicle_type;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return defaultThumbPosition;
|
||||
};
|
||||
|
||||
const dynamicImageUrl = computed(() => {
|
||||
const departmentId = nearestDepartment.value?.id;
|
||||
const laneId = washLaneId.value;
|
||||
@@ -70,7 +83,7 @@ export function useWashSessionActions(options) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${API_URL}/department/lanes/dynamic-image?department=${departmentId}&lane=${laneId}¤t_step=${machineStartCurrentStep.value}&buttons=${JSON.stringify(getButtonsToPress())}&vehicle_type=${vehicleTypeSelect.value}`;
|
||||
return `${API_URL}/department/lanes/dynamic-image?department=${departmentId}&lane=${laneId}¤t_step=${machineStartCurrentStep.value}&buttons=${JSON.stringify(getButtonsToPress())}&vehicle_type=${vehicleTypeSelect.value}&thumb_position=${getThumbPosition()}`;
|
||||
});
|
||||
|
||||
const executeSelfServeCommand = async (laneId, command, args = {
|
||||
|
||||
+136
-38
@@ -33,6 +33,52 @@ const parseScopedProduct = (value) => {
|
||||
return parsed && parsed > 0 ? parsed : null;
|
||||
};
|
||||
|
||||
const compareByOrderPriority = (left, right) => {
|
||||
const priorityDiff = parseIntOrZero(left?.order_priority) - parseIntOrZero(right?.order_priority);
|
||||
if (priorityDiff !== 0) {
|
||||
return priorityDiff;
|
||||
}
|
||||
return parseIntOrZero(left?.id) - parseIntOrZero(right?.id);
|
||||
};
|
||||
|
||||
const nextOrderPriority = (items) => (
|
||||
(Array.isArray(items) ? items : []).reduce((highest, entry) => (
|
||||
Math.max(highest, parseIntOrZero(entry?.order_priority))
|
||||
), 0) + 1
|
||||
);
|
||||
|
||||
const TASK_GATE_TYPES = ["ALWAYS", "CONDITION", "QUESTION"];
|
||||
const TASK_SERVICE_OPTIONS = ["MACHINE"];
|
||||
const TASK_BUTTON_MIN = 0;
|
||||
const TASK_BUTTON_MAX = 11;
|
||||
|
||||
const normalizeTaskGateType = (value) => {
|
||||
const gateType = String(value || "ALWAYS").toUpperCase();
|
||||
return TASK_GATE_TYPES.includes(gateType) ? gateType : "ALWAYS";
|
||||
};
|
||||
|
||||
const normalizeTaskServices = (value) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value
|
||||
.map((entry) => String(entry || "").toUpperCase())
|
||||
.filter((entry, index, source) => TASK_SERVICE_OPTIONS.includes(entry) && source.indexOf(entry) === index);
|
||||
};
|
||||
|
||||
const normalizeTaskButtons = (value) => {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalized = value
|
||||
.map((entry) => parseIntOrZero(entry))
|
||||
.filter((entry) => entry >= TASK_BUTTON_MIN && entry <= TASK_BUTTON_MAX);
|
||||
|
||||
return [...new Set(normalized)].sort((a, b) => a - b);
|
||||
};
|
||||
|
||||
const ensureArray = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
@@ -150,7 +196,7 @@ const ruleForm = ref({
|
||||
const taskForm = ref({
|
||||
id: null,
|
||||
scope_mode: "legacy",
|
||||
machine_type_id: null,
|
||||
machine_type_id: 0,
|
||||
department: 0,
|
||||
lane: 0,
|
||||
product: 0,
|
||||
@@ -202,6 +248,20 @@ const vehicleTypeOptions = computed(() => {
|
||||
return [...options].sort((a, b) => String(a.name || "").localeCompare(String(b.name || "")));
|
||||
});
|
||||
|
||||
const machineScopePrimaryVehicleTypeOptions = computed(() => {
|
||||
const options = vehicleTypeOptions.value.map((entry) => ({
|
||||
id: parseIntOrZero(entry.id),
|
||||
name: `${entry.name} (#${parseIntOrZero(entry.id)})`,
|
||||
}));
|
||||
|
||||
const selectedId = parseIntOrZero(taskForm.value.machine_type_id);
|
||||
if (selectedId > 0 && !options.some((entry) => entry.id === selectedId)) {
|
||||
options.unshift({ id: selectedId, name: `#${selectedId}` });
|
||||
}
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
const vehicleTypeName = (productId) => {
|
||||
const found = vehicleTypeOptions.value.find((entry) => parseInt(entry.id) === parseInt(productId));
|
||||
return found?.name || `#${productId}`;
|
||||
@@ -302,7 +362,10 @@ const loadStudioData = async () => {
|
||||
questions.value = ensureArray(questionList).filter((entry) => [0, departmentId.value].includes(parseIntOrZero(entry.department)));
|
||||
conditions.value = ensureArray(conditionList).filter((entry) => [0, departmentId.value].includes(parseIntOrZero(entry.department)));
|
||||
rules.value = ensureArray(ruleList);
|
||||
tasks.value = ensureArray(taskList).filter((entry) => [0, departmentId.value].includes(parseIntOrZero(entry.department)));
|
||||
tasks.value = ensureArray(taskList)
|
||||
.filter((entry) => [0, departmentId.value].includes(parseIntOrZero(entry.department)))
|
||||
.sort(compareByOrderPriority);
|
||||
questions.value = [...questions.value].sort(compareByOrderPriority);
|
||||
machineTypes.value = ensureArray(machineTypeList);
|
||||
products.value = ensureArray(productList);
|
||||
lanes.value = ensureArray(laneList).filter((entry) => parseIntOrZero(entry.department) === departmentId.value);
|
||||
@@ -378,6 +441,7 @@ const openQuestionModal = (record = null) => {
|
||||
? {
|
||||
...record,
|
||||
scope_mode: parseIntOrZero(record.department) === 0 && parseIntOrZero(record.lane) === 0 && parseIntOrZero(record.product) === 0 ? "shared" : "legacy",
|
||||
order_priority: parseIntOrZero(record.order_priority),
|
||||
}
|
||||
: {
|
||||
id: null,
|
||||
@@ -388,7 +452,7 @@ const openQuestionModal = (record = null) => {
|
||||
question: "",
|
||||
description: "",
|
||||
condition_id: null,
|
||||
order_priority: questions.value.length + 1,
|
||||
order_priority: nextOrderPriority(questions.value),
|
||||
};
|
||||
showQuestionModal.value = true;
|
||||
};
|
||||
@@ -453,7 +517,7 @@ const openConditionModal = (record = null) => {
|
||||
: {
|
||||
id: null,
|
||||
scope_mode: scopedMachineTypeId.value ? "machine_type" : "legacy",
|
||||
machine_type_id: scopedMachineTypeId.value,
|
||||
machine_type_id: scopedProductId.value || scopedMachineTypeId.value || 0,
|
||||
department: departmentId.value,
|
||||
lane: scopedLaneId.value || 0,
|
||||
product: scopedProductId.value || 0,
|
||||
@@ -563,16 +627,19 @@ const openTaskModal = (record = null) => {
|
||||
taskForm.value = record
|
||||
? {
|
||||
...record,
|
||||
machine_type_id: parseIntOrZero(record.machine_type_id),
|
||||
scope_mode: parseNullableInt(record.machine_type_id) ? "machine_type" : "legacy",
|
||||
gate_type: String(record.gate_type || "ALWAYS").toUpperCase(),
|
||||
gate_type: normalizeTaskGateType(record.gate_type || (parseNullableInt(record.gate_ref_id ?? record.condition_id) ? "CONDITION" : "ALWAYS")),
|
||||
gate_ref_id: parseNullableInt(record.gate_ref_id ?? record.condition_id),
|
||||
services: Array.isArray(record.services) ? [...record.services] : [],
|
||||
buttons: Array.isArray(record.buttons) ? [...record.buttons] : [],
|
||||
order_priority: parseIntOrZero(record.order_priority),
|
||||
services: normalizeTaskServices(record.services),
|
||||
buttons: normalizeTaskButtons(record.buttons),
|
||||
dynamic_images_vehicle_type: parseNullableInt(record.dynamic_images_vehicle_type),
|
||||
}
|
||||
: {
|
||||
id: null,
|
||||
scope_mode: scopedMachineTypeId.value ? "machine_type" : "legacy",
|
||||
machine_type_id: scopedMachineTypeId.value,
|
||||
scope_mode: "legacy",
|
||||
machine_type_id: scopedProductId.value || 0,
|
||||
department: departmentId.value,
|
||||
lane: scopedLaneId.value || 0,
|
||||
product: scopedProductId.value || 0,
|
||||
@@ -580,7 +647,7 @@ const openTaskModal = (record = null) => {
|
||||
gate_ref_id: null,
|
||||
task: "",
|
||||
description: "",
|
||||
order_priority: tasks.value.length + 1,
|
||||
order_priority: nextOrderPriority(tasks.value),
|
||||
services: [],
|
||||
buttons: [],
|
||||
dynamic_images_vehicle_type: null,
|
||||
@@ -589,20 +656,31 @@ const openTaskModal = (record = null) => {
|
||||
};
|
||||
|
||||
const saveTask = async () => {
|
||||
const machineScopePrimaryVehicleTypeProductId = taskForm.value.scope_mode === "machine_type"
|
||||
? parseIntOrZero(taskForm.value.machine_type_id)
|
||||
: 0;
|
||||
const gateType = normalizeTaskGateType(taskForm.value.gate_type);
|
||||
const resolvedDepartmentId = parseIntOrZero(taskForm.value.department || departmentId.value);
|
||||
const resolvedLaneId = parseIntOrZero(taskForm.value.lane);
|
||||
|
||||
const payload = {
|
||||
task: String(taskForm.value.task || "").trim(),
|
||||
description: String(taskForm.value.description || "").trim(),
|
||||
order_priority: parseIntOrZero(taskForm.value.order_priority),
|
||||
services: Array.isArray(taskForm.value.services) ? taskForm.value.services : [],
|
||||
buttons: Array.isArray(taskForm.value.buttons) ? taskForm.value.buttons.map((entry) => parseIntOrZero(entry)) : [],
|
||||
services: normalizeTaskServices(taskForm.value.services),
|
||||
buttons: normalizeTaskButtons(taskForm.value.buttons),
|
||||
dynamic_images_vehicle_type: parseNullableInt(taskForm.value.dynamic_images_vehicle_type),
|
||||
gate_type: String(taskForm.value.gate_type || "ALWAYS").toUpperCase(),
|
||||
gate_type: gateType,
|
||||
gate_ref_id: null,
|
||||
condition_id: null,
|
||||
machine_type_id: taskForm.value.scope_mode === "machine_type" ? parseNullableInt(taskForm.value.machine_type_id) : null,
|
||||
department: taskForm.value.scope_mode === "machine_type" ? 0 : parseIntOrZero(taskForm.value.department || departmentId.value),
|
||||
lane: taskForm.value.scope_mode === "machine_type" ? 0 : parseIntOrZero(taskForm.value.lane),
|
||||
product: taskForm.value.scope_mode === "machine_type" ? 0 : parseIntOrZero(taskForm.value.product),
|
||||
machine_type_id: taskForm.value.scope_mode === "machine_type"
|
||||
? (machineScopePrimaryVehicleTypeProductId > 0 ? machineScopePrimaryVehicleTypeProductId : null)
|
||||
: null,
|
||||
department: resolvedDepartmentId,
|
||||
lane: resolvedLaneId,
|
||||
product: taskForm.value.scope_mode === "machine_type"
|
||||
? machineScopePrimaryVehicleTypeProductId
|
||||
: parseIntOrZero(taskForm.value.product),
|
||||
};
|
||||
|
||||
if (!payload.task) {
|
||||
@@ -616,8 +694,8 @@ const saveTask = async () => {
|
||||
}
|
||||
|
||||
if (taskForm.value.scope_mode === "machine_type") {
|
||||
if (!payload.machine_type_id) {
|
||||
toast.error("Machine type is required for this scope.");
|
||||
if (!machineScopePrimaryVehicleTypeProductId) {
|
||||
toast.error("Primary vehicle type product id is required for this scope.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -641,7 +719,7 @@ const saveTask = async () => {
|
||||
toast.error("Gate reference is required for selected gate type.");
|
||||
return;
|
||||
}
|
||||
payload.condition_id = payload.gate_ref_id;
|
||||
payload.condition_id = payload.gate_type === "CONDITION" ? payload.gate_ref_id : null;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -1056,6 +1134,10 @@ onMounted(async () => {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Order Priority</label>
|
||||
<input v-model="questionForm.order_priority" class="input" type="number" min="0">
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-success" @click="saveQuestion">Save</button>
|
||||
@@ -1211,30 +1293,32 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="taskForm.scope_mode === 'machine_type'" class="field">
|
||||
<label class="label">Machine Type</label>
|
||||
<label class="label">Primary Vehicle Type Product ID</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="taskForm.machine_type_id">
|
||||
<option :value="null">Select machine type</option>
|
||||
<option v-for="entry in machineTypes" :key="entry.id" :value="parseInt(entry.id)">{{ entry.name }}</option>
|
||||
<option :value="0">Select primary vehicle type</option>
|
||||
<option v-for="entry in machineScopePrimaryVehicleTypeOptions" :key="entry.id" :value="parseInt(entry.id)">
|
||||
{{ entry.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Department</label>
|
||||
<input :value="departmentId" class="input" disabled>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Lane</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="taskForm.lane">
|
||||
<option :value="0">Select lane</option>
|
||||
<option v-for="entry in lanes" :key="entry.id" :value="parseInt(entry.id)">
|
||||
{{ entry.name }} ({{ entry.id }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="taskForm.scope_mode === 'legacy'">
|
||||
<div class="field">
|
||||
<label class="label">Department</label>
|
||||
<input :value="departmentId" class="input" disabled>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Lane</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="taskForm.lane">
|
||||
<option :value="0">Select lane</option>
|
||||
<option v-for="entry in lanes" :key="entry.id" :value="parseInt(entry.id)">
|
||||
{{ entry.name }} ({{ entry.id }})
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Vehicle Type</label>
|
||||
<div class="select is-fullwidth">
|
||||
@@ -1255,6 +1339,10 @@ onMounted(async () => {
|
||||
<label class="label">Description</label>
|
||||
<input v-model="taskForm.description" class="input">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Order Priority</label>
|
||||
<input v-model="taskForm.order_priority" class="input" type="number" min="0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Gate Type</label>
|
||||
<div class="select is-fullwidth">
|
||||
@@ -1300,6 +1388,16 @@ onMounted(async () => {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">Dynamic Images Thumb Position</label>
|
||||
<div class="select is-fullwidth">
|
||||
<select v-model="taskForm.dynamic_images_vehicle_type">
|
||||
<option :value="null">None</option>
|
||||
<!-- 1-12 -->
|
||||
<option v-for="index in 12" :key="index" :value="index - 1">{{ index - 1 }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button is-success" @click="saveTask">Save</button>
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
type RelayKind,
|
||||
} from "@/views/dashboards/superUserDashboard/selfserve/components/SelfServeMachineConnectivity.vue";
|
||||
import type { Machine } from "@/views/dashboards/superUserDashboard/selfserve/types/MachineType.vue";
|
||||
import {BTooltip} from "buefy";
|
||||
import {BSwitch, BTooltip} from "buefy";
|
||||
import MachineStatusType from "@/views/dashboards/superUserDashboard/selfserve/types/MachineStatusType.vue";
|
||||
|
||||
const props = defineProps<{ machine: Machine }>();
|
||||
|
||||
@@ -27,4 +27,12 @@ describe("self-serve studio question scope", () => {
|
||||
expect(studioSource).toContain("if (payload.lane <= 0)");
|
||||
expect(studioSource).toContain("if (payload.product <= 0)");
|
||||
});
|
||||
|
||||
it("renders and submits question order priority", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("<label class=\"label\">Order Priority</label>");
|
||||
expect(studioSource).toContain("v-model=\"questionForm.order_priority\"");
|
||||
expect(studioSource).toContain("order_priority: parseIntOrZero(questionForm.value.order_priority)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,10 +5,9 @@ import { describe, expect, it } from "vitest";
|
||||
const readSource = (relativePath) => readFileSync(join(process.cwd(), relativePath), "utf8");
|
||||
|
||||
describe("self-serve studio task scope", () => {
|
||||
it("renders legacy scope selectors for task lane and vehicle", () => {
|
||||
it("renders task selectors for lane and vehicle", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("v-if=\"taskForm.scope_mode === 'legacy'\"");
|
||||
expect(studioSource).toContain("v-model=\"taskForm.lane\"");
|
||||
expect(studioSource).toContain("v-model=\"taskForm.product\"");
|
||||
expect(studioSource).toContain("Select lane");
|
||||
@@ -16,12 +15,39 @@ describe("self-serve studio task scope", () => {
|
||||
expect(studioSource).toContain("v-for=\"entry in lanes\"");
|
||||
});
|
||||
|
||||
it("keeps department and lane visible for machine-type task scope", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("v-if=\"taskForm.scope_mode === 'machine_type'\" class=\"field\"");
|
||||
expect(studioSource).toContain("<label class=\"label\">Department</label>");
|
||||
expect(studioSource).toContain("<select v-model=\"taskForm.lane\">");
|
||||
expect(studioSource).toContain("<template v-if=\"taskForm.scope_mode === 'legacy'\">");
|
||||
expect(studioSource).toContain("<label class=\"label\">Vehicle Type</label>");
|
||||
});
|
||||
|
||||
it("uses primary vehicle type product selection for machine scope", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("const machineScopePrimaryVehicleTypeOptions = computed(() => {");
|
||||
expect(studioSource).toContain("Primary Vehicle Type Product ID");
|
||||
expect(studioSource).toContain("v-for=\"entry in machineScopePrimaryVehicleTypeOptions\"");
|
||||
expect(studioSource).toContain("machineScopePrimaryVehicleTypeProductId");
|
||||
expect(studioSource).toContain("product: taskForm.value.scope_mode === \"machine_type\"");
|
||||
});
|
||||
|
||||
it("defaults new task modal to legacy scope so department and lane are immediately visible", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("scope_mode: \"legacy\"");
|
||||
expect(studioSource).toContain("machine_type_id: scopedProductId.value || 0");
|
||||
});
|
||||
|
||||
it("guards task submission for machine-type and legacy scope requirements", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("Task name is required.");
|
||||
expect(studioSource).toContain("Task description is required.");
|
||||
expect(studioSource).toContain("Machine type is required for this scope.");
|
||||
expect(studioSource).toContain("Primary vehicle type product id is required for this scope.");
|
||||
expect(studioSource).toContain("Department is required for Department/Lane/Vehicle scope.");
|
||||
expect(studioSource).toContain("Lane is required for Department/Lane/Vehicle scope.");
|
||||
expect(studioSource).toContain("Vehicle type is required for Department/Lane/Vehicle scope.");
|
||||
@@ -35,5 +61,36 @@ describe("self-serve studio task scope", () => {
|
||||
expect(studioSource).toContain("if (payload.gate_type !== \"ALWAYS\")");
|
||||
expect(studioSource).toContain("if (!payload.gate_ref_id)");
|
||||
expect(studioSource).toContain("Gate reference is required for selected gate type.");
|
||||
expect(studioSource).toContain("payload.condition_id = payload.gate_type === \"CONDITION\" ? payload.gate_ref_id : null;");
|
||||
});
|
||||
|
||||
it("includes all task payload parameters in saveTask submission", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("const payload = {");
|
||||
expect(studioSource).toContain("task: String(taskForm.value.task || \"\").trim()");
|
||||
expect(studioSource).toContain("description: String(taskForm.value.description || \"\").trim()");
|
||||
expect(studioSource).toContain("order_priority: parseIntOrZero(taskForm.value.order_priority)");
|
||||
expect(studioSource).toContain("services: normalizeTaskServices(taskForm.value.services)");
|
||||
expect(studioSource).toContain("buttons: normalizeTaskButtons(taskForm.value.buttons)");
|
||||
expect(studioSource).toContain("dynamic_images_vehicle_type: parseNullableInt(taskForm.value.dynamic_images_vehicle_type)");
|
||||
expect(studioSource).toContain("gate_type: gateType");
|
||||
expect(studioSource).toContain("gate_ref_id: null");
|
||||
expect(studioSource).toContain("condition_id: null");
|
||||
expect(studioSource).toContain("machine_type_id: taskForm.value.scope_mode === \"machine_type\"");
|
||||
expect(studioSource).toContain("const resolvedDepartmentId = parseIntOrZero(taskForm.value.department || departmentId.value);");
|
||||
expect(studioSource).toContain("const resolvedLaneId = parseIntOrZero(taskForm.value.lane);");
|
||||
expect(studioSource).toContain("department: resolvedDepartmentId");
|
||||
expect(studioSource).toContain("lane: resolvedLaneId");
|
||||
expect(studioSource).toContain("product: taskForm.value.scope_mode === \"machine_type\"");
|
||||
});
|
||||
|
||||
it("renders task controls for order priority and dynamic images vehicle type", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("<label class=\"label\">Order Priority</label>");
|
||||
expect(studioSource).toContain("v-model=\"taskForm.order_priority\"");
|
||||
expect(studioSource).toContain("<label class=\"label\">Dynamic Images Thumb Position</label>");
|
||||
expect(studioSource).toContain("v-model=\"taskForm.dynamic_images_vehicle_type\"");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,4 +23,12 @@ describe("self-serve studio vehicle type scope", () => {
|
||||
expect(studioSource).toContain("const scopedTasks = computed(() => tasks.value.filter((entry) => matchesScopedProduct(entry)));");
|
||||
expect(studioSource).toContain("product: scopedProductId.value || 0,");
|
||||
});
|
||||
|
||||
it("sorts scoped question and task source lists by order priority", () => {
|
||||
const studioSource = readSource("src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue");
|
||||
|
||||
expect(studioSource).toContain("const compareByOrderPriority = (left, right) => {");
|
||||
expect(studioSource).toContain(".sort(compareByOrderPriority);");
|
||||
expect(studioSource).toContain("questions.value = [...questions.value].sort(compareByOrderPriority);");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
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 tasks payload contract", () => {
|
||||
it("maps task form condition gating to gate_type, gate_ref_id and condition_id", () => {
|
||||
const formSource = readSource("src/components/session/token/SessionUser/Objects/selfServeObjectForms.js");
|
||||
|
||||
expect(formSource).toContain("const selectedConditionId = parseNullablePositiveInt(getFieldValue(\"condition_id\"));");
|
||||
expect(formSource).toContain("condition_id: selectedConditionId");
|
||||
expect(formSource).toContain("gate_type: selectedConditionId ? \"CONDITION\" : \"ALWAYS\"");
|
||||
expect(formSource).toContain("gate_ref_id: selectedConditionId");
|
||||
});
|
||||
|
||||
it("normalizes task payloads with explicit gate and nullable fields", () => {
|
||||
const tasksSource = readSource("src/components/session/token/SessionUser/Objects/SelfServeTasks.vue");
|
||||
|
||||
expect(tasksSource).toContain("const normalizePayload = (payload = {}) => {");
|
||||
expect(tasksSource).toContain("condition_id: normalizedGateRefId");
|
||||
expect(tasksSource).toContain("gate_type: normalizedGateType");
|
||||
expect(tasksSource).toContain("gate_ref_id: normalizedGateRefId");
|
||||
expect(tasksSource).toContain("dynamic_images_vehicle_type: parseNullablePositiveInt(payload.dynamic_images_vehicle_type)");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user