payfrit-works/kds/kds.js
John Mizerek e20aede25f Fix KDS to show all selected modifiers including defaults
Previously the KDS was skipping modifiers marked as "checked by default",
but for exclusive selection groups (like drink choices) the customer's
actual selection should always be visible to the kitchen.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 18:09:54 -08:00

428 lines
13 KiB
JavaScript

// Configuration
let config = {
apiBaseUrl: 'https://biz.payfrit.com/api',
businessId: null,
servicePointId: null,
stationId: null,
stationName: null,
stationColor: null,
refreshInterval: 5000,
};
// State
let orders = [];
let stations = [];
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();
checkStationSelection();
});
// 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.stationId = parsed.stationId || null;
config.stationName = parsed.stationName || null;
config.stationColor = parsed.stationColor || 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();
}
}
// Check if station selection is needed
async function checkStationSelection() {
if (!config.businessId) return;
// If station already selected, start KDS
if (config.stationId !== null) {
updateStationBadge();
startAutoRefresh();
return;
}
// Load stations and show selection
await loadStations();
if (stations.length > 0) {
showStationSelection();
} else {
// No stations configured, just start with all orders
startAutoRefresh();
}
}
// 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 = `
<button class="station-btn all-stations" onclick="selectStation(0, 'All Stations', null)">
<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="M4 6h16M4 10h16M4 14h16M4 18h16" />
</svg>
All Stations
</button>
`;
stations.forEach(s => {
const color = s.StationColor || '#666';
html += `
<button class="station-btn" style="background: ${color}; color: #fff;" onclick="selectStation(${s.StationID}, '${escapeHtml(s.StationName)}', '${color}')">
${escapeHtml(s.StationName)}
</button>
`;
});
buttons.innerHTML = html;
overlay.classList.remove('hidden');
}
// Select a station
function selectStation(stationId, stationName, stationColor) {
config.stationId = stationId || null;
config.stationName = stationName;
config.stationColor = stationColor;
// Save to localStorage
localStorage.setItem('kds_config', JSON.stringify({
businessId: config.businessId,
servicePointId: config.servicePointId,
stationId: config.stationId,
stationName: config.stationName,
stationColor: config.stationColor,
refreshInterval: config.refreshInterval / 1000
}));
// Hide overlay and start
document.getElementById('stationOverlay').classList.add('hidden');
updateStationBadge();
startAutoRefresh();
}
// Update station badge in header
function updateStationBadge() {
const badge = document.getElementById('stationBadge');
if (config.stationId && config.stationName) {
const color = config.stationColor || '#3b82f6';
badge.innerHTML = `<span class="station-name-display" style="background: ${color}; color: #fff;">${escapeHtml(config.stationName)}</span>`;
} else {
badge.innerHTML = `<span class="station-name-display" style="background: #3b82f6; color: #fff;">All Stations</span>`;
}
}
// 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;
config.stationId = null; // Reset station selection when changing business
config.stationName = null;
config.stationColor = null;
localStorage.setItem('kds_config', JSON.stringify({
businessId: config.businessId,
servicePointId: config.servicePointId,
stationId: null,
stationName: null,
stationColor: null,
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 url = `${config.apiBaseUrl}/orders/listForKDS.cfm`;
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
BusinessID: config.businessId,
ServicePointID: config.servicePointId || 0,
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 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);
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);
return `
<div class="line-item">
<div class="line-item-main">
<div class="item-name">${escapeHtml(item.ItemName)}</div>
<div class="item-qty">x${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
function renderAllModifiers(modifiers, allItems) {
let html = '<div class="modifiers">';
function getModifierPath(mod) {
const path = [];
let current = mod;
while (current) {
path.unshift(current.ItemName);
const parentId = current.OrderLineItemParentOrderLineItemID;
if (parentId === 0) break;
current = allItems.find(item => item.OrderLineItemID === parentId);
}
return path;
}
const leafModifiers = [];
function collectLeafModifiers(mods) {
mods.forEach(mod => {
// Show ALL selected modifiers - don't skip defaults
// The customer made a choice and it should be visible on the KDS
const children = allItems.filter(item => item.OrderLineItemParentOrderLineItemID === mod.OrderLineItemID);
if (children.length === 0) {
// This is a leaf node (actual selection)
leafModifiers.push({ mod, path: getModifierPath(mod) });
} else {
// Has children, recurse deeper
collectLeafModifiers(children);
}
});
}
collectLeafModifiers(modifiers);
leafModifiers.forEach(({ path }) => {
html += `<div class="modifier">+ ${escapeHtml(path.join(': '))}</div>`;
});
html += '</div>';
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) {
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;
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;
}