// InteractiveSimulator.jsx // // A rubycrm.ai-style live event panel: three buttons let a visitor force a // scenario (Optimal / Yield Loss Alert / GRN Backlog), and a log console // streams JSON payloads shaped exactly like the real backend's // Production/WIP event stream - // trans-backend/src/inventory/events/stock-movement.events.ts // (StockInEvent / StockOutEvent / GrnStockInEvent: companyId, productId, // warehouseId, quantity, stockMovementId, performedById, occurredAt) // trans-backend/src/entities/wastage-ledger.entity.ts // (stageId, productId, cycleId, accumulatedStageStockIn/Out, // remainingInProgressQuantity, wastageQuantity, wastagePercentage, // isCycleClosed, hasNegativeBalanceWarning, triggeredByEventAction) // trans-backend/src/types.ts (TargetEventAction enum: STOCK_IN, STOCK_OUT, // GRN_STOCK_IN, MANUAL_ADJUSTMENT) // // Scenario names are the real signals the data already carries - a // wastage-percentage spike with a negative WIP balance (hasNegativeBalanceWarning, // a real WastageLedger field) is labeled "Yield Loss Alert", and stalled // GRN receipts against continuing stock-out is labeled "GRN Backlog" - // not invented floor-automation buzzwords like a fictional temperature // sensor. No values here are live or connected to any company account - // every number is generated client-side on a timer, per SPEC-07 Section 5's // anti-hallucination guardrail for this exact component. // // Loaded as a classic (non-module) Babel-transformed script - see // index.html's header comment. React 19 ships no UMD global, so // `window.React` here is the bridge index.html's ESM import sets up; hooks // are called as `React.useState` etc. (fully qualified, not destructured at // top level) because sibling classic scripts share one global lexical // scope - a repeated top-level `const useState = ...` across files is a // SyntaxError there, not a harmless shadow. const SCENARIOS = { optimal: { key: "optimal", label: "Optimal", subtitle: "Yield within target range", dot: "bg-emerald-500", badge: "border-emerald-200 bg-emerald-50 text-emerald-700", ring: "ring-emerald-500/30", }, yieldAlert: { key: "yieldAlert", label: "Yield Loss Alert", subtitle: "Wastage spike, negative WIP balance flagged", dot: "bg-red-500", badge: "border-red-200 bg-red-50 text-red-700", ring: "ring-red-500/30", }, grnBacklog: { key: "grnBacklog", label: "GRN Backlog", subtitle: "GRN receipts lagging stage demand", dot: "bg-amber-500", badge: "border-amber-200 bg-amber-50 text-amber-700", ring: "ring-amber-500/30", }, }; const PRODUCTS = [ { id: "PC-8841", name: "HDPE Resin Pellets", unit: "KG" }, { id: "PC-2207", name: "Recycled PET Flakes", unit: "KG" }, { id: "PC-5519", name: "Woven Sack Fabric", unit: "MT" }, ]; const WAREHOUSES = [ { id: "WH-MAIN", name: "Main Warehouse" }, { id: "WH-DEPOT2", name: "Depot-2" }, ]; const STAGES = [ { id: "STG-EXT-01", name: "Extrusion" }, { id: "STG-BLND-02", name: "Blending" }, { id: "STG-PACK-03", name: "Packing" }, ]; const INITIAL_METRICS = { inputMass: 4820, wastage: 96, yieldLossPct: 2.1, negBalance: false }; function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function randomFloat(min, max, decimals = 2) { return parseFloat((Math.random() * (max - min) + min).toFixed(decimals)); } function randomId(prefix) { return `${prefix}_${Math.random().toString(16).slice(2, 10)}`; } function buildLogEntry(scenarioKey, tick) { const product = pick(PRODUCTS); const warehouse = pick(WAREHOUSES); const stage = pick(STAGES); const now = new Date().toISOString(); const emitWastageEval = tick % 3 === 2 || scenarioKey !== "optimal"; if (!emitWastageEval) { const eventType = scenarioKey === "grnBacklog" ? pick(["STOCK_OUT", "STOCK_OUT", "STOCK_IN"]) : pick(["STOCK_IN", "STOCK_OUT", "GRN_STOCK_IN"]); const eventNameMap = { STOCK_IN: "inventory.stock-in", STOCK_OUT: "inventory.stock-out", GRN_STOCK_IN: "inventory.grn-stock-in", }; const qty = scenarioKey === "grnBacklog" && eventType === "GRN_STOCK_IN" ? randomFloat(2, 15) : randomFloat(80, 420); return { tone: eventType === "STOCK_OUT" ? "neutral" : "positive", summary: `${eventType.replace(/_/g, " ")} · ${product.name} · ${warehouse.name} · ${qty} ${product.unit}`, json: { event: eventNameMap[eventType], payload: { companyId: randomId("co"), productId: product.id, warehouseId: warehouse.id, quantity: qty, stockMovementId: randomId("mv"), performedById: randomId("usr"), occurredAt: now, }, }, }; } const stockIn = scenarioKey === "grnBacklog" ? randomFloat(2, 20) : randomFloat(300, 600); const stockOut = scenarioKey === "yieldAlert" ? randomFloat(250, 520) : randomFloat(150, 350); const remaining = parseFloat((stockIn - stockOut).toFixed(2)); const wastage = scenarioKey === "yieldAlert" ? randomFloat(60, 140) : scenarioKey === "grnBacklog" ? randomFloat(5, 20) : randomFloat(2, 18); const wastagePct = parseFloat(((wastage / Math.max(stockIn, 1)) * 100).toFixed(2)); const negBalance = remaining < 0 || scenarioKey === "yieldAlert"; return { tone: scenarioKey === "yieldAlert" ? "danger" : scenarioKey === "grnBacklog" ? "warning" : "positive", summary: `WASTAGE EVAL · ${stage.name} · Yield Loss ${wastagePct}%${negBalance ? " ⚠ negative balance" : ""}`, json: { event: "production.wastage-ledger.evaluated", payload: { stageId: stage.id, productId: product.id, cycleId: randomId("cyc"), accumulatedStageStockIn: stockIn, accumulatedStageStockOut: stockOut, remainingInProgressQuantity: remaining, wastageQuantity: wastage, wastagePercentage: wastagePct, isCycleClosed: tick % 5 === 0, hasNegativeBalanceWarning: negBalance, triggeredByEventAction: scenarioKey === "grnBacklog" ? "STOCK_OUT" : "STOCK_IN", evaluatedAt: now, }, }, }; } function nextMetrics(scenarioKey, prev) { let { inputMass, wastage, yieldLossPct } = prev; let negBalance = prev.negBalance; if (scenarioKey === "optimal") { inputMass += randomFloat(40, 90); wastage += randomFloat(0.5, 2); yieldLossPct = parseFloat(Math.max(1, yieldLossPct + randomFloat(-0.3, 0.3)).toFixed(1)); negBalance = false; } else if (scenarioKey === "yieldAlert") { inputMass += randomFloat(10, 30); wastage += randomFloat(8, 22); yieldLossPct = parseFloat(Math.min(38, yieldLossPct + randomFloat(1.5, 4)).toFixed(1)); negBalance = yieldLossPct > 14; } else { inputMass += randomFloat(0, 8); wastage += randomFloat(1, 5); yieldLossPct = parseFloat(Math.min(22, yieldLossPct + randomFloat(0.5, 1.8)).toFixed(1)); negBalance = true; } return { inputMass: parseFloat(inputMass.toFixed(1)), wastage: parseFloat(wastage.toFixed(1)), yieldLossPct, negBalance, }; } const TONE_STYLES = { positive: { dot: "bg-emerald-500", text: "text-emerald-300" }, neutral: { dot: "bg-blue-300", text: "text-blue-200" }, warning: { dot: "bg-amber-400", text: "text-amber-300" }, danger: { dot: "bg-red-400", text: "text-red-300" }, }; function StatTile({ label, value, unit, danger }) { return (
{label}
{value} {unit ? {unit} : null}
); } function InteractiveSimulator() { const [scenario, setScenario] = React.useState("optimal"); const [tick, setTick] = React.useState(0); const [logs, setLogs] = React.useState([]); const [metrics, setMetrics] = React.useState(INITIAL_METRICS); const logRef = React.useRef(null); React.useEffect(() => { const interval = setInterval(() => setTick((t) => t + 1), 2200); return () => clearInterval(interval); }, []); React.useEffect(() => { if (tick === 0) return; const entry = buildLogEntry(scenario, tick); setLogs((prev) => [...prev.slice(-24), { id: `${tick}-${Date.now()}`, ...entry }]); setMetrics((prev) => nextMetrics(scenario, prev)); }, [tick]); // eslint-disable-line React.useEffect(() => { if (logRef.current) logRef.current.scrollTop = logRef.current.scrollHeight; }, [logs]); const handleScenario = (key) => { if (key === scenario) return; setScenario(key); setTick(0); setLogs([]); setMetrics(INITIAL_METRICS); }; const active = SCENARIOS[scenario]; return (
{/* Header / status */}

Workflow Engine - Live Event Monitor

{active.label}

{active.subtitle}

{/* Scenario trigger buttons - Accent Green per SPEC-07 UI direction */}
{Object.values(SCENARIOS).map((s) => { const isActive = s.key === scenario; return ( ); })}
{/* KPI tiles - reuse the real dashboard's own widget titles/units */}
12} />
{/* Log console */}
Workflow Engine Event Stream streaming
{logs.length === 0 ? (

Waiting for the first event…

) : ( logs.map((entry) => { const tone = TONE_STYLES[entry.tone]; return (
{entry.summary}
{JSON.stringify(entry.json, null, 2)}
                  
); }) )}
{/* Mandatory disclaimer - SPEC-07 Section 5 */}
Simulated for demonstration only. Event names and field structure mirror SkelBiz's real Workflow Engine and Wastage Ledger, but every value here is generated in your browser and is not connected to any live company account.
); }