switch (msg.action) { case "upload-excel": msg._mode = "upload"; return [msg, null, null, null]; case "refresh-work-orders": msg._mode = "select"; msg.topic = "SELECT * FROM work_orders ORDER BY created_at DESC;"; return [null, msg, null, null]; case "start-work-order": { // PHASE 3: Check for existing progress before loading msg._mode = "check-progress"; const order = msg.payload || {}; if (!order.id) { node.error("No work order id supplied for start", msg); return [null, null, null, null]; } // Store order temporarily in flow context for resume/restart handlers flow.set("pendingWorkOrder", order); // Query database for existing progress msg.topic = "SELECT cycle_count, good_parts, progress_percent FROM work_orders WHERE work_order_id = ?"; msg.payload = [order.id]; return [null, null, msg, null]; } case "resume-work-order": { // PHASE 3: Resume existing work order from database state msg._mode = "resume"; const order = msg.payload || {}; const existingCycles = Number(order.cycle_count) || 0; const existingGoodParts = Number(order.good_parts) || 0; if (!order.id) { node.error("No work order id supplied for resume", msg); return [null, null, null, null]; } // Set status to RUNNING msg.topic = "UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END WHERE status <> 'DONE'"; msg.payload = [order.id, order.id]; // Initialize global state with existing values from database global.set("activeWorkOrder", order); global.set("cycleCount", existingCycles); flow.set("lastMachineState", 0); global.set("scrapPromptIssuedFor", null); node.warn(`[RESUME] Loaded WO ${order.id} with ${existingCycles} cycles, ${existingGoodParts} good parts`); return [null, null, msg, null]; } case "restart-work-order": { // PHASE 3: Restart work order from zero (reset database) msg._mode = "restart"; const order = msg.payload || {}; if (!order.id) { node.error("No work order id supplied for restart", msg); return [null, null, null, null]; } // Reset database values to 0 and set status to RUNNING msg.topic = "UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, cycle_count = 0, good_parts = 0, progress_percent = 0, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END WHERE status <> 'DONE'"; msg.payload = [order.id, order.id]; // Initialize global state to zero global.set("activeWorkOrder", order); global.set("cycleCount", 0); flow.set("lastMachineState", 0); global.set("scrapPromptIssuedFor", null); node.warn(`[RESTART] Reset WO ${order.id} to zero`); return [null, null, msg, null]; } case "complete-work-order": { // PHASE 4: Persist final counts before marking DONE msg._mode = "complete"; const order = msg.payload || {}; if (!order.id) { node.error("No work order id supplied for complete", msg); return [null, null, null, null]; } // Get current state values to persist const activeOrder = global.get("activeWorkOrder") || {}; const finalCycles = Number(global.get("cycleCount")) || 0; const cavities = Number(global.get("moldActive")) || 1; const scrapTotal = Number(activeOrder.scrap) || 0; const totalProduced = finalCycles * cavities; const finalGoodParts = Math.max(0, totalProduced - scrapTotal); msg.completeOrder = order; // PHASE 4: SQL updated to persist final counts msg.topic = "UPDATE work_orders SET status = 'DONE', cycle_count = ?, good_parts = ?, progress_percent = 100, updated_at = NOW() WHERE work_order_id = ?"; msg.payload = [finalCycles, finalGoodParts, order.id]; // Clear ALL state on completion global.set("activeWorkOrder", null); global.set("trackingEnabled", false); global.set("productionStarted", false); global.set("kpiStartupMode", false); global.set("operatingTime", 0); global.set("lastCycleTime", null); global.set("cycleCount", 0); flow.set("lastMachineState", 0); global.set("scrapPromptIssuedFor", null); node.warn(`[COMPLETE] Persisting final counts: cycles=${finalCycles}, good_parts=${finalGoodParts}. Cleared all state flags`); return [null, null, null, msg]; } case "get-current-state": { // Return current state for UI sync on tab switch const activeOrder = global.get("activeWorkOrder") || null; const trackingEnabled = global.get("trackingEnabled") || false; const productionStarted = global.get("productionStarted") || false; const kpis = global.get("currentKPIs") || { oee: 0, availability: 0, performance: 0, quality: 0 }; msg._mode = "current-state"; msg.payload = { activeWorkOrder: activeOrder, trackingEnabled: trackingEnabled, productionStarted: productionStarted, kpis: kpis }; return [null, msg, null, null]; } case "restore-session": { // Query DB for any RUNNING work order on startup msg._mode = "restore-query"; msg.topic = "SELECT * FROM work_orders WHERE status = 'RUNNING' LIMIT 1"; msg.payload = []; node.warn('[RESTORE] Checking for running work order on startup'); return [null, msg, null, null]; } case "scrap-entry": { const { id, scrap } = msg.payload || {}; const scrapNum = Number(scrap) || 0; if (!id) { node.error("No work order id supplied for scrap entry", msg); return [null, null, null, null]; } const activeOrder = global.get("activeWorkOrder"); if (activeOrder && activeOrder.id === id) { activeOrder.scrap = (Number(activeOrder.scrap) || 0) + scrapNum; global.set("activeWorkOrder", activeOrder); } global.set("scrapPromptIssuedFor", null); msg._mode = "scrap-update"; msg.scrapEntry = { id, scrap: scrapNum }; // SQL with bound parameters for safety msg.topic = "UPDATE work_orders SET scrap_parts = scrap_parts + ?, updated_at = NOW() WHERE work_order_id = ?"; msg.payload = [scrapNum, id]; return [null, null, msg, null]; } case "scrap-skip": { const { id, remindAgain } = msg.payload || {}; if (!id) { node.error("No work order id supplied for scrap skip", msg); return [null, null, null, null]; } if (remindAgain) { global.set("scrapPromptIssuedFor", null); } msg._mode = "scrap-skipped"; return [null, null, null, null]; } case "start": { // START with KPI timestamp init - FIXED const now = Date.now(); global.set("trackingEnabled", true); global.set("productionStarted", true); global.set("kpiStartupMode", true); global.set("kpiBuffer", []); global.set("lastKPIRecordTime", now - 60000); global.set("productionStartTime", now); global.set("lastMachineCycleTime", now); global.set("lastCycleTime", now); global.set("operatingTime", 0); node.warn('[START] Initialized: trackingEnabled=true, productionStarted=true, kpiStartupMode=true, operatingTime=0'); const activeOrder = global.get("activeWorkOrder") || {}; msg._mode = "production-state"; msg.payload = msg.payload || {}; msg.trackingEnabled = true; msg.productionStarted = true; msg.machineOnline = true; msg.payload.trackingEnabled = true; msg.payload.productionStarted = true; msg.payload.machineOnline = true; return [null, msg, null, null]; } case "stop": { global.set("trackingEnabled", false); global.set("productionStarted", false); node.warn('[STOP] Set trackingEnabled=false, productionStarted=false'); // Send UI update so button state reflects change msg._mode = "production-state"; msg.payload = msg.payload || {}; msg.trackingEnabled = false; msg.productionStarted = false; msg.machineOnline = true; msg.payload.trackingEnabled = false; msg.payload.productionStarted = false; msg.payload.machineOnline = true; return [null, msg, null, null]; } case "start-tracking": { const activeOrder = global.get('activeOrder') || {}; if (!activeOrder.id) { node.warn('[START] Cannot start tracking: No active order loaded.'); return [null, { topic: "alert", payload: "Error: No active work order loaded." }, null, null]; } const now = Date.now(); global.set("trackingEnabled", true); global.set("kpiBuffer", []); global.set("lastKPIRecordTime", now - 60000); global.set("lastMachineCycleTime", now); global.set("lastCycleTime", now); global.set("operatingTime", 0.001); node.warn('[START] Cleared kpiBuffer for fresh production run'); // FIX: Use work_order_id consistently const dbMsg = { topic: `UPDATE work_orders SET production_start_time = ${now}, is_tracking = 1 WHERE work_order_id = '${activeOrder.id}'`, payload: [] }; const stateMsg = { topic: "machineStatus", payload: msg.payload || {} }; stateMsg.payload.trackingEnabled = true; stateMsg.payload.productionStarted = true; stateMsg.payload.machineOnline = true; return [dbMsg, stateMsg, null, null]; } }