Documentation
Function Codes
Flow 1 — Machine
Flow 1 — Machine
if (msg.payload.length > 1000) {
return msg;
} else {
return {payload: ""}
}
const unavailable_stream = { "MTConnectStreams": { "$": { "xmlns:m": "urn:mtconnect.org:MTConnectStreams:1.3", "xmlns": "urn:mtconnect.org:MTConnectStreams:1.3", "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance", "xmlns:x": "urn:CustomData.com:CustomDataStreams:1.3", "xsi:schemaLocation": "urn:CustomData.com:CustomDataStreams:1.3 DevicesCustomDataStreams.xsd" }, "Header": [{ "$": { "creationTime": "2025-11-21T05:11:26Z", "sender": "IKTBNPG-Milling", "instanceId": "1763696481", "version": "1.3.0.17", "bufferSize": "131072", "nextSequence": "5089", "firstSequence": "1", "lastSequence": "5088" } }], "Streams": [{ "DeviceStream": [{ "$": { "name": "FANUC_CNC", "uuid": "000" }, "ComponentStream": [{ "$": { "component": "Rotary", "name": "C1", "componentId": "C1" }, "Samples": [{ "Load": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nloadSPP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.load-SP_P1", "sequence": "5071" } }], "RotaryVelocity": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nspeedSPP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.speed-SP_P1", "sequence": "5070", "subType": "ACTUAL" } }] }], "Events": [{ "RotaryMode": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nspdlmodeSPP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.spdlmode-SP_P1", "sequence": "5072" } }] }], "Condition": [{ "Unavailable": [{ "$": { "dataItemId": "nservoSPP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.servo-SP_P1", "sequence": "5088", "type": "ACTUATOR" } }] }] }, { "$": { "component": "Path", "name": "Path", "componentId": "Path" }, "Samples": [{ "PathFeedrate": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "npathfdrt1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.pathfdrt-1", "sequence": "5055", "subType": "ACTUAL" } }] }], "Events": [{ "ActiveAxes": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nactaxes1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.actaxes-1", "sequence": "5056" } }], "Block": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nblock1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.block-1", "sequence": "5054" } }], "Execution": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nexecution1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.execution-1", "sequence": "5058" } }], "PathFeedrateOverride": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nfeedoverride1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.feedoverride-1", "sequence": "5061", "subType": "PROGRAMMED" } }, { "_": "UNAVAILABLE", "$": { "dataItemId": "nrapidoverride1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.rapidoverride-1", "sequence": "5062", "subType": "RAPID" } }], "Line": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nline1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.line-1", "sequence": "5053" } }], "ControllerMode": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nmode1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.mode-1", "sequence": "5057" } }], "PartCount": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "npartcnt1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.partcnt-1", "sequence": "5059" } }], "ProgramComment": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nprogcom1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.progcom-1", "sequence": "5052" } }], "Program": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nprogram1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.program-1", "sequence": "5051" } }], "RotaryVelocityOverride": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nspdloverride1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.spdloverride-1", "sequence": "5060" } }], "ToolNumber": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "ntoolnumber1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.toolnumber-1", "sequence": "5050" } }] }], "Condition": [{ "Unavailable": [{ "$": { "dataItemId": "ncomms1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.comms-1", "sequence": "5074", "type": "COMMUNICATIONS" } }, { "$": { "dataItemId": "nlogic1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.logic-1", "sequence": "5075", "type": "LOGIC_PROGRAM" } }, { "$": { "dataItemId": "nmotion1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.motion-1", "sequence": "5076", "type": "MOTION_PROGRAM" } }, { "$": { "dataItemId": "nopmessage1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.opmessage-1", "sequence": "5078", "type": "SYSTEM" } }, { "$": { "dataItemId": "nservonoaxis1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.servonoaxis-1", "sequence": "5073", "type": "ACTUATOR" } }, { "$": { "dataItemId": "nsystem1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.system-1", "sequence": "5077", "type": "SYSTEM" } }] }] }, { "$": { "component": "Linear", "name": "X1", "componentId": "X1" }, "Samples": [{ "Position": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nactXP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.act-X_P1", "sequence": "5064", "subType": "ACTUAL" } }], "Load": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nloadXP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.load-X_P1", "sequence": "5065" } }] }], "Condition": [{ "Unavailable": [{ "$": { "dataItemId": "noverheatXP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.overheat-X_P1", "sequence": "5080", "type": "TEMPERATURE" } }, { "$": { "dataItemId": "nservoXP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.servo-X_P1", "sequence": "5081", "type": "ACTUATOR" } }, { "$": { "dataItemId": "ntravelXP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.travel-X_P1", "sequence": "5079", "type": "POSITION" } }] }] }, { "$": { "component": "Linear", "name": "Y1", "componentId": "Y1" }, "Samples": [{ "Position": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nactYP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.act-Y_P1", "sequence": "5066", "subType": "ACTUAL" } }], "Load": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nloadYP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.load-Y_P1", "sequence": "5067" } }] }], "Condition": [{ "Unavailable": [{ "$": { "dataItemId": "noverheatYP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.overheat-Y_P1", "sequence": "5083", "type": "TEMPERATURE" } }, { "$": { "dataItemId": "nservoYP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.servo-Y_P1", "sequence": "5084", "type": "ACTUATOR" } }, { "$": { "dataItemId": "ntravelYP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.travel-Y_P1", "sequence": "5082", "type": "POSITION" } }] }] }, { "$": { "component": "Linear", "name": "Z1", "componentId": "Z1" }, "Samples": [{ "Position": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nactZP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.act-Z_P1", "sequence": "5068", "subType": "ACTUAL" } }], "Load": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nloadZP1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.load-Z_P1", "sequence": "5069" } }] }], "Condition": [{ "Unavailable": [{ "$": { "dataItemId": "noverheatZP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.overheat-Z_P1", "sequence": "5086", "type": "TEMPERATURE" } }, { "$": { "dataItemId": "nservoZP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.servo-Z_P1", "sequence": "5087", "type": "ACTUATOR" } }, { "$": { "dataItemId": "ntravelZP1", "timestamp": "2025-11-21T04:25:31.519Z", "name": "n.travel-Z_P1", "sequence": "5085", "type": "POSITION" } }] }] }, { "$": { "component": "Controller", "name": "controller", "componentId": "cn1" }, "Events": [{ "EmergencyStop": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "nestop1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.estop-1", "sequence": "5063" } }] }] }, { "$": { "component": "Device", "name": "FANUC_CNC", "componentId": "dev" }, "Events": [{ "x:Cuttingtime": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "CuttingTime", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.CuttingTime", "sequence": "5046" } }], "x:Cuttingtime2": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "CuttingTime2", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.CuttingTime2", "sequence": "5047" } }], "x:Cycletime1": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "CycleTime1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.CycleTime1", "sequence": "5048" } }], "x:Cycletime2": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "CycleTime2", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.CycleTime2", "sequence": "5049" } }], "x:Operatingtime1": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "OperatingTime1", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.OperatingTime1", "sequence": "5044" } }], "x:Operatingtime2": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "OperatingTime2", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.OperatingTime2", "sequence": "5045" } }], "x:Partsrequired": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "PartsRequired", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.PartsRequired", "sequence": "5043" } }], "x:Partstotal": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "PartsTotal", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.PartsTotal", "sequence": "5042" } }], "x:Powerontime": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "PowerOnTime", "timestamp": "2025-11-21T04:25:20.264Z", "name": "n.PowerOnTime", "sequence": "5040" } }], "AssetChanged": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "dev_asset_chg", "timestamp": "2025-11-21T03:41:21.063989Z", "sequence": "10", "assetType": "" } }], "AssetRemoved": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "dev_asset_rem", "timestamp": "2025-11-21T03:41:21.063989Z", "sequence": "11", "assetType": "" } }], "Availability": [{ "_": "UNAVAILABLE", "$": { "dataItemId": "navail", "timestamp": "2025-11-21T04:25:31.518Z", "name": "n.avail-", "sequence": "5041" } }] }] }] }] }] } }
let milling_stream = flow.get('milling_stream');
let milling_stream1 = flow.get('milling_stream1');
let store = flow.get("deviceStreamStore") || [null, null];
if (msg.topic === "from_milling") {
if (milling_stream == null) {
milling_stream = unavailable_stream.MTConnectStreams?.Streams?.[0]?.DeviceStream || [];
} else {
milling_stream = milling_stream.MTConnectStreams?.Streams?.[0]?.DeviceStream || [];
}
if (milling_stream.length > 0) {
if (!milling_stream[0].$) {
milling_stream[0].$ = {};
}
milling_stream[0].$.name = `${milling_stream[0].$.name}_0`;
milling_stream[0].$.uuid = `${milling_stream[0].$.uuid}_0`;
store[0] = milling_stream[0];
}
} else if (msg.topic === "from_milling1") {
if (milling_stream1 == null) {
milling_stream1 = unavailable_stream.MTConnectStreams?.Streams?.[0]?.DeviceStream || [];
} else {
milling_stream1 = milling_stream1.MTConnectStreams?.Streams?.[0]?.DeviceStream || [];
}
if (milling_stream1.length > 0) {
if (!milling_stream1[0].$) {
milling_stream1[0].$ = {};
}
milling_stream1[0].$.name = `${milling_stream1[0].$.name}_1`;
milling_stream1[0].$.uuid = `${milling_stream1[0].$.uuid}_1`;
store[1] = milling_stream1[0];
}
}
flow.set("deviceStreamStore", store);
/*
node.warn({
milling: !!store[0],
milling1: !!store[1]
});
*/
if (!store[0] || !store[1]) {
return null;
}
msg.payload = {
MTConnectStreams: {
Streams: [{
DeviceStream: [
store[0],
store[1]
]
}]
}
};
return msg;
let allDeviceStreams = msg.payload.MTConnectStreams.Streams[0].DeviceStream;
let finalResult = [];
allDeviceStreams.forEach(deviceStream => {
let result = {};
let eventMap = {};
result.name1 = deviceStream.$.name || "Unknown";
result.uuid = deviceStream.$.uuid || "Unknown";
result.Faults = [];
deviceStream.ComponentStream.forEach(component => {
let componentName =
component.$.component +
(component.$.name ? `_${component.$.name}` : "");
['Events', 'Samples', 'Condition'].forEach(section => {
if (component[section]) {
component[section].forEach(group => {
for (let tag in group) {
if (Array.isArray(group[tag]) && group[tag][0]) {
group[tag].forEach(item => {
let value = item['_'] || item;
result[`${componentName}_${tag}`] = value;
eventMap[tag] = value;
if (tag === "Fault") {
result.Faults.push({
component: componentName,
tag: tag,
dataItemId: item.$?.dataItemId || "",
timestamp: item.$?.timestamp || "",
name: item.$?.name || "",
sequence: item.$?.sequence || "",
nativeCode: item.$?.nativeCode || "",
type: item.$?.type || "",
message: value
});
}
});
}
}
});
}
});
});
function getEventValue(tagName) {
return eventMap[tagName] ?? null;
}
result.ActiveAxes = getEventValue('ActiveAxes');
result.ToolId = getEventValue('ToolId');
let programRaw = getEventValue('Program') || "";
let match = programRaw.match(/PATH\d+\/O(\d+)/);
if (match) {
result.Program = `PATH1/${match[1].padStart(3, '0')}`;
} else {
result.Program = "";
}
result.ProgramComment = getEventValue('ProgramComment');
result.Line = getEventValue('Line');
result.Block = getEventValue('Block');
result.ControllerMode = getEventValue('ControllerMode');
result.Execution = getEventValue('Execution');
result.PartCount = getEventValue('PartCount');
result.RotaryVelocityOverride = getEventValue('RotaryVelocityOverride');
result.PathFeedrateOverride = getEventValue('PathFeedrateOverride');
result.RotaryMode = getEventValue('RotaryMode');
result.EmergencyStop = getEventValue('EmergencyStop');
result.Availability = getEventValue('Availability') === 'AVAILABLE' ? 1 : 0;
result.Availability1 = getEventValue('Availability');
result.AssetChanged = getEventValue('AssetChanged');
result.AssetRemoved = getEventValue('AssetRemoved');
result.SPCC = String(
result['Device_FANUC_CNC_x:Spccsignal'] || "FALSE"
).trim().toUpperCase() === 'TRUE' ? 1 : 0;
result.SPCW = String(
result['Device_FANUC_CNC_x:Spcwsignal'] || "FALSE"
).trim().toUpperCase() === 'TRUE' ? 1 : 0;
finalResult.push(result);
});
msg.payload = {
machines: finalResult
};
return msg;
let now = Date.now();
function getValueByKeyword(machine, keyword) {
let key = Object.keys(machine).find(k => k.includes(keyword));
return key ? machine[key] : "0";
}
function formatDuration(ms) {
let totalSeconds = Math.floor(ms / 1000);
let hrs = String(Math.floor(totalSeconds / 3600)).padStart(2, '0');
let mins = String(Math.floor((totalSeconds % 3600) / 60)).padStart(2, '0');
let secs = String(totalSeconds % 60).padStart(2, '0');
return `${hrs}:${mins}:${secs}`;
}
msg.payload.machines = msg.payload.machines.map(machine => {
//Operating Time
let operatingTimeRaw = parseInt(getValueByKeyword(machine, "Operatingtime1"), 10);
let operatingTime1Raw = parseFloat(getValueByKeyword(machine, "Operatingtime2"));
let totalOperatingMs = operatingTimeRaw + (operatingTime1Raw * 60 * 1000);
machine.TotalOperatingMins = Math.round(totalOperatingMs / (60 * 1000));
machine.OperatingTimeFormatted = formatDuration(totalOperatingMs);
//Cycle Time
let cycleTimeRaw = parseInt(getValueByKeyword(machine, "Cycletime1"), 10);
let cycleTime1Raw = parseFloat(getValueByKeyword(machine, "Cycletime2"));
let totalCycleMs = cycleTimeRaw + (cycleTime1Raw * 60 * 1000);
machine.TotalCycleTimeMins = Math.round(totalCycleMs / (60 * 1000));
machine.CycleTimeFormatted = formatDuration(totalCycleMs);
return machine;
});
return msg;
let machines = msg.payload.machines || [];
let updatedMachines = [];
let executionStats = {};
let total = machines.length;
machines.forEach(machine => {
let status = machine.Execution || "UNKNOWN";
if (!executionStats[status]) {
executionStats[status] = { count: 0 };
}
executionStats[status].count += 1;
updatedMachines.push(machine);
});
for (let status in executionStats) {
executionStats[status].percentage = ((executionStats[status].count / total) * 100).toFixed(2);
}
msg.payload = {
machines: updatedMachines,
executionStats: executionStats,
total: total
};
return msg;
const odoo_data = global.get('odoo_data') || []
const machines_data = global.get('machine_data')
const maintenance_data = global.get('maintenance_data') || []
msg.payload = {
machines: machines_data.machines,
executionStats: machines_data.executionStats,
total: machines_data.total,
productionData: odoo_data.productionData,
maintenanceData: maintenance_data.maintenanceData
}
return msg;
let points = [];
msg.payload.forEach(tool => {
points.push({
measurement: "toollife",
tags: {
machine: tool.machine,
toolNumber: String(tool.toolNumber)
},
fields: {
name: String(tool.name),
type: String(tool.type),
material: String(tool.material),
expectedLife: Number(tool.expectedLife),
usedMins: Number(tool.usedMins),
remainingMins: Number(tool.remainingMins),
percentage: Number(tool.percentage),
status: String(tool.status),
startTime: tool.startTime ? String(tool.startTime) : '',
lastUpdatedTime: tool.lastUpdatedTime ? String(tool.lastUpdatedTime): ''
},
timestamp: new Date()
});
});
msg.payload = points;
return msg;
const topic = msg.topic;
let workorder = context.get("workorder") || null;
let machines = context.get("machines") || []; // now store ALL machines
if (topic === "workorder_raw") {
workorder = msg.payload;
context.set("workorder", workorder);
}
if (topic === "machine_data") {
if (Array.isArray(msg.payload.machines)) {
machines = msg.payload.machines;
} else if (Array.isArray(msg.payload)) {
machines = msg.payload;
}
context.set("machines", machines);
}
if (!workorder || machines.length === 0) {
return null;
}
const matchedMachine = machines.find(m =>
m.name1 === workorder.workcenter_name &&
m.uuid === workorder.workcenter_code
);
if (!matchedMachine) {
//node.warn("⚠ No matching machine found for workorder: " + workorder.workcenter_name);
return null;
}
let planned_time = Number(workorder.planned_time) || 0;
let product_qty = Number(workorder.product_qty) || 0;
let actual_qty = Number(matchedMachine.PartCount) || 0;
let runtime = Number(matchedMachine.TotalOperatingMins) || 0;
let totalCycle = Number(matchedMachine.TotalCycleTimeMins) || 0;
let availability = 0;
if (planned_time > 0) {
availability = (runtime / planned_time) * 100;
availability = Math.min(availability, 100);
}
let idealCycleTime = 0;
if (product_qty > 0) idealCycleTime = planned_time / product_qty;
let performance = 0;
if (runtime > 0 && idealCycleTime > 0 && actual_qty > 0) {
performance = ((idealCycleTime * actual_qty) / runtime) * 100;
performance = Math.min(performance, 100);
}
let oee = 0;
if (availability > 0 && performance > 0) {
oee = (availability * performance) / 100;
}
msg.payload = {
workorder_id: workorder.workorder_id,
workorder_name: workorder.workorder_name,
order_id: workorder.order_id,
order_name: workorder.order_name,
workorder_state: workorder.workorder_state,
product_name: workorder.product_name,
workcenter_name: workorder.workcenter_name,
workcenter_code: workorder.workcenter_code,
date_start: workorder.date_start,
timestamp: workorder.timestamp,
planned_time,
runtime,
product_qty,
actual_qty,
availability: Number(availability.toFixed(2)),
performance: Number(performance.toFixed(2)),
oee: Number(oee.toFixed(2))
};
return msg;
msg.payload = [
{
measurement: "oee_metrics",
fields: {
availability: msg.payload.availability,
performance: msg.payload.performance,
oee: msg.payload.oee,
runtime: msg.payload.runtime,
planned_time: msg.payload.planned_time,
product_qty: msg.payload.product_qty,
actual_qty: msg.payload.actual_qty,
workorder_id: Number(msg.payload.workorder_id),
workorder_name: msg.payload.workorder_name,
order_name: msg.payload.order_name,
product_name: msg.payload.product_name,
workorder_state: msg.payload.workorder_state,
workcenter_name: msg.payload.workcenter_name,
workcenter_code: msg.payload.workcenter_code,
date_start: msg.payload.date_start
},
tags: {
workorder_id: String(Number(msg.payload.workorder_id)),
workcenter: msg.payload.workcenter_name
},
time: new Date().toISOString()
}
];
return msg;
// When someone starts/pauses/continues/done
if (msg.topic === "sync_active_order") {
flow.set("activeOrder", msg.payload || null);
node.send({
topic: "broadcast_active_order",
payload: msg.payload || null
});
return null;
}
//When a new dashboard connects
if (msg.topic === "request_active_order") {
const activeOrder = flow.get("activeOrder") || null;
node.send({
topic: "broadcast_active_order",
payload: activeOrder
});
return null;
}
return null;
<style>
md-toolbar {
display: none !important;
}
md-content.md-default-theme,
md-content {
margin-top: 0 !important;
padding-top: 0 !important;
}
:root {
--primary-color: black;
--secondary-color: #3498db;
--accent-color: #e74c3c;
--light-color: #ecf0f1;
--dark-color: #34495e;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f7fa;
}
.fullscreen-dashboard {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
padding: 20px;
background-color: var(--light-color);
overflow: auto;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
background-color: var(--primary-color);
padding: 15px 20px;
border-radius: 8px;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.search-container {
display: flex;
gap: 15px;
align-items: center;
}
.time-info {
font-size: 18px;
font-weight: bold;
}
.fault-info {
font-size: 30px;
font-weight: bold;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
margin-bottom: 25px;
}
.metric-card {
background: white;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
transition: transform 0.3s, box-shadow 0.3s;
}
.metric-card:hover {
transform: translateY(-5px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}
.metric-title {
font-size: 14px;
color: #7f8c8d;
margin-bottom: 5px;
}
.metric-value {
font-size: 24px;
font-weight: bold;
color: var(--dark-color);
}
.connection-value {
font-size: 50px;
font-weight: bold;
color: red;
animation: blink 2s infinite;
text-align: center;
}
.connection-value1 {
font-size: 28px;
font-weight: bold;
color: black;
text-align: center;
}
.main-content {
display: flex;
gap: 5px;
margin-bottom: 25px;
height: 35vh;
}
.fault-content {
display: flex;
gap: 5px;
margin-bottom: 25px;
height: 35vh;
overflow-y: auto;
overflow-x: hidden;
}
.fault-section {
flex: 1;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
}
.production-container {
display: flex;
flex-wrap: wrap;
gap: 5px;
width: 100%;
}
.production-section,
.stats-section {
flex: 1 1 48%;
min-width: 300px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
box-sizing: border-box;
margin: 10px;
}
.machine-container {
display: flex;
flex-wrap: wrap;
gap: 10px;
width: 100%;
}
.machine-section {
flex: 0 0 300px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stat-section {
flex: 0 0 300px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.start-section {
flex: 0 0 300px;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
align-items: center;
}
.section-title {
font-size: 18px;
color: var(--dark-color);
margin-bottom: 15px;
border-bottom: 2px solid var(--light-color);
padding-bottom: 8px;
}
.big-number {
font-size: 48px;
font-weight: bold;
text-align: center;
margin: 20px 0;
color: var(--primary-color);
}
.chart-container {
height: 200px;
position: relative;
margin-top: 20px;
}
?.production&view_type=form .bar-chart {
position: absolute;
bottom: 0;
width: 100%;
display: flex;
align-items: flex-end;
height: 100%;
}
.bar-column {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.bar {
width: 30px;
background: var(--secondary-color);
border-radius: 4px 4px 0 0;
transition: height 0.5s ease;
}
.time-labels {
display: flex;
justify-content: space-around;
margin-top: 10px;
font-size: 12px;
color: #7f8c8d;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-bottom: 20px;
}
.stat-card {
text-align: center;
padding: 10px;
background: #f8f9fa;
border-radius: 6px;
}
.stat-title {
font-size: 14px;
color: #7f8c8d;
}
.stat-value {
font-size: 32px;
font-weight: bold;
margin-top: 5px;
}
.behind-value {
color: var(--accent-color);
}
.oee-pie {
width: 200px;
height: 200px;
border-radius: 50%;
background: conic-gradient(#2ecc71 0% 70%, #3498db 70% 85%, #f39c12 85% 95%, #ecf0f1 95% 100%);
position: relative;
margin: 20px 0;
}
.oee-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 36px;
font-weight: bold;
color: var(--primary-color);
}
.oee-legend {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 15px;
}
.oee-legend-item {
display: flex;
align-items: center;
font-size: 14px;
}
.oee-legend-color {
width: 15px;
height: 15px;
margin-right: 8px;
border-radius: 3px;
}
.action-buttons {
display: flex;
justify-content: space-between;
margin-top: 25px;
}
.action-button {
padding: 10px 20px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s;
}
.alarm-button {
background: var(--accent-color);
color: white;
}
.alarm-button:hover {
background: #c0392b;
transform: translateY(-2px);
}
.reject-button {
background: #f39c12;
color: white;
}
.reject-button:hover {
background: #d35400;
transform: translateY(-2px);
}
.categorize-button {
background: black;
color: white;
}
.categorize-button:hover {
background: #2980b9;
transform: translateY(-2px);
}
.home-button {
background: white;
color: black;
}
.home-button:hover {
background: #2980b9;
transform: translateY(-2px);
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
}
.btn {
padding: 5px 10px;
margin: 2px;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #007bff;
color: white;
}
.btn:hover {
background-color: #0056b3;
}
.active-row {
background-color: #d9edf7;
}
.dt-summary {
font-size: 20px;
font-weight: bold;
}
.filters {
margin: 10px 0;
display: flexflex-wrap: wrap;
gap: 10px;
align-items: center;
}
.filters input[type="text"],
.filters input[type="date"],
.filters select {
padding: 5px 8px;
font-size: 14px;
border-radius: 5px;
border: 1px solid #ccc;
}
.dt-summary table {
width: 100%;
border-collapse: collapse;
text-align: center;
background-color: white;
}
.dt-summary th,
.dt-summary td {
border: 1px solid #ccc;
padding: 8px 12px;
}
.dt-summary th {
background-color: #f0f0f0;
color: #222;
}
.dt-summary tr:nth-child(even) {
background-color: #f5f5f5;
}
.export-btn {
margin-top: 10px;
padding: 8px 14px;
background-color: #007bff;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
}
.export-btn:hover {
background-color: #0056b3;
}
.blink-red {
background-color: red;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.emergency-stop {
background-color: red;
color: white;
font-weight: bold;
font-size: 18px;
padding: 8px 16px;
border-radius: 8px;
display: inline-block;
animation: blink 1s infinite;
text-align: center;
box-shadow: 0 0 10px rgba(255, 0, 0, 0.6);
}
@keyframes blink {
0% {
opacity: 1;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.disabled-btn {
background-color: #6c757d !important;
border-color: #6c757d !important;
color: white !important;
cursor: not-allowed;
}
.progress-bar {
height: 24px;
color: white;
text-align: center;
border-radius: 4px;
}
.header {
margin-top: 10px;
padding: 5px;
border-radius: 4px;
font-family: sans-serif;
font-size: 12px;
}
.running {
color: #2e7d32;
border: 1px solid #a5d6a7;
background: #e8f5e9;
}
.stopped {
color: #c62828;
border: 1px solid #ef9a9a;
background: #ffebee;
}
</style>
<template>
<div class="fullscreen-dashboard">
<div class="header-container">
<div class="time-info">
<div id="date" class="date-display">{{ dateDisplay }}</div>
<div id="time" class="time-display">{{ timeDisplay }}</div>
</div>
<div class="fault-info" v-if="selected">
<div v-if="selected.EmergencyStop === 'TRIGGERED'" class="emergency-stop">🚨 EMERGENCY STOP</div>
<div class="emergency-stop" v-if="selected.Faults && selected.Faults.length > 0">⚠️ ALARM</div>
<div class="emergency-stop" v-if="selected && selected.maintenance && selected.maintenance.length > 0">🔧
MAINTENANCE</div>
</div>
<div class="search-container" v-if="selected">
<div class="action-buttons"><button class="action-button home-button" @click="goBack()">Home</button>
</div>
</div>
</div>
<div class="metrics-grid" v-if="selected" style="display: flex; flex-wrap: wrap; gap: 1rem;">
<div class="metric-card"
style="flex: 1 1 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; box-shadow: 1px 1px 4px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 0.75rem;">
<div style="font-size: 4em; line-height: 1; min-width: 50px; text-align: center;">🏭</div>
<div>
<div class="metric-title" style="font-weight: 600;">Machine Status</div>
<div class="metric-value" style="font-size: 1.2em;">{{ selected.Availability1 }}</div>
</div>
</div>
<div class="metric-card"
style="flex: 1 1 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; box-shadow: 1px 1px 4px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 0.75rem;">
<div style="font-size: 4em; line-height: 1; min-width: 50px; text-align: center;">▶️</div>
<div>
<div class="metric-title" style="font-weight: 600;">Execution Status</div>
<div class="metric-value" style="font-size: 1.2em;">{{ selected.Execution }}</div>
</div>
</div>
<div class="metric-card"
style="flex: 1 1 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; box-shadow: 1px 1px 4px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 0.75rem;">
<div style="font-size: 4em; line-height: 1; min-width: 50px; text-align: center;">🖥️</div>
<div>
<div class="metric-title" style="font-weight: 600;">Current Program</div>
<div class="metric-value" style="font-size: 1.2em;">{{ selected.Program }}</div>
<br>
<div class="metric-title" style="font-weight: 600;">Line Number</div>
<div class="metric-value" style="font-size: 1.2em;">{{ selected.Line }}</div>
</div>
</div>
<div class="metric-card"
style="flex: 1 1 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; box-shadow: 1px 1px 4px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 0.75rem;">
<div style="font-size: 4em; line-height: 1; min-width: 50px; text-align: center;">🎛️</div>
<div>
<div class="metric-title" style="font-weight: 600;">Current Control Mode</div>
<div class="metric-value" style="font-size: 1.2em;">{{ selected.ControllerMode }}</div>
</div>
</div>
<div class="metric-card"
style="flex: 1 1 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; box-shadow: 1px 1px 4px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 0.75rem;">
<div style="font-size: 2em; line-height: 1; min-width: 50px; text-align: center;">↕️ ↔️ ↗️</div>
<div>
<div class="metric-title" style="font-weight: 600;">Absolute Positions</div>
<div class="metric-value" style="font-size: 1.2em;">X: {{ selected.Linear_X1_Position }} <br> Y: {{ selected.Linear_Y1_Position }}
<br> Z: {{ selected.Linear_Z1_Position }}
</div>
</div>
</div>
<div class="metric-card"
style="flex: 1 1 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; box-shadow: 1px 1px 4px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 0.75rem;">
<div style="font-size: 4em; line-height: 1; min-width: 50px; text-align: center;">🕒</div>
<div>
<div class="metric-title" style="font-weight: 600;">Current Runtime</div>
<div class="metric-value" style="font-size: 1.2em;">{{ selected.OperatingTimeFormatted }}</div>
</div>
</div>
<div class="metric-card"
style="flex: 1 1 200px; padding: 1rem; border: 1px solid #ddd; border-radius: 8px; box-shadow: 1px 1px 4px rgba(0,0,0,0.1); display: flex; align-items: center; gap: 0.75rem;">
<div style="font-size: 4em; line-height: 1; min-width: 50px; text-align: center;">↻</div>
<div>
<div class="metric-title" style="font-weight: 600;">Current Cycle Time</div>
<div class="metric-value" style="font-size: 1.2em;">{{ selected.CycleTimeFormatted }}</div>
</div>
</div>
</div>
<div class="production-container">
<div class="production-section" v-if="selected && selected.Faults && selected.Faults.length > 0">
<h3 style="color: #d32f2f; display: flex; align-items: center;">
<span style="font-size: 2.2rem; margin-right: 12px; animation: pulse 1.5s infinite;">🚨</span>
Alarm Message:
</h3>
<div style="max-height: 300px; overflow-y: auto;">
<table class="table">
<thead>
<tr>
<th>No.</th>
<th>Component</th>
<th>Type</th>
<th>Code</th>
<th>Message</th>
</tr>
</thead>
<tbody>
<tr v-for="(fault, index) in pagedFaults" :key="index">
<td>{{ index + 1 + (faultsCurrentPage - 1) * faultsPerPage }}</td>
<td>{{ fault.component }}</td>
<td>{{ fault.type }}</td>
<td>{{ fault.nativeCode }}</td>
<td>{{ fault.message }}</td>
</tr>
</tbody>
</table>
<div class="pagination-controls" v-if="selected.Faults.length > faultsPerPage" style="margin-top:10px;">
<button class="btn btn-sm btn-primary" :disabled="faultsCurrentPage === 1" @click="setFaultsPage(faultsCurrentPage - 1)">Prev</button>
<span> Page {{ faultsCurrentPage }} / {{ ceil(selected.Faults.length / faultsPerPage) }} </span>
<button class="btn btn-sm btn-primary" :disabled="faultsCurrentPage === ceil(selected.Faults.length / faultsPerPage)" @click="setFaultsPage(faultsCurrentPage + 1)">Next</button>
</div>
</div>
</div>
</div>
<div class="main-content">
<div class="production-container">
<div class="production-section" v-if="!selected && machines.length === 0">
<br>
<div class="connection-value">Connection Error!</div><br>
<div class="connection-value1">Please ensure that the PC is on</div>
</div>
<template v-if="machines.length > 0 && !selected">
<div class="machine-section" v-for="machine in machines" :key="machine.uuid">
<svg class="cnc-3d" viewBox="0 0 250 180" xmlns="http://www.w3.org/2000/svg" @click="selectMachine(machine)"
style="cursor:pointer; width: 100%; height: auto;">
<ellipse cx="120" cy="165" rx="85" ry="10" fill="rgba(0,0,0,0.1)" />
<line x1="185" y1="15" x2="185" y2="30" stroke="black" stroke-width="2" />
<rect x="182" y="10" width="6" height="5" fill="#dc3545" stroke="black"
:opacity="machine.Execution === 'STOPPED' ? 1 : 0.3" />
<rect x="182" y="5" width="6" height="5" fill="#ffc107" stroke="black"
:opacity="machine.Execution === 'IDLE' ? 1 : 0.3" />
<rect x="182" y="0" width="6" height="5" fill="#28a745" stroke="black"
:opacity="machine.Execution === 'ACTIVE' ? 1 : 0.3" />
<path d="M30 40 L170 40 L205 15 L65 15 Z" fill="#e0e0e0" stroke="black" stroke-width="0.5" />
<path d="M170 40 L205 15 L205 105 L170 130 Z" fill="#b0b0b0" stroke="black" stroke-width="0.5" />
<line x1="178" y1="45" x2="195" y2="33" stroke="black" stroke-width="1" />
<line x1="178" y1="52" x2="195" y2="40" stroke="black" stroke-width="1" />
<line x1="178" y1="59" x2="195" y2="47" stroke="black" stroke-width="1" />
<rect x="30" y="40" width="140" height="90" :fill="getExecutionColor(machine.Execution)" stroke="black"
stroke-width="0.5" />
<rect x="40" y="55" width="80" height="65" fill="#333" rx="2" />
<rect x="43" y="58" width="74" height="59" fill="#1a1a1a" />
<rect x="43" y="58" width="36" height="59" fill="#88b0d0" opacity="0.2" />
<rect x="81" y="58" width="36" height="59" fill="#88b0d0" opacity="0.2" />
<circle cx="85" cy="90" r="3" fill="white" stroke="black" stroke-width="0.5" />
<circle cx="75" cy="90" r="3" fill="white" stroke="black" stroke-width="0.5" />
<rect x="125" y="45" width="40" height="80" fill="#f5f5f5" stroke="black" rx="1" />
<rect x="128" y="48" width="34" height="25" fill="#111" />
<text x="145" y="65" font-size="4" fill="#00ff00" text-anchor="middle" font-family="monospace">{{
machine.Execution }}</text>
<circle cx="135" cy="80" r="3" fill="#4fd615" stroke="black" stroke-width="0.5" />
<circle cx="145" cy="80" r="3" fill="#f0ec0a" stroke="black" stroke-width="0.5" />
<circle cx="155" cy="80" r="3" fill="#b71c1c" stroke="black" stroke-width="0.5" />
<rect x="128" y="88" width="34" height="32" fill="#333" stroke="black" rx="1" />
<rect x="30" y="130" width="140" height="25" fill="#333" stroke="black" />
<path d="M170 130 L205 105 L205 130 L170 155 Z" fill="#222" stroke="black" />
<rect x="45" y="140" width="30" height="8" fill="#111" stroke="black" rx="1" />
</svg>
<h3>{{ machine.name1 }}</h3>
<div class="header"
:class="{ running: machine.Availability1 === 'AVAILABLE', stopped: machine.Availability1 !== 'AVAILABLE' }">
<i class="fa fa-cogs"></i> Machine Status: {{ machine.Availability1 === 'AVAILABLE' ? 'AVAILABLE' :
'UNAVAILABLE' }}
</div>
</div>
</template>
<div class="stat-section" v-if="machines.length > 0 && !selected">
<div style="display: flex; align-items: center;">
<div style="flex: 1; text-align: center;">
<svg viewBox="0 0 200 200" width="200" height="200">
<template v-for="slice in pieChartSlices" :key="slice.status">
<circle v-if="slice.isFull" cx="100" cy="100" r="90" :fill="slice.color" />
<path v-else :d="slice.path" :fill="slice.color" stroke="#fff" stroke-width="1" />
</template>
</svg>
</div>
<div style="flex: 2; margin-left: 20px;">
<table class="table">
<thead>
<tr>
<th>Execution Status</th>
<th>Count</th>
<th>%</th>
</tr>
</thead>
<tbody>
<tr v-for="(stat, status) in executionStats" :key="status"
:style="{ backgroundColor: getStatusColor(status), color: '#fff' }">
<td>{{ status }}</td>
<td>{{ stat.count }}</td>
<td>{{ stat.percentage }}%</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="production-container">
<div class="production-section" v-if="!selected">
<button
class="action-button categorize-button"
@click="loadToolLife">
Tool Life Tracker
</button>
</div>
</div>
<!-- PRODUCTION SECTION -->
<div class="production-container">
<div class="production-section" v-if="selected">
<div v-if="selectedOrders.length">
<h3>
<span style="font-size: 2rem; vertical-align: middle; margin-right: 10px;">📋</span>
Available Manufacturing Orders:
</h3>
<div style="max-height: 300px; overflow-y: auto;">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>No</th>
<th>Order Name</th>
<th>MO Status</th>
<th>Product</th>
<th>Qty</th>
<th>Work Order</th>
<th>Work Order Status</th>
<th>Expected Duration</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<template v-for="(item, index) in pagedOrders"
:key="(item.order && item.order.id || index) + '-' + (item.workorder && item.workorder.id || index)">
<tr v-if="!isStarted(item)">
<td>{{ index + 1 + (currentPage - 1) * itemsPerPage }}</td>
<td>{{ item.order.name }}</td>
<td>{{ item.order.state }}</td>
<td>{{ item.order.product_id[1] }}</td>
<td>{{ item.order.product_qty }}</td>
<td>{{ item.workorder.name }}</td>
<td>{{ item.workorder.state }}</td>
<td>{{ item.workorder.duration_expected }} mins</td>
<td>
<button class="btn btn-sm" @click="startProduction(item)" :disabled="selectedProductionOrder" :class="selectedProductionOrder ? 'btn-secondary disabled-btn' : 'btn-success'">
{{ selectedProductionOrder ? 'Running...' : 'Start' }}
</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<div class="pagination-controls" v-if="selectedOrders.length > itemsPerPage" style="margin-top:10px;">
<button class="btn btn-sm btn-primary" :disabled="currentPage === 1" @click="setPage(currentPage - 1)">Prev</button>
<span> Page {{ currentPage }} / {{ ceil(selectedOrders.length / itemsPerPage) }} </span>
<button class="btn btn-sm btn-primary" :disabled="currentPage === ceil(selectedOrders.length / itemsPerPage)" @click="setPage(currentPage + 1)">Next</button>
</div>
</div>
<div v-if="!selectedOrders.length">
<p>No Manufacturing Orders found for this machine.</p>
</div>
</div>
<div class="stats-section" v-if="selectedProductionOrder && selected">
<h3> Current Work Order:</h3>
<table class="table">
<tr>
<th>MO Reference</th>
<td>{{ selectedProductionOrder.order.name }}</td>
</tr>
<tr>
<th>Product</th>
<td>{{ selectedProductionOrder.order.product_id[1] }}</td>
</tr>
<tr>
<th>Planned Qty</th>
<td>{{ selectedProductionOrder.order.product_qty }}</td>
</tr>
<tr>
<th>Actual Qty</th>
<td>{{ selected.PartCount }}</td>
</tr>
</table> <br>
<div class="mt-2" style="text-align:center;">
<div class="btn-group">
<button class="btn btn-warning" v-if="!selectedProductionOrder.paused" @click="pauseProduction()">Pause</button>
<button class="btn btn-primary" v-if="selectedProductionOrder.paused" @click="continueProduction()">Continue</button>
<button class="btn btn-danger" @click="doneProduction()">Done</button>
</div>
</div>
</div>
</div>
<!-- TOOL LIFE SECTION -->
<div class="production-container" v-if="selected">
<div class="production-section">
<div style="display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:8px;">
<h3 style="margin:0; display:flex; align-items:center;">
<span style="font-size: 1.8rem; margin-right: 8px;">🛠️</span>
Tool Life
<small style="font-weight:400; color:#777; margin-left:8px; font-size:0.6em;">— {{ selected.name1 }}</small>
</h3>
<button class="btn btn-sm btn-success" @click="openToolForm()" v-if="!showToolForm">+ Add Tool</button>
</div>
<!-- CREATE TOOL FORM -->
<div v-if="showToolForm" class="tool-form"
style="margin-top:12px; padding:14px; border:1px solid #ddd; border-radius:8px; background:#fafafa;">
<div style="display:flex; flex-wrap:wrap; gap:12px;">
<div style="flex:1 1 130px;">
<label style="display:block; font-weight:600; margin-bottom:4px; font-size:0.9em;">Tool Number</label>
<select class="form-control" v-model.number="newTool.toolNumber"
style="width:100%; padding:6px 8px; border:1px solid #ccc; border-radius:4px;">
<option value="" disabled>Select</option>
<option v-for="n in 12" :key="n" :value="n">{{ n }}</option>
</select>
</div>
<div style="flex:1 1 180px;">
<label style="display:block; font-weight:600; margin-bottom:4px; font-size:0.9em;">Tool Name</label>
<input type="text" class="form-control" v-model="newTool.name" placeholder="e.g. Finish Boring Bar"
style="width:100%; padding:6px 8px; border:1px solid #ccc; border-radius:4px;">
</div>
<div style="flex:1 1 160px;">
<label style="display:block; font-weight:600; margin-bottom:4px; font-size:0.9em;">Type</label>
<select class="form-control" v-model="newTool.type"
style="width:100%; padding:6px 8px; border:1px solid #ccc; border-radius:4px;">
<option value="" disabled>Select</option>
<option v-for="t in toolTypes" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<div style="flex:1 1 160px;">
<label style="display:block; font-weight:600; margin-bottom:4px; font-size:0.9em;">Material</label>
<select class="form-control" v-model="newTool.material"
style="width:100%; padding:6px 8px; border:1px solid #ccc; border-radius:4px;">
<option value="" disabled>Select</option>
<option v-for="mat in toolMaterials" :key="mat" :value="mat">{{ mat }}</option>
</select>
</div>
<div style="flex:1 1 160px;">
<label style="display:block; font-weight:600; margin-bottom:4px; font-size:0.9em;">Machine</label>
<input type="text" class="form-control" :value="selected.name1" disabled
style="width:100%; padding:6px 8px; border:1px solid #ccc; border-radius:4px; background:#eee; color:#555;">
</div>
<div style="flex:1 1 160px;">
<label style="display:block; font-weight:600; margin-bottom:4px; font-size:0.9em;">Expected Rated
Life (mins)</label>
<input type="number" min="0" class="form-control" v-model.number="newTool.expectedLife"
placeholder="e.g. 500" style="width:100%; padding:6px 8px; border:1px solid #ccc; border-radius:4px;">
</div>
</div>
<div style="margin-top:14px; text-align:right;">
<button class="btn btn-secondary" @click="cancelToolForm()">Cancel</button>
<button class="btn btn-primary" @click="saveTool()">Save</button>
</div>
</div>
<div style="max-height: 300px; overflow-y: auto; margin-top:12px;" v-if="selectedToolList.length > 0">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>No.</th>
<th>Tool No.</th>
<th>Tool Name</th>
<th>Type</th>
<th>Material</th>
<th>Expected Life (mins)</th>
<!--
<th>Used (mins)</th>
<th>Remaining (mins)</th>
-->
<th>Wear %</th>
<th>Status</th>
<th>Start Time</th>
<th>Last Updated</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="(tool, index) in pagedTools" :key="tool.id">
<td>{{ index + 1 + (toolsCurrentPage - 1) * toolsPerPage }}</td>
<td>{{ tool.toolNumber }}</td>
<td>{{ tool.name }}</td>
<td>{{ tool.type }}</td>
<td>{{ tool.material }}</td>
<td>{{ tool.expectedLife }}</td>
<!--
<td>{{ tool.usedMins }}</td>
<td>{{ tool.remainingMins }}</td>
-->
<td>
<div style="display:flex; flex-direction:column; align-items:center; gap:4px;">
<span :style="{ fontWeight: 600, fontSize: '0.85em', color: tool.statusColor }">
{{ tool.percentage }}%
</span>
<div style="width:100px; height:8px; background:#e5e7eb; border-radius:6px; overflow:hidden;">
<div :style="{
height: '100%',
width: tool.percentage + '%',
background: tool.statusColor,
borderRadius: '6px',
transition: 'width 0.3s ease'
}"></div>
</div>
</div>
</td>
<td>
<span :style="{ padding: '2px 10px', borderRadius: '12px', color: '#fff', fontWeight: 600, fontSize: '0.85em', backgroundColor: tool.statusColor }">
{{ tool.status }}
</span>
</td>
<td style="font-size:0.82em; white-space:nowrap; color:#555;">
{{ tool.startTime ? new Date(tool.startTime).toLocaleString() : '—' }}
</td>
<td style="font-size:0.82em; white-space:nowrap; color:#555;">
{{ tool.lastUpdatedTime ? new Date(tool.lastUpdatedTime).toLocaleString() : '—' }}
</td>
<td><button class="btn btn-sm btn-danger" @click="deleteTool(tool)">Delete</button></td>
</tr>
</tbody>
</table>
<div class="pagination-controls" v-if="selectedToolList.length > toolsPerPage" style="margin-top:10px;">
<button class="btn btn-sm btn-primary" :disabled="toolsCurrentPage === 1" @click="setToolsPage(toolsCurrentPage - 1)">Prev</button>
<span> Page {{ toolsCurrentPage }} / {{ ceil(selectedToolList.length / toolsPerPage) }} </span>
<button class="btn btn-sm btn-primary" :disabled="toolsCurrentPage === ceil(selectedToolList.length / toolsPerPage)" @click="setToolsPage(toolsCurrentPage + 1)">Next</button>
</div>
</div>
<p v-if="selectedToolList.length === 0 && !showToolForm">No tools added yet for {{ selected.name1 }}.</p>
</div>
</div>
<!-- MAINTENANCE SECTION -->
<div class="production-container" v-if="selected && selected.maintenance && selected.maintenance.length > 0">
<div class="production-section">
<div style="max-height: 300px; overflow-y: auto;">
<h3>
<span style="font-size: 1.8rem; vertical-align: middle; margin-right: 8px;">⚙️</span>
Maintenance Requests:
</h3>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>No</th>
<th>Maintenance</th>
<th>Type</th>
<th>Stage</th>
<th>Schedule Date</th>
<th>Remaining Days</th>
</tr>
</thead>
<tbody>
<tr v-for="(m, index) in pagedMaintenance" :key="index">
<td>{{ index + 1 + (maintenanceCurrentPage - 1) * maintenanceItemsPerPage }}</td>
<td>{{ m.name }}</td>
<td>{{ m.maintenance_type }}</td>
<td>
<div class="progress-bar" :style="{
backgroundColor:
m.stage === 'New Request' ? 'grey' :
m.stage === 'In Progress' ? 'orange' : 'gray'
}">
{{ m.stage }}
</div>
</td>
<td>{{ m.schedule_date.split(' ')[0] }}</td>
<td>
<div class="progress-bar" :style="{
width: m.remaining_days <= 0 ? '100%' :
m.remaining_days <= 3 ? '80%' :
m.remaining_days <= 10 ? '50%' : '20%',
backgroundColor:
m.remaining_days <= 3 ? 'red' :
m.remaining_days <= 7 ? 'orange' : 'green'
}">
{{ m.remaining_days }} days
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination-controls" v-if="selected.maintenance.length > maintenanceItemsPerPage"
style="margin-top:10px;">
<button class="btn btn-sm btn-primary" :disabled="maintenanceCurrentPage === 1" @click="setMaintenancePage(maintenanceCurrentPage - 1)">Prev</button>
<span> Page {{ maintenanceCurrentPage }} / {{ ceil(selected.maintenance.length / maintenanceItemsPerPage) }} </span>
<button class="btn btn-sm btn-primary" :disabled="maintenanceCurrentPage === ceil(selected.maintenance.length / maintenanceItemsPerPage)" @click="setMaintenancePage(maintenanceCurrentPage + 1)">Next</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
dateDisplay: '-- -- ----',
timeDisplay: '--:--:--',
clockInterval: null,
machines: [],
selected: null,
executionStats: null,
productionData: [],
maintenanceData: [],
selectedOrders: [],
selectedProductionOrder: null,
startedOrders: {},
calcInterval: null,
currentPage: 1,
itemsPerPage: 2,
pagedOrders: [],
faultsCurrentPage: 1,
faultsPerPage: 2,
pagedFaults: [],
maintenanceCurrentPage: 1,
maintenanceItemsPerPage: 2,
pagedMaintenance: [],
toolList: [],
showToolForm: false,
newTool: {
toolNumber: '',
name: '',
type: '',
material: '',
expectedLife: ''
},
toolTypes: ['Boring Bar', 'Countersink', 'Drill', 'End Mill', 'Face Mill', 'Reamer', 'Tap', 'Insert', 'Other'],
toolMaterials: ['Carbide', 'HSS', 'Ceramic', 'Diamond (PCD)', 'Cermet', 'Other'],
toolsCurrentPage: 1,
toolsPerPage: 5,
pagedTools: [],
toolWearInterval: null,
exampleMode: false
};
},
computed: {
readyToRestore() {
return this.productionData.length > 0 &&
this.maintenanceData.length > 0 &&
this.selected &&
!this.selectedOrders.length;
},
pieChartSlices() {
if (!this.executionStats) return [];
const entries = Object.entries(this.executionStats);
const total = entries.reduce((sum, [, stat]) => sum + (stat.percentage || 0), 0) || 100;
let startAngle = -90;
return entries.map(([status, stat]) => {
const sweep = (stat.percentage / total) * 360;
const endAngle = startAngle + sweep;
const slice = {
status,
stat,
color: this.getStatusColor(status),
isFull: sweep >= 359.9,
path: this.describeArc(100, 100, 90, startAngle, endAngle)
};
startAngle = endAngle;
return slice;
});
},
selectedToolList() {
if (!this.selected) return [];
return this.toolList.filter(t => t.machine === this.selected.name1);
}
},
watch: {
msg(newVal) {
if (!newVal) return;
if (newVal.topic === 'broadcast_active_order') {
console.log('Received broadcast_active_order:', newVal.payload);
if (this.calcInterval) {
clearInterval(this.calcInterval);
this.calcInterval = null;
}
if (newVal.payload) {
this.selectedProductionOrder = newVal.payload;
localStorage.setItem('activeProductionOrder', JSON.stringify({
machine_uuid: this.selected?.uuid || null,
item: newVal.payload
}));
if (!newVal.payload.paused) {
this.startInterval(newVal.payload);
}
} else {
console.log('Clearing active production order (broadcast)');
this.selectedProductionOrder = null;
localStorage.removeItem('activeProductionOrder');
}
return;
}
if (!newVal.payload) return;
if (newVal.payload.machines) {
this.machines = newVal.payload.machines;
if (this.selected && this.selected.uuid) {
const updated = this.machines.find(m => m.uuid === this.selected.uuid);
if (updated) {
this.selected = updated;
this.refreshSelectedPaging();
}
}
}
if (newVal.payload.executionStats) {
this.executionStats = newVal.payload.executionStats;
}
if (newVal.payload.maintenanceData && this.machines) {
this.maintenanceData = newVal.payload.maintenanceData;
this.machines.forEach(machine => {
machine.maintenance = this.maintenanceData.filter(req =>
req.workcenter &&
req.workcenter.name === machine.name1 &&
req.workcenter.code === machine.uuid
);
});
if (this.selected) {
const updated = this.machines.find(m => m.uuid === this.selected.uuid);
if (updated) {
this.selected = updated;
this.refreshSelectedPaging();
}
}
}
if (newVal.payload.orders && this.selected && this.selected.uuid) {
this.selectedOrders = newVal.payload.orders.filter(
o => o.machine_id === this.selected.uuid
);
localStorage.setItem('selectedOrders', JSON.stringify(this.selectedOrders));
this.updatePagedOrders();
}
if (newVal.payload.productionData && typeof newVal.payload.productionData === 'object') {
this.productionData = newVal.payload.productionData;
// this used to only get filtered into selectedOrders inside
// selectMachine() — i.e. once, at click time — so the
// Manufacturing Orders table went stale until a page refresh
// re-ran selectMachine(). Re-deriving it here on every tick
// keeps it live, same as machines/maintenance above.
this.deriveOrdersForSelected();
}
if (newVal.payload.maintenanceData && typeof newVal.payload.maintenanceData === 'object') {
this.maintenanceData = newVal.payload.maintenanceData;
}
},
readyToRestore(ready) {
if (ready) {
console.log('Restoring machine and orders after refresh...');
this.selectMachine(this.selected);
}
},
selected() {
this.refreshSelectedPaging();
},
selectedOrders: {
deep: true,
handler() {
this.currentPage = 1;
this.updatePagedOrders();
}
}
},
methods: {
ceil(value) {
return Math.ceil(value);
},
refreshSelectedPaging() {
this.faultsCurrentPage = 1;
this.updatePagedFaults();
this.maintenanceCurrentPage = 1;
this.updatePagedMaintenance();
this.toolsCurrentPage = 1;
this.updatePagedTools();
},
updateTime() {
const now = new Date();
const malaysiaTime = new Date(now.toLocaleString('en-US', { timeZone: 'Asia/Kuala_Lumpur' }));
const hours = String(malaysiaTime.getHours()).padStart(2, '0');
const minutes = String(malaysiaTime.getMinutes()).padStart(2, '0');
const seconds = String(malaysiaTime.getSeconds()).padStart(2, '0');
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const day = days[malaysiaTime.getDay()];
const date = malaysiaTime.getDate();
const month = malaysiaTime.toLocaleString('default', { month: 'long' });
const year = malaysiaTime.getFullYear();
this.timeDisplay = `${hours}:${minutes}:${seconds}`;
this.dateDisplay = `${day}, ${date} ${month} ${year}`;
},
getExecutionColor(status) {
const colors = {
ACTIVE: '#28a745',
UNAVAILABLE: '#6c757d',
READY: '#007bff',
STOPPED: '#dc3545',
IDLE: '#ffc107'
};
return colors[status] || '#999';
},
getStatusColor(status) {
return this.getExecutionColor(status);
},
polarToCartesian(cx, cy, r, angleDeg) {
const angleRad = (angleDeg * Math.PI) / 180;
return { x: cx + r * Math.cos(angleRad), y: cy + r * Math.sin(angleRad) };
},
describeArc(cx, cy, r, startAngle, endAngle) {
const startPt = this.polarToCartesian(cx, cy, r, startAngle);
const endPt = this.polarToCartesian(cx, cy, r, endAngle);
const sweep = endAngle - startAngle;
const largeArcFlag = sweep > 180 ? 1 : 0;
return ['M', cx, cy, 'L', startPt.x, startPt.y, 'A', r, r, 0, largeArcFlag, 1, endPt.x, endPt.y, 'Z'].join(' ');
},
requestActiveOrder() {
console.log('Requesting current active order from flow...');
this.send({ topic: 'request_active_order' });
},
sendStartPayload(item) {
const now = Date.now();
this.send({
topic: 'start_workorder',
payload: {
id: item.workorder.id,
name: item.workorder.name,
order_id: item.order.id,
order_name: item.order.name,
product_name: item.order.product_id[1],
product_qty: item.order.product_qty,
planned_time: item.workorder.duration_expected || 0,
workorder_state: item.workorder.state,
workcenter_name: item.workorder.workcenter_id[1],
workcenter_code: item.workorder.code,
date_start: item.order.date_start,
timestamp: now
}
});
},
startInterval(item) {
if (this.calcInterval) clearInterval(this.calcInterval);
this.calcInterval = setInterval(() => {
if (!item || item.paused) return;
const now = Date.now();
this.send({
topic: 'workorder_raw',
payload: {
workorder_id: item.workorder.id,
workorder_name: item.workorder.name,
order_id: item.order.id,
order_name: item.order.name,
product_name: item.order.product_id[1],
product_qty: item.order.product_qty,
planned_time: item.workorder.duration_expected || 0,
workorder_state: item.workorder.state,
workcenter_name: item.workorder.workcenter_id[1],
workcenter_code: item.workorder.code,
date_start: item.order.date_start,
timestamp: now
}
});
}, 1000);
},
selectMachine(machine) {
this.selected = machine;
this.refreshSelectedPaging();
localStorage.setItem('selectedMachine', JSON.stringify(machine));
if (this.selectedOrders && this.selectedOrders.length > 0) {
localStorage.setItem('selectedOrders', JSON.stringify(this.selectedOrders));
}
// restore whatever's already running on this machine (if anything),
// then let deriveOrdersForSelected() build the pending list around it
this.selectedProductionOrder = this.startedOrders[machine.uuid] || null;
this.deriveOrdersForSelected();
},
startProduction(item) {
this.selectedProductionOrder = item;
item.paused = false;
localStorage.setItem('activeProductionOrder', JSON.stringify({
machine_uuid: this.selected?.uuid || null,
item
}));
if (this.selected && this.selected.uuid) {
this.startedOrders[this.selected.uuid] = item;
}
this.sendStartPayload(item);
this.startInterval(item);
this.send({ topic: 'sync_active_order', payload: item });
},
pauseProduction() {
if (this.selectedProductionOrder) {
this.selectedProductionOrder.paused = true;
if (this.calcInterval) {
clearInterval(this.calcInterval);
this.calcInterval = null;
}
const saved = localStorage.getItem('activeProductionOrder');
if (saved) {
const parsed = JSON.parse(saved);
parsed.item.paused = true;
localStorage.setItem('activeProductionOrder', JSON.stringify(parsed));
}
this.send({
topic: 'pause_workorder',
payload: { id: this.selectedProductionOrder.workorder.id, name: this.selectedProductionOrder.workorder.name }
});
}
this.send({ topic: 'sync_active_order', payload: this.selectedProductionOrder });
},
continueProduction() {
if (this.selectedProductionOrder) {
this.selectedProductionOrder.paused = false;
const saved = localStorage.getItem('activeProductionOrder');
if (saved) {
const parsed = JSON.parse(saved);
parsed.item.paused = false;
localStorage.setItem('activeProductionOrder', JSON.stringify(parsed));
}
this.send({
topic: 'continue_workorder',
payload: { id: this.selectedProductionOrder.workorder.id, name: this.selectedProductionOrder.workorder.name }
});
this.startInterval(this.selectedProductionOrder);
}
this.send({ topic: 'sync_active_order', payload: this.selectedProductionOrder });
},
doneProduction() {
if (this.selectedProductionOrder) {
if (this.calcInterval) {
clearInterval(this.calcInterval);
this.calcInterval = null;
}
this.send({
topic: 'finish_workorder',
payload: {
id: this.selectedProductionOrder.workorder.id,
name: this.selectedProductionOrder.workorder.name
}
});
this.send({ topic: 'sync_active_order', payload: null });
if (this.selected && this.selected.uuid) {
delete this.startedOrders[this.selected.uuid];
}
localStorage.removeItem('activeProductionOrder');
this.selectedProductionOrder = null;
}
},
isStarted(item) {
return this.selectedProductionOrder === item;
},
goBack() {
this.selected = null;
this.selectedOrders = [];
this.selectedProductionOrder = null;
this.showToolForm = false;
this.resetToolForm();
localStorage.removeItem('selectedMachine');
localStorage.removeItem('selectedOrders');
},
updatePagedOrders() {
const start = (this.currentPage - 1) * this.itemsPerPage;
const end = start + this.itemsPerPage;
this.pagedOrders = this.selectedOrders.slice(start, end);
},
// Re-filters productionData down to the pending work orders for
// whichever machine is selected right now. Pulled out of
// selectMachine() so it can ALSO be called every time a fresh
// productionData payload arrives (see the msg watcher) — previously
// this only ran once, at the moment you clicked a machine card, so
// the Manufacturing Orders table went stale until you refreshed the
// page (which re-ran selectMachine() via readyToRestore).
// Deliberately does NOT touch selectedProductionOrder — the
// currently-running order is only ever changed by explicit actions
// (startProduction/doneProduction) or a broadcast_active_order sync,
// never by this background refresh.
deriveOrdersForSelected() {
if (!this.selected || !Array.isArray(this.productionData)) {
this.selectedOrders = [];
this.currentPage = 1;
this.updatePagedOrders();
return;
}
const machine = this.selected;
const runningId = this.selectedProductionOrder ? this.selectedProductionOrder.workorder.id : null;
const orders = [];
this.productionData.forEach(order => {
(order.workorders || []).forEach(wo => {
if (wo.workcenter_id && wo.workcenter_id[1] === machine.name1 && wo.code === machine.uuid) {
if (runningId !== null && wo.id === runningId) return; // already shown in the "Current Work Order" panel
orders.push({ order, workorder: wo });
}
});
});
this.selectedOrders = orders;
localStorage.setItem('selectedOrders', JSON.stringify(this.selectedOrders));
if (this.currentPage > Math.ceil(orders.length / this.itemsPerPage)) {
this.currentPage = 1;
}
this.updatePagedOrders();
},
setPage(page) {
if (page >= 1 && page <= Math.ceil(this.selectedOrders.length / this.itemsPerPage)) {
this.currentPage = page;
this.updatePagedOrders();
}
},
updatePagedFaults() {
if (!this.selected || !this.selected.Faults) return;
const start = (this.faultsCurrentPage - 1) * this.faultsPerPage;
const end = start + this.faultsPerPage;
this.pagedFaults = this.selected.Faults.slice(start, end);
},
setFaultsPage(page) {
if (page >= 1 && page <= Math.ceil(this.selected.Faults.length / this.faultsPerPage)) {
this.faultsCurrentPage = page;
this.updatePagedFaults();
}
},
updatePagedMaintenance() {
if (!this.selected || !this.selected.maintenance) {
this.pagedMaintenance = [];
return;
}
const start = (this.maintenanceCurrentPage - 1) * this.maintenanceItemsPerPage;
const end = start + this.maintenanceItemsPerPage;
this.pagedMaintenance = this.selected.maintenance.slice(start, end);
},
setMaintenancePage(page) {
if (!this.selected || !this.selected.maintenance) return;
const maxPage = Math.ceil(this.selected.maintenance.length / this.maintenanceItemsPerPage);
if (page >= 1 && page <= maxPage) {
this.maintenanceCurrentPage = page;
this.updatePagedMaintenance();
}
},
publishToolLife() {
const payload = this.toolList.map(tool => ({
machine: tool.machine,
toolNumber: tool.toolNumber,
name: tool.name,
type: tool.type,
material: tool.material,
expectedLife: tool.expectedLife,
usedMins: tool.usedMins ?? 0,
remainingMins: tool.remainingMins ?? 0,
percentage: tool.percentage ?? 0,
status: tool.status ?? 'OK',
startTime: tool.startTime ?? null,
lastUpdatedTime: tool.lastUpdatedTime ?? null
}));
this.send({ topic: 'toollife', payload });
},
openToolForm() {
this.showToolForm = true;
},
cancelToolForm() {
this.showToolForm = false;
this.resetToolForm();
},
resetToolForm() {
this.newTool = { toolNumber: '', name: '', type: '', material: '', expectedLife: '' };
},
saveTool() {
if (!this.selected) return;
if (!this.newTool.toolNumber || !this.newTool.name || !this.newTool.type ||
!this.newTool.material || !this.newTool.expectedLife) {
alert('Please fill in all fields before saving.');
return;
}
const tool = {
id: Date.now(),
toolNumber: this.newTool.toolNumber,
name: this.newTool.name,
type: this.newTool.type,
material: this.newTool.material,
machine: this.selected.name1,
expectedLife: this.newTool.expectedLife,
usedSeconds: 0,
startTime: null,
lastUpdatedTime: null
};
this.recalcToolMetrics(tool);
this.toolList.push(tool);
localStorage.setItem('toolList', JSON.stringify(this.toolList));
this.publishToolLife();
this.resetToolForm();
this.showToolForm = false;
this.toolsCurrentPage = Math.ceil(this.selectedToolList.length / this.toolsPerPage) || 1;
this.updatePagedTools();
},
deleteTool(tool) {
this.toolList = this.toolList.filter(t => t.id !== tool.id);
localStorage.setItem('toolList', JSON.stringify(this.toolList));
this.publishToolLife();
const maxPage = Math.ceil(this.selectedToolList.length / this.toolsPerPage) || 1;
if (this.toolsCurrentPage > maxPage) this.toolsCurrentPage = maxPage;
this.updatePagedTools();
},
updatePagedTools() {
const start = (this.toolsCurrentPage - 1) * this.toolsPerPage;
const end = start + this.toolsPerPage;
this.pagedTools = this.selectedToolList.slice(start, end);
},
setToolsPage(page) {
const maxPage = Math.ceil(this.selectedToolList.length / this.toolsPerPage) || 1;
if (page >= 1 && page <= maxPage) {
this.toolsCurrentPage = page;
this.updatePagedTools();
}
},
recalcToolMetrics(tool) {
const usedMins = Math.round(((tool.usedSeconds || 0) / 60) * 10) / 10;
const expected = Number(tool.expectedLife) || 0;
let percentage = expected > 0 ? (usedMins / expected) * 100 : 0;
percentage = Math.min(100, Math.round(percentage));
const remainingMins = Math.max(0, Math.round((expected - usedMins) * 10) / 10);
let status;
let statusColor;
if (percentage > 90) {
status = 'Replace';
statusColor = '#d32f2f'; // red
} else if (percentage > 80) {
status = 'Critical';
statusColor = '#fd7e14'; // orange
} else if (percentage > 70) {
status = 'Warning';
statusColor = '#ffc107'; // amber
} else {
status = 'OK';
statusColor = '#28a745'; // green
}
tool.usedMins = usedMins;
tool.remainingMins = remainingMins;
tool.percentage = percentage;
tool.status = status;
tool.statusColor = statusColor;
},
evaluateToolWear() {
if (!this.toolList.length) return;
const machineList = this.machines.length > 0
? this.machines
: (this.selected ? [this.selected] : []);
if (!machineList.length) return;
let anyChanged = false;
machineList.forEach(machine => {
const isActive = machine.Execution === 'ACTIVE';
const isAuto = machine.ControllerMode === 'AUTOMATIC';
const spcc = Number(machine.SPCC) === 1;
const spcw = Number(machine.SPCW) === 1;
if (!isActive || !isAuto || !(spcc || spcw)) return;
const activeTcode = Number(machine['Device_FANUC_CNC_x:Tcode2']);
if (!activeTcode) return;
const candidates = this.toolList.filter(t =>
t.machine === machine.name1 &&
Number(t.toolNumber) === activeTcode
);
if (!candidates.length) return;
const activeCandidates = candidates.filter(t =>
(Number(t.percentage) || 0) < 100 && t.status !== 'Replace'
);
if (!activeCandidates.length) return;
const activeTool = activeCandidates.reduce((latest, t) =>
t.id > latest.id ? t : latest
);
activeTool.usedSeconds = (activeTool.usedSeconds || 0) + 1;
const now = new Date().toISOString();
if (!activeTool.startTime) {
activeTool.startTime = now;
}
activeTool.lastUpdatedTime = now;
this.recalcToolMetrics(activeTool);
anyChanged = true;
});
if (anyChanged) {
localStorage.setItem('toolList', JSON.stringify(this.toolList));
this.updatePagedTools();
this.publishToolLife();
}
},
loadToolLife() {
console.log("ToolLife button clicked");
this.toolMode = true
this.send({
payload: {
tab: 'ToolLife'
}
})
}
},
mounted() {
this.updateTime();
this.clockInterval = setInterval(this.updateTime, 1000);
const savedOrder = localStorage.getItem('activeProductionOrder');
if (savedOrder) {
try {
const parsed = JSON.parse(savedOrder);
this.selectedProductionOrder = parsed.item;
if (parsed.item.paused) {
this.selectedProductionOrder.paused = true;
}
if (parsed.machine_uuid) {
this.startedOrders[parsed.machine_uuid] = parsed.item;
}
if (!this.selectedProductionOrder.paused) {
this.startInterval(this.selectedProductionOrder);
}
} catch (e) {
console.error('Failed to restore saved work order:', e);
localStorage.removeItem('activeProductionOrder');
}
}
const savedMachine = localStorage.getItem('selectedMachine');
if (savedMachine) {
try {
this.selected = JSON.parse(savedMachine);
this.refreshSelectedPaging();
} catch (e) {
console.error('Failed to restore selected machine:', e);
localStorage.removeItem('selectedMachine');
}
}
const savedOrders = localStorage.getItem('selectedOrders');
if (savedOrders) {
try {
this.selectedOrders = JSON.parse(savedOrders);
this.updatePagedOrders();
} catch (e) {
console.error('Failed to restore selected orders:', e);
localStorage.removeItem('selectedOrders');
}
}
const savedTools = localStorage.getItem('toolList');
if (savedTools) {
try {
this.toolList = JSON.parse(savedTools);
this.toolList.forEach(t => this.recalcToolMetrics(t));
this.updatePagedTools();
this.publishToolLife();
} catch (e) {
console.error('Failed to restore tool list:', e);
localStorage.removeItem('toolList');
}
}
this.toolWearInterval = setInterval(this.evaluateToolWear, 1000);
setTimeout(() => this.requestActiveOrder(), 800);
setTimeout(() => this.requestActiveOrder(), 3000);
},
unmounted() {
if (this.clockInterval) clearInterval(this.clockInterval);
if (this.calcInterval) clearInterval(this.calcInterval);
if (this.toolWearInterval) clearInterval(this.toolWearInterval);
}
};
</script>