// Configuration let config = { apiBaseUrl: '/api', businessId: parseInt(localStorage.getItem('payfrit_portal_business')) || 0, stationId: null, stationName: null, stationColor: null, refreshInterval: 5000, }; // State let orders = []; let stations = []; let refreshTimer = null; let expandedOrders = new Set(); // 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(); checkStationSelection(); updateClock(); setInterval(updateClock, 1000); // Monitor online/offline status window.addEventListener('online', () => { console.log('[KDS] Back online'); updateStatus(true); loadOrders(); // Refresh immediately when back online }); window.addEventListener('offline', () => { console.log('[KDS] Went offline'); updateStatus(false); }); // Initial connection check if (!navigator.onLine) { updateStatus(false); } }); // Update clock display function updateClock() { const now = new Date(); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); document.getElementById('clock').textContent = `${hours}:${minutes}:${seconds}`; } // Load config from localStorage function loadConfig() { // Load KDS-specific settings (station, refresh interval) const saved = localStorage.getItem('kds_config'); if (saved) { try { const parsed = JSON.parse(saved); config.stationId = parsed.stationId !== undefined ? parsed.stationId : null; config.stationName = parsed.stationName || null; config.stationColor = parsed.stationColor || null; config.refreshInterval = (parsed.refreshInterval || 5) * 1000; } catch (e) { console.error('[KDS] Failed to load config:', e); } } console.log('[KDS] Config loaded:', config); } function saveConfigToStorage() { localStorage.setItem('kds_config', JSON.stringify({ stationId: config.stationId, stationName: config.stationName, stationColor: config.stationColor, refreshInterval: config.refreshInterval / 1000 })); } // Check if station selection is needed async function checkStationSelection() { if (!config.businessId) { console.log('[KDS] No businessId - please log in via Portal first'); updateStatus(false, 'No business selected - log in via Portal'); return; } console.log('[KDS] Starting with businessId:', config.businessId, 'stationId:', config.stationId); // Load business name await loadName(); // If station already selected (including 0 for "all"), start KDS if (config.stationId !== null && config.stationId !== undefined) { updateStationBadge(); startAutoRefresh(); return; } // Load stations to see if we need to show picker await loadStations(); if (stations.length > 0) { showStationSelection(); } else { // No stations configured, default to all orders config.stationId = 0; updateStationBadge(); startAutoRefresh(); } } // Load business name from API async function loadName() { if (!config.businessId) return; try { const response = await fetch(`${config.apiBaseUrl}/businesses/get.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: config.businessId }) }); const data = await response.json(); if (data.OK && data.BUSINESS && data.BUSINESS.Name) { document.getElementById('businessName').textContent = ' - ' + data.BUSINESS.Name; } } catch (e) { console.error('[KDS] Failed to load business name:', e); } } // Load stations from API async function loadStations() { if (!config.businessId) return; try { const response = await fetch(`${config.apiBaseUrl}/stations/list.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: config.businessId }) }); const data = await response.json(); if (data.OK) { stations = data.STATIONS || []; } } catch (e) { console.error('Failed to load stations:', e); } } // Show station selection overlay async function showStationSelection() { if (stations.length === 0) { await loadStations(); } const overlay = document.getElementById('stationOverlay'); const buttons = document.getElementById('stationButtons'); let html = ` `; stations.forEach(s => { const color = s.Color || '#666'; html += ` `; }); buttons.innerHTML = html; overlay.classList.remove('hidden'); } // Select a station function selectStation(stationId, stationName, stationColor) { config.stationId = stationId; config.stationName = stationName; config.stationColor = stationColor; // Save to localStorage saveConfigToStorage(); // Hide overlay and start document.getElementById('stationOverlay').classList.add('hidden'); updateStationBadge(); startAutoRefresh(); } // Update station badge in header function updateStationBadge() { const badge = document.getElementById('stationBadge'); const name = config.stationId && config.stationName ? config.stationName : 'All'; badge.innerHTML = `/ ${escapeHtml(name)}`; } // 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; // Check if online before attempting fetch if (!navigator.onLine) { updateStatus(false); return; } try { const url = `${config.apiBaseUrl}/orders/listForKDS.cfm`; const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ BusinessID: config.businessId, StationID: config.stationId || 0 }) }); const data = await response.json(); if (data.OK) { orders = data.ORDERS || []; renderOrders(); updateStatus(true, `${orders.length} active orders`); } else { 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 indicator = document.getElementById('statusIndicator'); if (isConnected) { indicator.classList.remove('disconnected'); } else { indicator.classList.add('disconnected'); } } // Render orders to DOM function renderOrders() { const grid = document.getElementById('ordersGrid'); console.log('renderOrders called, orders count:', orders.length); if (orders.length === 0) { grid.innerHTML = `

No Active Orders

New orders will appear here automatically

`; return; } orders.forEach((order, i) => { console.log(`Order ${i}: ID=${order.OrderID}, LineItems count=${order.LineItems?.length || 0}`); if (order.LineItems) { order.LineItems.forEach(li => { console.log(` LineItem: ${li.Name} (ID=${li.OrderLineItemID}, ParentID=${li.ParentOrderLineItemID})`); }); } }); grid.innerHTML = orders.map(order => renderOrder(order)).join(''); } // Check if a root item belongs to the current station function isStationItem(item) { if (!config.stationId || config.stationId === 0) return true; const sid = parseInt(item.StationID) || 0; return sid === config.stationId || sid === 0; } // Toggle expand/collapse for an order function toggleExpand(orderId) { if (expandedOrders.has(orderId)) { expandedOrders.delete(orderId); } else { expandedOrders.add(orderId); } renderOrders(); } // Render single order card function renderOrder(order) { const statusClass = getStatusClass(order.StatusID); const elapsedTime = getElapsedTime(order.SubmittedOn); const timeClass = getTimeClass(elapsedTime); const allRootItems = order.LineItems.filter(item => item.ParentOrderLineItemID === 0); const isFiltering = config.stationId && config.stationId > 0; const isExpanded = expandedOrders.has(order.OrderID); // Split into station items and other-station items const stationItems = isFiltering ? allRootItems.filter(i => isStationItem(i)) : allRootItems; const otherItems = isFiltering ? allRootItems.filter(i => !isStationItem(i)) : []; const hasOtherItems = otherItems.length > 0; // Build expand toggle button let expandToggle = ''; if (isFiltering && hasOtherItems) { if (isExpanded) { expandToggle = ``; } else { expandToggle = ``; } } // Render line items: station items always, other items only when expanded let lineItemsHtml = stationItems.map(item => renderLineItem(item, order.LineItems, false)).join(''); if (isExpanded && hasOtherItems) { lineItemsHtml += otherItems.map(item => renderLineItem(item, order.LineItems, true)).join(''); } return `
#${order.OrderID}${order.OrderTypeID === 2 ? ' PICKUP' : ''}${order.OrderTypeID === 3 ? ' DELIVERY' : ''}
${formatElapsedTime(elapsedTime)}
${formatSubmitTime(order.SubmittedOn)}
${getLocationLabel(order)}: ${getLocationValue(order)}
Customer: ${order.FirstName || ''} ${order.LastName || ''}
Status: ${STATUS_NAMES[order.StatusID] || 'Unknown'}
${order.Remarks ? `
Note: ${escapeHtml(order.Remarks)}
` : ''}
${lineItemsHtml}
${expandToggle}
${renderActionButtons(order)}
`; } // Render line item with modifiers function renderLineItem(item, allItems, isOtherStation = false) { const modifiers = allItems.filter(mod => mod.ParentOrderLineItemID === item.OrderLineItemID); console.log(`Item: ${item.Name} (ID: ${item.OrderLineItemID}) has ${modifiers.length} direct modifiers:`, modifiers.map(m => m.Name)); const otherClass = isOtherStation ? ' other-station' : ''; const doneClass = (item.StatusID === 1) ? ' line-item-done' : ''; return `
${escapeHtml(item.Name)}
x${item.Quantity}
${modifiers.length > 0 ? renderAllModifiers(modifiers, allItems) : ''} ${item.Remark ? `
Note: ${escapeHtml(item.Remark)}
` : ''}
`; } // Render all modifiers function renderAllModifiers(modifiers, allItems) { let html = '
'; function getModifierPath(mod) { const path = []; let current = mod; while (current) { path.unshift(current.Name); const parentId = current.ParentOrderLineItemID; if (parentId === 0) break; current = allItems.find(item => item.OrderLineItemID === parentId); } return path; } const leafModifiers = []; function collectLeafModifiers(mods, depth = 0) { console.log(` collectLeafModifiers depth=${depth}, processing ${mods.length} mods:`, mods.map(m => m.Name)); mods.forEach(mod => { // Skip default modifiers - only show customizations if (mod.IsCheckedByDefault) { console.log(` Skipping default modifier: ${mod.Name}`); return; } const children = allItems.filter(item => item.ParentOrderLineItemID === mod.OrderLineItemID); console.log(` Mod: ${mod.Name} (ID: ${mod.OrderLineItemID}) has ${children.length} children`); if (children.length === 0) { // This is a leaf node (actual selection) const path = getModifierPath(mod); console.log(` -> LEAF, path: ${path.join(' > ')}`); leafModifiers.push({ mod, path }); } else { // Has children, recurse deeper collectLeafModifiers(children, depth + 1); } }); } collectLeafModifiers(modifiers); console.log(` Total leaf modifiers found: ${leafModifiers.length}`); leafModifiers.forEach(({ mod }) => { // Use ItemParentName (the category/template name) if available, otherwise just show the item name // This gives us "Drink Choice: Coke" instead of "Double Double Combo: Coke" const displayText = mod.ItemParentName ? `${mod.ItemParentName}: ${mod.Name}` : mod.Name; html += `
+ ${escapeHtml(displayText)}
`; }); html += '
'; return html; } // Check if this station's items are all done for an order function isStationDoneForOrder(order) { if (!config.stationId || config.stationId === 0) return false; const stationRootItems = order.LineItems.filter(li => li.ParentOrderLineItemID === 0 && (parseInt(li.StationID) || 0) === config.stationId ); if (stationRootItems.length === 0) return true; // no items for this station return stationRootItems.every(li => li.StatusID === 1); } // Render action buttons based on order status function renderActionButtons(order) { const isFiltering = config.stationId && config.stationId > 0; if (isFiltering) { // Station worker view: per-station "Done" button if (order.StatusID === STATUS.NEW || order.StatusID === STATUS.PREPARING) { if (isStationDoneForOrder(order)) { return ``; } else { return ``; } } if (order.StatusID === STATUS.READY) { return ``; } return ''; } // Manager view (no station selected): whole-order buttons switch (order.StatusID) { 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) { 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.'); } } // Mark station's items as done for an order async function markStationDone(orderId) { try { const response = await fetch(`${config.apiBaseUrl}/orders/markStationDone.cfm`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ OrderID: orderId, StationID: config.stationId }) }); const data = await response.json(); if (data.OK) { await loadOrders(); } else { alert(`Failed to mark station done: ${data.MESSAGE || data.ERROR}`); } } catch (error) { console.error('Failed to mark station done:', error); alert('Failed to mark station done. Please try again.'); } } // Helper functions // Get location label based on order type (Table vs Type) function getLocationLabel(order) { // OrderTypeID: 1=Dine-In, 2=Takeaway, 3=Delivery if (order.OrderTypeID === 2 || order.OrderTypeID === 3) { return 'Type'; } return 'Table'; } // Get location value based on order type function getLocationValue(order) { // OrderTypeID: 1=Dine-In, 2=Takeaway, 3=Delivery if (order.OrderTypeID === 2) { return 'Takeaway'; } if (order.OrderTypeID === 3) { return 'Delivery'; } // Dine-in: show service point name return order.Name || 'N/A'; } 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; return Math.floor((new Date() - new Date(submittedOn)) / 1000); } function getTimeClass(seconds) { if (seconds > 900) return 'critical'; if (seconds > 600) return 'warning'; 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 ''; return new Date(dateString).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } function escapeHtml(text) { if (!text) return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; }