// Configuration let config = { apiBaseUrl: '/biz.payfrit.com/api', businessId: null, servicePointId: null, refreshInterval: 5000, // 5 seconds }; // State let orders = []; let refreshTimer = null; // Status ID mapping const STATUS = { NEW: 1, PREPARING: 2, READY: 3, COMPLETED: 4 }; const STATUS_NAMES = { 1: 'New', 2: 'Preparing', 3: 'Ready', 4: 'Completed' }; // Initialize document.addEventListener('DOMContentLoaded', () => { loadConfig(); startAutoRefresh(); }); // Load config from localStorage function loadConfig() { const saved = localStorage.getItem('kds_config'); if (saved) { try { const parsed = JSON.parse(saved); config.businessId = parsed.businessId || null; config.servicePointId = parsed.servicePointId || null; config.refreshInterval = (parsed.refreshInterval || 5) * 1000; document.getElementById('businessIdInput').value = config.businessId || ''; document.getElementById('servicePointIdInput').value = config.servicePointId || ''; document.getElementById('refreshIntervalInput').value = config.refreshInterval / 1000; } catch (e) { console.error('Failed to load config:', e); } } // If no business ID, show config panel if (!config.businessId) { toggleConfig(); } } // Save config to localStorage function saveConfig() { const businessId = parseInt(document.getElementById('businessIdInput').value) || null; const servicePointId = parseInt(document.getElementById('servicePointIdInput').value) || null; const refreshInterval = parseInt(document.getElementById('refreshIntervalInput').value) || 5; if (!businessId) { alert('Business ID is required'); return; } config.businessId = businessId; config.servicePointId = servicePointId; config.refreshInterval = refreshInterval * 1000; localStorage.setItem('kds_config', JSON.stringify({ businessId: config.businessId, servicePointId: config.servicePointId, refreshInterval: refreshInterval })); toggleConfig(); location.reload(); } // Toggle config panel function toggleConfig() { const panel = document.getElementById('configPanel'); panel.classList.toggle('show'); } // Start auto-refresh function startAutoRefresh() { if (!config.businessId) return; loadOrders(); if (refreshTimer) clearInterval(refreshTimer); refreshTimer = setInterval(loadOrders, config.refreshInterval); } // Load orders from API async function loadOrders() { if (!config.businessId) return; console.log('[loadOrders] Fetching with BusinessID:', config.businessId, 'ServicePointID:', config.servicePointId); try { const url = `${config.apiBaseUrl}/orders/listForKDS.cfm`; console.log('[loadOrders] URL:', url); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ BusinessID: config.businessId, ServicePointID: config.servicePointId || 0 }) }); console.log('[loadOrders] Response status:', response.status); const data = await response.json(); console.log('[loadOrders] Response data:', data); if (data.OK) { orders = data.ORDERS || []; console.log('[loadOrders] Orders received:', orders.length); if (orders.length > 0) { console.log('[loadOrders] First order LineItems:', orders[0].LineItems); } renderOrders(); updateStatus(true, `${orders.length} active orders`); } else { console.error('[loadOrders] API returned OK=false:', data); updateStatus(false, `Error: ${data.MESSAGE || data.ERROR}`); } } catch (error) { console.error('Failed to load orders:', error); updateStatus(false, 'Connection error'); } } // Update status indicator function updateStatus(isConnected, message) { const dot = document.getElementById('statusDot'); const text = document.getElementById('statusText'); if (isConnected) { dot.classList.remove('error'); text.textContent = message || 'Connected'; } else { dot.classList.add('error'); text.textContent = message || 'Disconnected'; } } // Render orders to DOM function renderOrders() { const grid = document.getElementById('ordersGrid'); if (orders.length === 0) { grid.innerHTML = `

No Active Orders

New orders will appear here automatically

`; return; } grid.innerHTML = orders.map(order => renderOrder(order)).join(''); } // Render single order card function renderOrder(order) { const statusClass = getStatusClass(order.OrderStatusID); const elapsedTime = getElapsedTime(order.OrderSubmittedOn); const timeClass = getTimeClass(elapsedTime); // Group line items by root items and their modifiers const rootItems = order.LineItems.filter(item => item.OrderLineItemParentOrderLineItemID === 0); return `
#${order.OrderID}
${formatElapsedTime(elapsedTime)}
${formatSubmitTime(order.OrderSubmittedOn)}
Table: ${order.ServicePointName || 'N/A'}
Customer: ${order.UserFirstName || ''} ${order.UserLastName || ''}
Status: ${STATUS_NAMES[order.OrderStatusID] || 'Unknown'}
${order.OrderRemarks ? `
Note: ${escapeHtml(order.OrderRemarks)}
` : ''}
${rootItems.map(item => renderLineItem(item, order.LineItems)).join('')}
${renderActionButtons(order)}
`; } // Render line item with modifiers function renderLineItem(item, allItems) { const modifiers = allItems.filter(mod => mod.OrderLineItemParentOrderLineItemID === item.OrderLineItemID); console.log(`[renderLineItem] Item: ${item.ItemName}, ID: ${item.OrderLineItemID}, Modifiers found: ${modifiers.length}`); if (modifiers.length > 0) { console.log('[renderLineItem] Modifiers:', modifiers); } return `
${escapeHtml(item.ItemName)}
×${item.OrderLineItemQuantity}
${modifiers.length > 0 ? renderAllModifiers(modifiers, allItems) : ''} ${item.OrderLineItemRemark ? `
Note: ${escapeHtml(item.OrderLineItemRemark)}
` : ''}
`; } // Render all modifiers (flattened, no nested wrappers) function renderAllModifiers(modifiers, allItems) { let html = '
'; // Build path for each leaf modifier function getModifierPath(mod) { const path = []; let current = mod; // Walk up the tree to build the path while (current) { path.unshift(current.ItemName); const parentId = current.OrderLineItemParentOrderLineItemID; if (parentId === 0) break; current = allItems.find(item => item.OrderLineItemID === parentId); } return path; } // Collect all leaf modifiers with their paths const leafModifiers = []; function collectLeafModifiers(mods) { mods.forEach(mod => { const children = allItems.filter(item => item.OrderLineItemParentOrderLineItemID === mod.OrderLineItemID); if (children.length === 0) { // This is a leaf - no children const path = getModifierPath(mod); leafModifiers.push({ mod, path }); } else { // This has children, recurse into them collectLeafModifiers(children); } }); } collectLeafModifiers(modifiers); // Render leaf modifiers with breadcrumb paths leafModifiers.forEach(({ path }) => { const displayText = path.join(': '); html += `
+ ${escapeHtml(displayText)}
`; }); html += '
'; return html; } // Render modifier recursively with indentation function renderModifierRecursive(modifier, allItems, level) { const subModifiers = allItems.filter(mod => mod.OrderLineItemParentOrderLineItemID === modifier.OrderLineItemID); // For KDS MVP: no indentation, just flat list const indent = 0; // Changed from: level * 20 console.log(`[renderModifierRecursive] Level ${level}: ${modifier.ItemName} (ID: ${modifier.OrderLineItemID}), Sub-modifiers: ${subModifiers.length}`); let html = `
+ ${escapeHtml(modifier.ItemName)}
`; // Recursively render sub-modifiers if (subModifiers.length > 0) { subModifiers.forEach(sub => { html += renderModifierRecursive(sub, allItems, level + 1); }); } return html; } // Render action buttons based on order status function renderActionButtons(order) { switch (order.OrderStatusID) { case STATUS.NEW: return ``; case STATUS.PREPARING: return ``; case STATUS.READY: return ``; default: return ''; } } // Update order status async function updateOrderStatus(orderId, newStatusId) { try { const response = await fetch(`${config.apiBaseUrl}/orders/updateStatus.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ OrderID: orderId, StatusID: newStatusId }) }); const data = await response.json(); if (data.OK) { // Immediately reload orders to reflect the change await loadOrders(); } else { alert(`Failed to update order: ${data.MESSAGE || data.ERROR}`); } } catch (error) { console.error('Failed to update order status:', error); alert('Failed to update order status. Please try again.'); } } // Helper functions function getStatusClass(statusId) { switch (statusId) { case STATUS.NEW: return 'new'; case STATUS.PREPARING: return 'preparing'; case STATUS.READY: return 'ready'; default: return ''; } } function getElapsedTime(submittedOn) { if (!submittedOn) return 0; const submitted = new Date(submittedOn); const now = new Date(); return Math.floor((now - submitted) / 1000); // seconds } function getTimeClass(seconds) { if (seconds > 900) return 'critical'; // > 15 min if (seconds > 600) return 'warning'; // > 10 min return ''; } function formatElapsedTime(seconds) { const minutes = Math.floor(seconds / 60); const secs = seconds % 60; return `${minutes}:${secs.toString().padStart(2, '0')}`; } function formatSubmitTime(dateString) { if (!dateString) return ''; const date = new Date(dateString); return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }