This repository has been archived on 2026-03-21. You can view files and clone it, but cannot push or open issues or pull requests.
payfrit-biz/kds/kds.js
John Mizerek 1210249f54 Normalize database column and table names across entire codebase
Update all SQL queries, query result references, and ColdFusion code to match
the renamed database schema. Tables use plural CamelCase, PKs are all `ID`,
column prefixes stripped (e.g. BusinessName→Name, UserFirstName→FirstName).

Key changes:
- Strip table-name prefixes from all column references (Businesses, Users,
  Addresses, Hours, Menus, Categories, Items, Stations, Orders,
  OrderLineItems, Tasks, TaskCategories, TaskRatings, QuickTaskTemplates,
  ScheduledTaskDefinitions, ChatMessages, Beacons, ServicePoints, Employees,
  VisitorTrackings, ApiPerfLogs, tt_States, tt_Days, tt_AddressTypes,
  tt_OrderTypes, tt_TaskTypes)
- Rename PK references from {TableName}ID to ID in all queries
- Rewrite 7 admin beacon files to use ServicePoints.BeaconID instead of
  dropped lt_Beacon_Businesses_ServicePoints link table
- Rewrite beacon assignment files (list, save, delete) for new schema
- Fix FK references incorrectly changed to ID (OrderLineItems.OrderID,
  Categories.MenuID, Tasks.CategoryID, ServicePoints.BeaconID)
- Update Addresses: AddressLat→Latitude, AddressLng→Longitude
- Update Users: UserPassword→Password, UserIsEmailVerified→IsEmailVerified,
  UserIsActive→IsActive, UserBalance→Balance, etc.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:39:12 -08:00

521 lines
16 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() {
// Get business ID from portal's shared localStorage (same key portal uses)
const portalBusinessId = localStorage.getItem('payfrit_portal_business');
if (portalBusinessId && !isNaN(parseInt(portalBusinessId))) {
config.businessId = parseInt(portalBusinessId);
}
// Load KDS-specific settings
const saved = localStorage.getItem('kds_config');
if (saved) {
try {
const parsed = JSON.parse(saved);
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;
} catch (e) {
console.error('Failed to load config:', e);
}
}
document.getElementById('businessIdInput').value = config.businessId || '';
document.getElementById('servicePointIdInput').value = config.servicePointId || '';
document.getElementById('refreshIntervalInput').value = config.refreshInterval / 1000;
// If no business ID, show config panel
if (!config.businessId) {
toggleConfig();
}
}
function saveConfigToStorage() {
// Save KDS-specific settings only (businessId comes from portal)
localStorage.setItem('kds_config', JSON.stringify({
servicePointId: config.servicePointId,
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, cannot start');
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) {
document.getElementById('businessName').textContent = data.BUSINESS.Name || 'Kitchen Display System';
}
} catch (e) {
console.error('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 = `
<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.Color || '#666';
html += `
<button class="station-btn" style="background: ${color}; color: #fff;" onclick="selectStation(${s.StationID}, '${escapeHtml(s.Name)}', '${color}')">
${escapeHtml(s.Name)}
</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');
console.log('renderOrders called, orders count:', orders.length);
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;
}
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('');
}
// Render single order card
function renderOrder(order) {
const statusClass = getStatusClass(order.StatusID);
const elapsedTime = getElapsedTime(order.SubmittedOn);
const timeClass = getTimeClass(elapsedTime);
const rootItems = order.LineItems.filter(item => item.ParentOrderLineItemID === 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.SubmittedOn)}</div>
</div>
</div>
<div class="order-info">
<div><strong>${getLocationLabel(order)}:</strong> ${getLocationValue(order)}</div>
<div><strong>Customer:</strong> ${order.FirstName || ''} ${order.LastName || ''}</div>
<div><strong>Status:</strong> ${STATUS_NAMES[order.StatusID] || 'Unknown'}</div>
</div>
${order.Remarks ? `<div class="order-remarks">Note: ${escapeHtml(order.Remarks)}</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.ParentOrderLineItemID === item.OrderLineItemID);
console.log(`Item: ${item.Name} (ID: ${item.OrderLineItemID}) has ${modifiers.length} direct modifiers:`, modifiers.map(m => m.Name));
return `
<div class="line-item">
<div class="line-item-main">
<div class="item-name">${escapeHtml(item.Name)}</div>
<div class="item-qty">x${item.Quantity}</div>
</div>
${modifiers.length > 0 ? renderAllModifiers(modifiers, allItems) : ''}
${item.Remark ? `<div class="item-remark">Note: ${escapeHtml(item.Remark)}</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.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 += `<div class="modifier">+ ${escapeHtml(displayText)}</div>`;
});
html += '</div>';
return html;
}
// Render action buttons based on order status
function renderActionButtons(order) {
switch (order.StatusID) {
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
// 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;
}