payfrit-works/kds/kds.js
John Mizerek 4f694850e6 Fix KDS nested modifier display with breadcrumb paths
- Rewrote modifier rendering to display leaf modifiers only
- Added breadcrumb paths for full context (e.g., "Customize Spread: Extra")
- Eliminated nested div wrappers that were breaking the display
- Added comprehensive debug logging for troubleshooting
- Modifiers now show complete hierarchy without clutter

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 14:40:53 -08:00

392 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 = `
<div class="empty-state">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
<h2>No Active Orders</h2>
<p>New orders will appear here automatically</p>
</div>
`;
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 `
<div class="order-card ${statusClass}">
<div class="order-header">
<div class="order-number">#${order.OrderID}</div>
<div class="order-time">
<div class="elapsed-time ${timeClass}">${formatElapsedTime(elapsedTime)}</div>
<div class="submit-time">${formatSubmitTime(order.OrderSubmittedOn)}</div>
</div>
</div>
<div class="order-info">
<div><strong>Table:</strong> ${order.ServicePointName || 'N/A'}</div>
<div><strong>Customer:</strong> ${order.UserFirstName || ''} ${order.UserLastName || ''}</div>
<div><strong>Status:</strong> ${STATUS_NAMES[order.OrderStatusID] || 'Unknown'}</div>
</div>
${order.OrderRemarks ? `<div class="order-remarks">Note: ${escapeHtml(order.OrderRemarks)}</div>` : ''}
<div class="line-items">
${rootItems.map(item => renderLineItem(item, order.LineItems)).join('')}
</div>
<div class="action-buttons">
${renderActionButtons(order)}
</div>
</div>
`;
}
// 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 `
<div class="line-item">
<div class="line-item-main">
<div class="item-name">${escapeHtml(item.ItemName)}</div>
<div class="item-qty">×${item.OrderLineItemQuantity}</div>
</div>
${modifiers.length > 0 ? renderAllModifiers(modifiers, allItems) : ''}
${item.OrderLineItemRemark ? `
<div class="item-remark">Note: ${escapeHtml(item.OrderLineItemRemark)}</div>
` : ''}
</div>
`;
}
// Render all modifiers (flattened, no nested wrappers)
function renderAllModifiers(modifiers, allItems) {
let html = '<div class="modifiers">';
// 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 += `<div class="modifier">+ ${escapeHtml(displayText)}</div>`;
});
html += '</div>';
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 = `<div class="modifier" style="padding-left: ${indent}px;">+ ${escapeHtml(modifier.ItemName)}</div>`;
// 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 `<button class="btn btn-start" onclick="updateOrderStatus(${order.OrderID}, ${STATUS.PREPARING})">Start Preparing</button>`;
case STATUS.PREPARING:
return `<button class="btn btn-ready" onclick="updateOrderStatus(${order.OrderID}, ${STATUS.READY})">Mark as Ready</button>`;
case STATUS.READY:
return `<button class="btn btn-complete" onclick="updateOrderStatus(${order.OrderID}, ${STATUS.COMPLETED})">Complete</button>`;
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;
}