import { configState } from './js/core/ConfigState.js'; import { SettingsModal } from './js/components/SettingsModal.js'; import { AreaPopovers } from './js/components/AreaPopovers.js'; import { dashboardStats } from './js/core/DashboardStats.js'; import { simulationClient } from './js/core/SimulationClient.js'; import { analyticsModal } from './js/ui/AnalyticsModal.js'; import { tableRenderer } from './js/ui/TableRenderer.js'; import { staffRenderer } from './js/ui/StaffRenderer.js'; import { customerRenderer } from './js/ui/CustomerRenderer.js'; // Initialize UI Components const settingsModal = new SettingsModal(); const areaPopovers = new AreaPopovers(); // Listen to configuration updates to save state configState.addEventListener('updated', (e) => { localStorage.setItem('catsySimConfig', JSON.stringify(e.detail)); const toast = document.getElementById('toast'); if (toast) { toast.style.opacity = '1'; clearTimeout(toast.timeoutId); toast.timeoutId = setTimeout(() => { toast.style.opacity = '0'; }, 3000); } // We intentionally do NOT auto-restart startSimulation() here. // The user must manually click 'Restart' to apply new settings, // to avoid wiping the active simulation UI abruptly when tweaking settings. }); document.addEventListener('DOMContentLoaded', () => { loadConfigFromStorage(); }); function loadConfigFromStorage() { const saved = localStorage.getItem('catsySimConfig'); if (!saved) return; try { const config = JSON.parse(saved); const directMap = { 'cashiers': 'cfg-cashiers', 'baristas': 'cfg-baristas', 'tables': 'cfg-tables', 'resArrivalMin': 'cfg-res-arrival-min', 'resArrivalMax': 'cfg-res-arrival-max', 'arrival': 'cfg-arrival', 'duration': 'cfg-duration', 'warmupTime': 'cfg-warmup', 'balkThreshold': 'cfg-balk-threshold', 'maxStrikes': 'cfg-max-strikes', 'decideMin': 'cfg-decide-min', 'decideMax': 'cfg-decide-max', 'payMin': 'cfg-pay-min', 'payMax': 'cfg-pay-max', 'prepMin': 'cfg-prep-min', 'prepMax': 'cfg-prep-max', 'dwellMin': 'cfg-dwell-min', 'dwellMax': 'cfg-dwell-max', 'replications': 'cfg-replications', 'shiftHours': 'cfg-shift-hours' }; const probMap = { 'takeoutProb': 'cfg-takeout-prob', 'resProb': 'cfg-res-prob', 'balkProb': 'cfg-balk-prob', 'renegeProb': 'cfg-renege-prob' }; for (const [key, domId] of Object.entries(directMap)) { if (config[key] !== undefined) { const el = document.getElementById(domId); if (el) el.value = config[key]; } } for (const [key, domId] of Object.entries(probMap)) { if (config[key] !== undefined) { const el = document.getElementById(domId); if (el) el.value = config[key] * 100; // Convert back to percentage } } // Also update the scenario select box if it matches an arrival value const scenarioSelect = document.getElementById('cfg-scenario'); if (scenarioSelect && config.arrival) { let match = false; for (let option of scenarioSelect.options) { if (option.value == config.arrival && option.value !== 'custom') { scenarioSelect.value = option.value; match = true; break; } } if (!match) scenarioSelect.value = 'custom'; } // Update the span displays for quick controls const dispCashiers = document.getElementById('disp-cashiers'); if (dispCashiers && config.cashiers) dispCashiers.innerText = config.cashiers; const dispBaristas = document.getElementById('disp-baristas'); if (dispBaristas && config.baristas) dispBaristas.innerText = config.baristas; const dispTables = document.getElementById('disp-tables'); if (dispTables && config.tables) dispTables.innerText = config.tables; } catch (e) { console.error('Failed to parse saved config', e); } } configState.refreshFromDOM(); const btnStart = document.getElementById('btn-start'); const btnStop = document.getElementById('btn-stop'); const btnRestart = document.getElementById('btn-restart'); const statusDot = document.getElementById('connection-status'); const statusText = document.getElementById('status-text'); const floatingLayer = document.getElementById('floating-layer'); let customerTableMap = new Map(); // Maps customer ID to table element ID let customerFrustrationMap = new Map(); // Maps customer ID to frustration level (0-3) let cashierQueueLens = []; // Array of lengths per cashier let waitingAreaLen = 0; let pickupAreaLen = 0; let totalCustomers = 0; let servedCustomers = 0; let lostCustomers = 0; let totalDineIn = 0; let totalTakeout = 0; let totalReservations = 0; let missedReservations = 0; const cashierOffsets = new Map(); const customerCashierMap = new Map(); const waitingAreaOffsets = new Map(); const pickupAreaOffsets = new Map(); const customerBaristaMap = new Map(); let availableTables = []; let availableResTables = []; let cashierTimers = {}; let baristaTimers = {}; function enforceBounds() { const cashiersEl = document.getElementById('cfg-cashiers'); if (cashiersEl) { let cVal = parseInt(cashiersEl.value); if (isNaN(cVal) || cVal < 1) cashiersEl.value = 1; else if (cVal > 10) cashiersEl.value = 10; } const baristasEl = document.getElementById('cfg-baristas'); if (baristasEl) { let bVal = parseInt(baristasEl.value); if (isNaN(bVal) || bVal < 1) baristasEl.value = 1; else if (bVal > 15) baristasEl.value = 15; } } function startSimulation() { enforceBounds(); // Ensure the backend isn't stuck in a paused state (e.g. if the user hit restart while the settings modal was open) fetch('/api/resume', { method: 'POST' }).catch(e => console.error(e)); simulationClient.resume(); // also ensure frontend is unpaused simulationClient.connect(); } window.addEventListener('sim:connected', () => { if (statusDot) statusDot.className = 'dot green'; if (statusText) statusText.innerText = 'Connected'; if (btnStart) { btnStart.disabled = false; btnStart.innerText = '⏸ Pause'; btnStart.classList.add('playing'); } if (btnStop) btnStop.disabled = false; if (btnRestart) btnRestart.disabled = false; const warmupTime = configState.getConfig().warmupTime || 0; const warmupIndicator = document.getElementById('warmup-indicator'); if (warmupIndicator) { if (warmupTime > 0) { warmupIndicator.style.display = 'inline-block'; } else { warmupIndicator.style.display = 'none'; } } resetSimulation(); }); window.addEventListener('sim:disconnected', () => { if (statusDot) statusDot.className = 'status-indicator__dot status-indicator__dot--disconnected'; if (statusText) statusText.innerText = 'Disconnected'; if (btnStart) { btnStart.disabled = false; btnStart.innerText = '▶ Play'; btnStart.classList.remove('playing'); } if (btnStop) btnStop.disabled = true; if (btnRestart) btnRestart.disabled = true; // Stop all visual timers immediately for (const key in cashierTimers) { if (cashierTimers[key] && cashierTimers[key].interval) { clearInterval(cashierTimers[key].interval); } } for (const key in baristaTimers) { if (baristaTimers[key] && baristaTimers[key].interval) { clearInterval(baristaTimers[key].interval); } } staffRenderer.clearAllTimers(); }); window.addEventListener('sim:event', (e) => { handleEvent(e.detail); }); btnStart.addEventListener('click', () => { // If not connected, start the simulation if (!simulationClient.socket || simulationClient.socket.readyState !== WebSocket.OPEN) { startSimulation(); return; } // If already connected, toggle pause/resume if (simulationClient.isPaused) { // Resume simulationClient.resume(); fetch('/api/resume', { method: 'POST' }).catch(e => console.error(e)); btnStart.innerText = '⏸ Pause'; btnStart.classList.add('playing'); } else { // Pause simulationClient.pause(); fetch('/api/pause', { method: 'POST' }).catch(e => console.error(e)); btnStart.innerText = '▶ Resume'; btnStart.classList.remove('playing'); } }); btnStop.addEventListener('click', () => { simulationClient.disconnect(); btnStart.disabled = false; btnStop.disabled = true; if (btnRestart) btnRestart.disabled = true; }); if (btnRestart) { btnRestart.addEventListener('click', () => { simulationClient.disconnect(); // Slight delay to ensure the disconnect processes completely before reconnecting setTimeout(() => { startSimulation(); }, 100); }); } function resetSimulation() { floatingLayer.innerHTML = ''; cashierOffsets.clear(); customerCashierMap.clear(); waitingAreaOffsets.clear(); pickupAreaOffsets.clear(); customerBaristaMap.clear(); // Clear all manual intervals for (const key in cashierTimers) { if (cashierTimers[key] && cashierTimers[key].interval) { clearInterval(cashierTimers[key].interval); } } cashierTimers = {}; for (const key in baristaTimers) { if (baristaTimers[key] && baristaTimers[key].interval) { clearInterval(baristaTimers[key].interval); } } baristaTimers = {}; staffRenderer.clearAllTimers(); customerRenderer.clearAll(); customerTableMap.clear(); customerFrustrationMap.clear(); totalCustomers = 0; servedCustomers = 0; lostCustomers = 0; totalDineIn = 0; totalTakeout = 0; cashierQueueLens = []; waitingAreaLen = 0; pickupAreaLen = 0; // Reset Dashboard UI values const paceEl = document.getElementById('current-pace'); if (paceEl) paceEl.innerText = 'Quiet 💤'; document.getElementById('sim-time').innerText = '0.0s'; document.getElementById('total-customers').innerText = '0'; document.getElementById('served-customers').innerText = '0'; document.getElementById('lost-customers').innerText = '0'; document.getElementById('cashier-queue-len').innerText = '0'; document.getElementById('waiting-area-len').innerText = '0'; // Dynamically draw cashiers const cashiersCount = parseInt(document.getElementById('cfg-cashiers').value); staffRenderer.drawCashiers(cashiersCount); // Initialize queue lengths array cashierQueueLens = Array(cashiersCount).fill(0); // Dynamically draw baristas const baristasCount = parseInt(document.getElementById('cfg-baristas').value); staffRenderer.drawBaristas(baristasCount); // Dynamically draw tables const tablesCount = parseInt(document.getElementById('cfg-tables').value); tableRenderer.draw(tablesCount); } function updateStats(time) { document.getElementById('sim-time').innerText = time.toFixed(1) + 's'; document.getElementById('total-customers').innerText = totalCustomers; document.getElementById('served-customers').innerText = servedCustomers; document.getElementById('lost-customers').innerText = lostCustomers; document.getElementById('total-dine-in').innerText = totalDineIn; document.getElementById('total-takeout').innerText = totalTakeout; // Sum all individual cashier queues const totalCashierQueue = cashierQueueLens.reduce((a, b) => a + b, 0); document.getElementById('cashier-queue-len').innerText = totalCashierQueue; document.getElementById('waiting-area-len').innerText = waitingAreaLen + pickupAreaLen; } function handleEvent(data) { if (tableRenderer && data.time) { tableRenderer.tickTimers(data.time); } if (data.event === 'warmup_complete') { document.getElementById('warmup-indicator').style.display = 'none'; totalCustomers = 0; servedCustomers = 0; lostCustomers = 0; totalDineIn = 0; totalTakeout = 0; totalReservations = 0; missedReservations = 0; document.getElementById('total-customers').innerText = totalCustomers; document.getElementById('served-customers').innerText = servedCustomers; document.getElementById('lost-customers').innerText = lostCustomers; document.getElementById('total-dine-in').innerText = totalDineIn; document.getElementById('total-takeout').innerText = totalTakeout; const totalResEl = document.getElementById('total-reservations'); if (totalResEl) totalResEl.innerText = totalReservations; const missedResEl = document.getElementById('missed-reservations'); if (missedResEl) missedResEl.innerText = missedReservations; return; } if (data.event === 'pace_change') { const paceEl = document.getElementById('current-pace'); if (paceEl) { const pace = data.pace; let color = '#3498db'; // Steady let text = 'Steady ☕'; if (pace === 'Peak') { color = '#e74c3c'; text = 'Peak 🔥'; } else if (pace === 'Quiet') { color = '#9b59b6'; text = 'Quiet 💤'; } paceEl.style.color = color; paceEl.innerText = text; } return; } const id = data.customer_id; switch(data.event) { case 'arrive': totalCustomers++; document.getElementById('total-customers').innerText = totalCustomers; const numId = parseInt(id.replace('Cust-', '')) || 0; customerRenderer.moveCustomer(id, 'entrance', numId); break; case 'balking_start': const balkStartEl = customerRenderer.customers.get(id); if (balkStartEl) balkStartEl.classList.add('customer--walkout'); customerRenderer.incrementFrustration(id, 4); break; case 'balk_leave': lostCustomers++; const balkEl = customerRenderer.customers.get(id); if (balkEl) { const badge = balkEl.querySelector('.customer__badge'); if (badge) { badge.innerText = '🙅'; badge.style.display = 'flex'; } // Storm out from entrance balkEl.classList.add('customer--walkout'); balkEl.style.transition = 'transform 3.5s ease-in, opacity 3.5s ease-in'; balkEl.style.transform += ' translate(-80px, 200px)'; balkEl.style.opacity = '0'; setTimeout(() => { customerRenderer.removeCustomer(id); }, 3500); } else { customerRenderer.removeCustomer(id); } break; case 'frustrated_waiting': customerRenderer.incrementFrustration(id, 1); break; case 'patience_warning': customerRenderer.incrementFrustration(id, 2); break; case 'prep_waiting_frustration': customerRenderer.incrementFrustration(id, 3); break; case 'renege_leave': lostCustomers++; // Count them as a lost walkout // Remove them from the queue tracking const cIdxToLeave = data.cashier_index; if (isNaN(cashierQueueLens[cIdxToLeave]) || cashierQueueLens[cIdxToLeave] === undefined) { cashierQueueLens[cIdxToLeave] = 0; } cashierQueueLens[cIdxToLeave] = Math.max(0, cashierQueueLens[cIdxToLeave] - 1); const leavingOffset = cashierOffsets.get(id); // Advance everyone behind them forward for (let [custId, cIdx] of customerCashierMap.entries()) { if (cIdx === cIdxToLeave && custId !== id) { let currentOffset = cashierOffsets.get(custId); if (currentOffset !== undefined && currentOffset > leavingOffset) { let newOffset = currentOffset - 1; cashierOffsets.set(custId, newOffset); customerRenderer.moveCustomer(custId, `cashier-queue-${cIdxToLeave}`, newOffset); } } } // Remove from maps cashierOffsets.delete(id); customerCashierMap.delete(id); // Animate them storming out in frustration const renegeEl = customerRenderer.customers.get(id); if (renegeEl) { renegeEl.classList.add('customer--walkout'); const badge = renegeEl.querySelector('.customer__badge'); if (badge) { badge.innerText = '🏃'; badge.animate([ { transform: 'scale(1)' }, { transform: 'scale(1.5)' }, { transform: 'scale(1)' } ], { duration: 600, iterations: Infinity, easing: 'ease-in-out' }); } // Instantly step OUT of the queue so the person behind them doesn't walk through them renegeEl.style.transform += ' translate(0px, 40px)'; // Wait for 2 seconds so the user can clearly see them get frustrated setTimeout(() => { // Then they storm out completely renegeEl.style.transition = 'transform 3.5s ease-in, opacity 3.5s ease-in'; renegeEl.style.transform += ' translate(-150px, 200px)'; renegeEl.style.opacity = '0'; setTimeout(() => { if (renegeEl.parentNode) renegeEl.parentNode.removeChild(renegeEl); customerRenderer.customers.delete(id); }, 3500); }, 2000); } break; case 'vip_checkin': const vipCashierIdx = data.cashier_index !== undefined ? data.cashier_index : 0; customerRenderer.moveCustomer(id, `vip-checkin-${vipCashierIdx}`); customerRenderer.markAsVIP(id); // Small timeout to let the movement start before triggering the animation setTimeout(() => { customerRenderer.triggerVIPAnimation(id); }, 300); break; case 'queue_cashier': const cIdx = data.cashier_index; customerCashierMap.set(id, cIdx); if (isNaN(cashierQueueLens[cIdx]) || cashierQueueLens[cIdx] === undefined) { cashierQueueLens[cIdx] = 0; } cashierQueueLens[cIdx]++; cashierOffsets.set(id, cashierQueueLens[cIdx] - 1); customerRenderer.moveCustomer(id, `cashier-queue-${cIdx}`, cashierQueueLens[cIdx] - 1); break; case 'start_deciding': const decIdx = data.cashier_index; if (isNaN(cashierQueueLens[decIdx]) || cashierQueueLens[decIdx] === undefined) { cashierQueueLens[decIdx] = 0; } cashierQueueLens[decIdx] = Math.max(0, cashierQueueLens[decIdx] - 1); customerRenderer.moveCustomer(id, `cashier-queue-${decIdx}`, -1); const decEl = customerRenderer.customers.get(id); if (decEl) { const badge = decEl.querySelector('.customer__badge'); if (badge) { badge.innerText = '💭'; badge.style.display = 'flex'; } } // Start the cashier countdown timer! if (data.duration) { if (cashierTimers[decIdx]) clearInterval(cashierTimers[decIdx].interval); let elapsed = 0; const totalDuration = data.duration; const speedFactor = 0.5; // Backend RealtimeEnvironment factor const realTickMs = 100; const simTickSec = (realTickMs / 1000) / speedFactor; const timerEl = document.getElementById(`timer-cashier-${decIdx}`); if (timerEl) { timerEl.innerText = '0.0s'; timerEl.classList.remove('idle'); } const interval = setInterval(() => { if (simulationClient && simulationClient.isPaused) return; elapsed += simTickSec; if (elapsed >= totalDuration) { elapsed = totalDuration; clearInterval(interval); // Freeze on final time until customer walks away if (timerEl) timerEl.innerText = elapsed.toFixed(1) + 's'; } else { if (timerEl) timerEl.innerText = elapsed.toFixed(1) + 's'; } }, realTickMs); cashierTimers[decIdx] = { interval }; } // Advance everyone else in this specific queue forward by 1 spot for (let [custId, cIdx] of customerCashierMap.entries()) { if (cIdx === decIdx && custId !== id) { let currentOffset = cashierOffsets.get(custId); if (currentOffset !== undefined && currentOffset > 0) { let newOffset = currentOffset - 1; cashierOffsets.set(custId, newOffset); customerRenderer.moveCustomer(custId, `cashier-queue-${decIdx}`, newOffset); } } } break; case 'start_paying': // Customer is paying, no physical move needed, just logical state const payingCIdx = customerCashierMap.get(id); if (payingCIdx !== undefined) { const cAnim = document.getElementById(`working-anim-cashier-${payingCIdx}`); if (cAnim) cAnim.style.display = 'block'; } const payEl = customerRenderer.customers.get(id); if (payEl) { const badge = payEl.querySelector('.customer__badge'); if (badge) { badge.innerText = '💳'; badge.style.display = 'flex'; } } break; case 'waiting_pickup': totalTakeout++; document.getElementById('total-takeout').innerText = totalTakeout; // Go to waiting/pickup area if tables are full, or if waiting for takeout pickupAreaLen++; pickupAreaOffsets.set(id, pickupAreaLen - 1); customerRenderer.moveCustomer(id, 'pickup-area', pickupAreaLen - 1); customerRenderer.updateFrustrationVisuals(id); // Customer left the cashier, reset the cashier timer const leftCIdxTakeout = customerCashierMap.get(id); if (leftCIdxTakeout !== undefined && cashierTimers[leftCIdxTakeout]) { clearInterval(cashierTimers[leftCIdxTakeout].interval); const timerEl = document.getElementById(`timer-cashier-${leftCIdxTakeout}`); if (timerEl) { timerEl.innerText = 'Idle'; timerEl.classList.add('idle'); } const cAnim = document.getElementById(`working-anim-cashier-${leftCIdxTakeout}`); if (cAnim) cAnim.style.display = 'none'; showPopUpIcon(`cashier-staff-${leftCIdxTakeout}`, '💰'); } break; case 'waiting_table': totalDineIn++; document.getElementById('total-dine-in').innerText = totalDineIn; // Go to waiting/pickup area if tables are full, or if waiting for takeout waitingAreaLen++; waitingAreaOffsets.set(id, waitingAreaLen - 1); customerRenderer.moveCustomer(id, 'waiting-area', waitingAreaLen - 1); customerRenderer.updateFrustrationVisuals(id); // Customer left the cashier, reset the cashier timer const leftCIdx = customerCashierMap.get(id); if (leftCIdx !== undefined && cashierTimers[leftCIdx]) { clearInterval(cashierTimers[leftCIdx].interval); const timerEl = document.getElementById(`timer-cashier-${leftCIdx}`); if (timerEl) { timerEl.innerText = 'Idle'; timerEl.classList.add('idle'); } const cAnim = document.getElementById(`working-anim-cashier-${leftCIdx}`); if (cAnim) cAnim.style.display = 'none'; showPopUpIcon(`cashier-staff-${leftCIdx}`, '💰'); } break; case 'table_reserved': totalReservations++; const totalResEl = document.getElementById('total-reservations'); if (totalResEl) totalResEl.innerText = totalReservations; tableRenderer.setReserved(data.table_id, data.time, data.limit); break; case 'table_unreserved': tableRenderer.setUnreserved(data.table_id); break; case 'missed_reservation': missedReservations++; const missedResEl = document.getElementById('missed-reservations'); if (missedResEl) missedResEl.innerText = missedReservations; break; case 'seated_waiting_order': waitingAreaLen = Math.max(0, waitingAreaLen - 1); const tableId = data.table_id; customerTableMap.set(id, tableId); customerRenderer.moveCustomer(id, `table-${tableId}`); tableRenderer.setOccupied(tableId, data.time, data.is_reservation, 'waiting'); break; case 'start_prep': // Barista started making it. We don't move the customer, they are still at the table. const bIdx = data.barista_index; customerBaristaMap.set(id, bIdx); const bStatus = document.getElementById(`status-barista-${bIdx}`); if (bStatus) { bStatus.innerText = `Prep for #${id.replace('Cust-', '')}`; bStatus.style.color = '#e94f37'; } const bAnim = document.getElementById(`working-anim-${bIdx}`); if (bAnim) bAnim.style.display = 'block'; // Start the barista countdown timer if (data.duration) { if (baristaTimers[bIdx]) clearInterval(baristaTimers[bIdx].interval); let elapsed = 0; const totalDuration = data.duration; const speedFactor = 0.5; // Backend RealtimeEnvironment factor const realTickMs = 100; const simTickSec = (realTickMs / 1000) / speedFactor; const timerEl = document.getElementById(`timer-barista-${bIdx}`); if (timerEl) { timerEl.innerText = '0.0s'; timerEl.classList.remove('idle'); } const interval = setInterval(() => { if (simulationClient && simulationClient.isPaused) return; elapsed += simTickSec; if (elapsed >= totalDuration) { elapsed = totalDuration; clearInterval(interval); if (timerEl) timerEl.innerText = elapsed.toFixed(1) + 's'; } else { if (timerEl) timerEl.innerText = elapsed.toFixed(1) + 's'; } }, realTickMs); baristaTimers[bIdx] = { interval }; } break; case 'served': customerRenderer.frustrationMap.set(id, 0); customerRenderer.updateFrustrationVisuals(id); // Order arrived! Time to eat. const tId = customerTableMap.get(id); if (tId) { // If it's a reserved customer, badge has already been hidden during setOccupied, but pass false to be safe tableRenderer.setOccupied(tId, data.time, false, 'eating'); } const finishedBIdx = customerBaristaMap.get(id); if (finishedBIdx !== undefined) { if (baristaTimers[finishedBIdx]) clearInterval(baristaTimers[finishedBIdx].interval); const timerEl = document.getElementById(`timer-barista-${finishedBIdx}`); if (timerEl) { timerEl.innerText = 'Idle'; timerEl.classList.add('idle'); } const finishedBStatus = document.getElementById(`status-barista-${finishedBIdx}`); if (finishedBStatus) { finishedBStatus.innerText = 'Idle'; finishedBStatus.style.color = '#aaa'; } const bAnim = document.getElementById(`working-anim-${finishedBIdx}`); if (bAnim) bAnim.style.display = 'none'; customerBaristaMap.delete(id); showPopUpIcon(`barista-staff-${finishedBIdx}`, '☕'); } break; case 'leave': servedCustomers++; const el = customerRenderer.customers.get(id); if (el) { el.style.transform += ' translate(500px, 0)'; el.style.opacity = '0'; setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); customerRenderer.customers.delete(id); }, 800); } // Check if they were in the waiting/pickup area (Takeout customers) if (waitingAreaOffsets.has(id)) { waitingAreaLen = Math.max(0, waitingAreaLen - 1); waitingAreaOffsets.delete(id); } else if (pickupAreaOffsets.has(id)) { pickupAreaLen = Math.max(0, pickupAreaLen - 1); pickupAreaOffsets.delete(id); } // Clear table status and mark table as available again (Dine-in customers) const leftTableId = customerTableMap.get(id); if (leftTableId) { tableRenderer.setEmpty(leftTableId); customerTableMap.delete(id); } break; case 'sim_end': statusDot.className = 'dot'; statusDot.style.backgroundColor = '#4a90e2'; statusText.innerText = 'Completed'; break; } updateStats(data.time); }