From d7f68e8098ea952af1347b56f83363a863ff74b7 Mon Sep 17 00:00:00 2001 From: John Mizerek Date: Mon, 29 Dec 2025 17:46:11 -0800 Subject: [PATCH] Add KDS web interface files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.html: Complete KDS interface with dark theme - Responsive grid layout for order cards - Status indicator with connection monitoring - Configuration panel for settings - Empty state display - kds.js: Real-time order management JavaScript - Auto-refresh with configurable interval - Order status updates via API - Elapsed time calculation with visual warnings - Hierarchical line item rendering with recursive modifiers - LocalStorage-based configuration persistence 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- kds/index.html | 351 +++++++++++++++++++++++++++++++++++++++++++++++++ kds/kds.js | 326 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 677 insertions(+) create mode 100644 kds/index.html create mode 100644 kds/kds.js diff --git a/kds/index.html b/kds/index.html new file mode 100644 index 0000000..4a1ca61 --- /dev/null +++ b/kds/index.html @@ -0,0 +1,351 @@ + + + + + + Payfrit KDS - Kitchen Display System + + + +
+
+

Kitchen Display System

+
Loading...
+
+
+
+ Connected + +
+
+ +
+ + + + +
+ +
+
+ + + +

No Active Orders

+

New orders will appear here automatically

+
+
+ + + + diff --git a/kds/kds.js b/kds/kds.js new file mode 100644 index 0000000..7a046d4 --- /dev/null +++ b/kds/kds.js @@ -0,0 +1,326 @@ +// 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; + + try { + const response = await fetch(`${config.apiBaseUrl}/orders/listForKDS.cfm`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + BusinessID: config.businessId, + ServicePointID: config.servicePointId || 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 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); + + return ` +
+
+
${escapeHtml(item.ItemName)}
+
×${item.OrderLineItemQuantity}
+
+ + ${modifiers.length > 0 ? ` +
+ ${modifiers.map(mod => renderModifier(mod, allItems)).join('')} +
+ ` : ''} + + ${item.OrderLineItemRemark ? ` +
Note: ${escapeHtml(item.OrderLineItemRemark)}
+ ` : ''} +
+ `; +} + +// Render modifier (recursive for nested modifiers) +function renderModifier(modifier, allItems) { + const subModifiers = allItems.filter(mod => mod.OrderLineItemParentOrderLineItemID === modifier.OrderLineItemID); + + return ` +
+ + ${escapeHtml(modifier.ItemName)} + ${subModifiers.length > 0 ? ` +
+ ${subModifiers.map(sub => renderModifier(sub, allItems)).join('')} +
+ ` : ''} +
+ `; +} + +// 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; +}