diff --git a/openapi.yaml b/openapi.yaml index 0560b593..b67c74b1 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -4644,6 +4644,22 @@ paths: customer_id: type: integer nullable: true + vehicle_type: + type: integer + nullable: true + description: Optional product/vehicle type override used when refreshing the self-serve summary. + vehicle_type_id: + type: integer + nullable: true + description: Alias for vehicle_type. + activate_machine: + type: boolean + default: true + description: Whether the session synchronization may enable the machine relay. User wash-start saves answers with false. + sync_relay_state: + type: boolean + default: true + description: Whether the answer mutation should synchronize live relay state. responses: '200': description: Successfully added vehicle condition @@ -4688,6 +4704,22 @@ paths: customer_id: type: integer nullable: true + vehicle_type: + type: integer + nullable: true + description: Optional product/vehicle type override used when refreshing the self-serve summary. + vehicle_type_id: + type: integer + nullable: true + description: Alias for vehicle_type. + activate_machine: + type: boolean + default: true + description: Whether the session synchronization may enable the machine relay. + sync_relay_state: + type: boolean + default: true + description: Whether the mutation should synchronize live relay state. responses: '200': description: Successfully updated vehicle condition @@ -5274,13 +5306,34 @@ paths: reg: { type: string } customer_number: { type: integer, nullable: true } vehicle_type_id: { type: integer, nullable: true } + config_source: + type: string + enum: [draft, published] + default: draft + answer_overrides: + type: array + items: + type: object + required: [question_id] + properties: + question_id: { type: integer } + value: + type: boolean + nullable: true + include_hardware: + type: boolean + default: true + mode: + type: string + enum: [full_dry_run] + default: full_dry_run responses: '200': description: Simulator result content: application/json: schema: - $ref: '#/components/schemas/SelfserveVehicleAllowedResponse' + $ref: '#/components/schemas/SelfserveStudioSimulationResponse' /department/selfserve/studio/publish: post: @@ -15129,6 +15182,75 @@ components: type: string format: date-time + SelfserveStudioSimulationDebug: + type: object + required: [summary, parameters, stages, questions, conditions, rules, tasks, hardware, graph_annotations, recommendations] + properties: + summary: + type: object + additionalProperties: true + parameters: + type: object + additionalProperties: true + stages: + type: array + items: + type: object + additionalProperties: true + questions: + type: array + items: + type: object + additionalProperties: true + conditions: + type: array + items: + type: object + additionalProperties: true + rules: + type: array + items: + type: object + additionalProperties: true + tasks: + type: array + items: + type: object + additionalProperties: true + hardware: + type: object + additionalProperties: true + graph_annotations: + type: object + properties: + nodes: + type: object + additionalProperties: + type: object + additionalProperties: true + edges: + type: object + additionalProperties: + type: object + additionalProperties: true + recommendations: + type: array + items: + type: object + additionalProperties: true + + SelfserveStudioSimulationResponse: + allOf: + - $ref: '#/components/schemas/SelfserveVehicleAllowedResponse' + - type: object + properties: + simulator_version: { type: integer } + dry_run: { type: boolean, enum: [true] } + mode: { type: string, enum: [full_dry_run] } + config_source: { type: string, enum: [draft, published] } + debug: + $ref: '#/components/schemas/SelfserveStudioSimulationDebug' + SelfserveVehicleAllowedResponse: type: object properties: @@ -15172,6 +15294,13 @@ components: allOf: - $ref: '#/components/schemas/SelfserveWashSession' nullable: true + config_source: + type: string + nullable: true + evaluation_trace: + type: object + nullable: true + additionalProperties: true SelfserveWashSummary: type: object diff --git a/src/components/session/token/SessionUser/Objects/SelfServeVehicleConditions.vue b/src/components/session/token/SessionUser/Objects/SelfServeVehicleConditions.vue index 13d7c509..97745c28 100644 --- a/src/components/session/token/SessionUser/Objects/SelfServeVehicleConditions.vue +++ b/src/components/session/token/SessionUser/Objects/SelfServeVehicleConditions.vue @@ -126,14 +126,15 @@ export const SelfServeVehicleConditions = { } } }, - add: async (department, lane, customer_id, reg, question, value) => { + add: async (department, lane, customer_id, reg, question, value, options = {}) => { return ObjectsGlobal.add.object(SelfServeVehicleConditions.meta.endpoint, { department: parseInt(department), lane: parseInt(lane), customer_id: parseInt(customer_id), reg: reg, question: parseInt(question), - value: value === "true" || value === true + value: value === "true" || value === true, + ...options }); }, set: { diff --git a/src/composables/useSelfServeLogic.js b/src/composables/useSelfServeLogic.js index dd93ca8e..31b055c3 100644 --- a/src/composables/useSelfServeLogic.js +++ b/src/composables/useSelfServeLogic.js @@ -612,13 +612,25 @@ export function useSelfServeLogic() { loading.value = true; try { const normalizedReg = reg.trim().toUpperCase(); + const requestVehicleTypeCandidate = vehicleTypeId === undefined || vehicleTypeId === null + ? (lastVehicleTypeOverride.value ?? lastResolvedVehicleTypeId.value) + : vehicleTypeId; + const normalizedRequestVehicleTypeId = parseInt(requestVehicleTypeCandidate); + const mutationOptions = { + activate_machine: false, + sync_relay_state: false, + }; + if (!Number.isNaN(normalizedRequestVehicleTypeId) && normalizedRequestVehicleTypeId > 0) { + mutationOptions.vehicle_type = normalizedRequestVehicleTypeId; + } const response = await SessionUser.objects.self_serve_vehicle_conditions.add( departmentId, laneId, customerNumber || 0, normalizedReg, questionId, - value + value, + mutationOptions ); const payload = response?.data?.data || response?.data || response || {}; @@ -629,9 +641,7 @@ export function useSelfServeLogic() { [parseInt(questionId)]: value, }; - const refreshVehicleTypeCandidate = vehicleTypeId === undefined || vehicleTypeId === null - ? (lastVehicleTypeOverride.value ?? lastResolvedVehicleTypeId.value) - : vehicleTypeId; + const refreshVehicleTypeCandidate = requestVehicleTypeCandidate; const normalizedRefreshVehicleTypeId = parseInt(refreshVehicleTypeCandidate); const refreshVehicleTypeId = !Number.isNaN(normalizedRefreshVehicleTypeId) && normalizedRefreshVehicleTypeId > 0 ? normalizedRefreshVehicleTypeId diff --git a/src/composables/useWashSessionActions.js b/src/composables/useWashSessionActions.js index b0fa2f3f..4b843649 100644 --- a/src/composables/useWashSessionActions.js +++ b/src/composables/useWashSessionActions.js @@ -206,7 +206,13 @@ export function useWashSessionActions(options) { isStartingWash.value = true; try { - await updateLaneAllowedServices(laneId); + try { + await updateLaneAllowedServices(laneId); + } catch (error) { + console.error("Error updating allowed services before start:", error); + return false; + } + const startResponse = await executeSelfServeCommand(laneId, "START", { customer_number: parseInt(customerNumber), license_plate: licensePlate.trim().toUpperCase(), @@ -229,7 +235,11 @@ export function useWashSessionActions(options) { }); if (radioWashType.value === "Machine" && isServiceAllowed("MACHINE")) { - await enableMachineRelay(laneId); + try { + await enableMachineRelay(laneId); + } catch (error) { + console.error("Error enabling machine relay after wash start:", error); + } } return true; } catch (error) { diff --git a/src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue b/src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue index a1d77783..15cdbb8f 100644 --- a/src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue +++ b/src/views/dashboards/departmentDashboard/modules/self-serve/DepartmentSelfServeStudio.vue @@ -137,8 +137,12 @@ const simulatorForm = ref({ reg: "TEST123", customer_number: null, vehicle_type_id: null, + config_source: "draft", + include_hardware: true, }); const simulatorResult = ref(null); +const simulatorAnswerOverrides = ref({}); +const simulatorShowJson = ref(false); const searchTerm = ref(""); const filters = ref({ lane_id: "", @@ -169,6 +173,30 @@ const permissions = computed(() => graphPayload.value?.permissions || {}); const selectedNode = computed(() => flowNodes.value.find((node) => node.id === selectedNodeId.value) || null); const editableNodeKinds = ["question", "condition", "rule", "task"]; const isSelectedEditable = computed(() => editableNodeKinds.includes(selectedNode.value?.data?.kind)); +const simulatorDebug = computed(() => simulatorResult.value?.debug || null); +const simulatorSummary = computed(() => simulatorDebug.value?.summary || null); +const simulatorRecommendations = computed(() => Array.isArray(simulatorDebug.value?.recommendations) ? simulatorDebug.value.recommendations : []); +const simulatorStages = computed(() => Array.isArray(simulatorDebug.value?.stages) ? simulatorDebug.value.stages : []); +const simulatorQuestions = computed(() => { + if (Array.isArray(simulatorDebug.value?.questions) && simulatorDebug.value.questions.length > 0) { + return simulatorDebug.value.questions; + } + return flowNodes.value + .filter((node) => node.data?.kind === "question") + .map((node) => ({ + id: node.data?.object_id || node.data?.raw?.id, + node_id: node.id, + label: node.data?.label || node.data?.raw?.question || node.id, + state: "available", + answer: null, + answer_source: "missing", + })) + .filter((question) => question.id); +}); +const simulatorConditions = computed(() => Array.isArray(simulatorDebug.value?.conditions) ? simulatorDebug.value.conditions : []); +const simulatorRules = computed(() => Array.isArray(simulatorDebug.value?.rules) ? simulatorDebug.value.rules : []); +const simulatorTasks = computed(() => Array.isArray(simulatorDebug.value?.tasks) ? simulatorDebug.value.tasks : []); +const simulatorHardware = computed(() => simulatorDebug.value?.hardware || null); const lookupRows = (type) => (Array.isArray(lookups.value?.[type]) ? lookups.value[type] : []); const labelFor = (type, id) => { @@ -436,8 +464,26 @@ const computeAutoLayoutPositions = (nodes, options = {}) => { return positions; }; -const PRESENTATION_NODE_CLASSES = new Set(["studio-node-filtered", "studio-node-selected"]); -const PRESENTATION_EDGE_CLASSES = new Set(["studio-edge-filtered"]); +const SIMULATION_STATE_CLASSES = { + active: "studio-node-sim-active", + visited: "studio-node-sim-visited", + blocked: "studio-node-sim-blocked", + warning: "studio-node-sim-warning", + error: "studio-node-sim-error", + next: "studio-node-sim-next", + not_applicable: "studio-node-sim-muted", +}; +const SIMULATION_EDGE_CLASSES = { + active: "studio-edge-sim-active", + visited: "studio-edge-sim-visited", + blocked: "studio-edge-sim-blocked", + warning: "studio-edge-sim-warning", + error: "studio-edge-sim-error", + next: "studio-edge-sim-next", + not_applicable: "studio-edge-sim-muted", +}; +const PRESENTATION_NODE_CLASSES = new Set(["studio-node-filtered", "studio-node-selected", ...Object.values(SIMULATION_STATE_CLASSES)]); +const PRESENTATION_EDGE_CLASSES = new Set(["studio-edge-filtered", ...Object.values(SIMULATION_EDGE_CLASSES)]); const baseNodeClass = (className) => String(className || "") .split(/\s+/) .filter((entry) => entry && !PRESENTATION_NODE_CLASSES.has(entry)) @@ -447,6 +493,16 @@ const baseEdgeClass = (className) => String(className || "") .filter((entry) => entry && !PRESENTATION_EDGE_CLASSES.has(entry)) .join(" "); +const simulationNodeClass = (nodeId) => { + const state = simulatorDebug.value?.graph_annotations?.nodes?.[nodeId]?.state; + return SIMULATION_STATE_CLASSES[state] || ""; +}; + +const simulationEdgeClass = (edgeId) => { + const state = simulatorDebug.value?.graph_annotations?.edges?.[edgeId]?.state; + return SIMULATION_EDGE_CLASSES[state] || ""; +}; + const visibleCanvasNodeIds = computed(() => new Set(flowNodes.value.filter(matchesNodeFilters).map((node) => node.id))); const hasActiveCanvasFilters = computed(() => Boolean( searchTerm.value.trim() @@ -495,6 +551,7 @@ const applyCanvasFilters = () => { class: [ baseNodeClass(node.class), visibleIds.has(node.id) ? "" : "studio-node-filtered", + simulationNodeClass(node.id), ].filter(Boolean).join(" "), })); flowEdges.value = flowEdges.value.map((edge) => ({ @@ -502,6 +559,7 @@ const applyCanvasFilters = () => { class: [ baseEdgeClass(edge.class), visibleIds.has(edge.source) && visibleIds.has(edge.target) ? "" : "studio-edge-filtered", + simulationEdgeClass(edge.id), ].filter(Boolean).join(" "), })); nextTick(() => { @@ -735,7 +793,11 @@ const hydrateFromGraph = (payload) => { reg: payload?.simulator_defaults?.reg || "TEST123", customer_number: payload?.simulator_defaults?.customer_number || null, vehicle_type_id: payload?.simulator_defaults?.vehicle_type_id || null, + config_source: simulatorForm.value?.config_source || "draft", + include_hardware: simulatorForm.value?.include_hardware !== false, }; + simulatorResult.value = null; + simulatorAnswerOverrides.value = {}; suppressLayoutSave.value = true; unfilteredNodePositions.value = null; @@ -883,6 +945,26 @@ const selectNode = (nodeId) => { activePanel.value = "inspector"; }; +const focusDebugNodes = async (nodeIds = [], openInspector = false) => { + const ids = [...new Set((Array.isArray(nodeIds) ? nodeIds : []).filter(Boolean))]; + if (ids.length === 0) { + return; + } + selectedNodeId.value = ids[0]; + if (openInspector) { + activePanel.value = "inspector"; + } + await nextTick(); + fitView({ + nodes: ids, + includeHiddenNodes: false, + minZoom: 0.62, + maxZoom: 1.05, + padding: 0.2, + duration: 240, + }).catch(() => {}); +}; + const refreshInspectorForm = () => { const raw = selectedRaw.value || {}; inspectorForm.value = JSON.parse(JSON.stringify(raw)); @@ -976,18 +1058,42 @@ const runGatewayAction = async (gatewayId, action, confirm = false) => { const runSimulator = async () => { try { + const answerOverrides = Object.entries(simulatorAnswerOverrides.value) + .filter(([, value]) => value === true || value === false || value === null) + .map(([questionId, value]) => ({ question_id: parseIntOrZero(questionId), value })); simulatorResult.value = await requestPost("/department/selfserve/studio/simulate", { department: departmentId.value, lane_id: parseIntOrZero(simulatorForm.value.lane_id), reg: simulatorForm.value.reg, customer_number: parseNullableInt(simulatorForm.value.customer_number), vehicle_type_id: parseNullableInt(simulatorForm.value.vehicle_type_id), + config_source: simulatorForm.value.config_source || "draft", + include_hardware: simulatorForm.value.include_hardware !== false, + mode: "full_dry_run", + answer_overrides: answerOverrides, }); + applyCanvasFilters(); } catch (error) { withErrorToast(error, "Simulation failed."); } }; +const setSimulatorAnswerOverride = (questionId, value) => { + simulatorAnswerOverrides.value = { + ...simulatorAnswerOverrides.value, + [questionId]: value, + }; +}; + +const clearSimulatorAnswerOverrides = () => { + simulatorAnswerOverrides.value = {}; +}; + +const simulatorAnswerButtonClass = (questionId, value) => ({ + "is-primary": simulatorAnswerOverrides.value?.[questionId] === value, + "is-light": simulatorAnswerOverrides.value?.[questionId] !== value, +}); + const reorderTask = async (taskNode, direction) => { const ordered = taskNodes.value .map((node) => node.data.raw) @@ -1799,8 +1905,15 @@ onBeforeUnmount(() => {