// Dashboard functionality
let currentFilter = 'all';
let currentOrderIdForCancel = null;
let pendingStatusChange = {
orderId: null,
newStatus: null,
currentStatus: null,
orderData: null
};
// Track orders for notification detection
let previousOrderIds = new Set();
let previousCanceledOrderIds = new Set();
let isFirstLoad = true;
// Connection status tracking
const connectionStatus = {
local: {
status: 'checking',
lastCheck: null,
consecutiveFailures: 0
},
api: {
status: 'checking',
lastCheck: null,
consecutiveFailures: 0,
responseTime: null
}
};
// Audio notification system
const audioNotification = {
newOrderSound: null,
canceledOrderSound: null,
enabled: true,
init: function() {
// Load notification settings from server
fetch('/api/notification-settings')
.then(response => response.json())
.then(data => {
if (!data.error) {
this.enabled = data.soundNotificationsEnabled !== 'false';
const newOrderPath = data.newOrderSoundPath || '/public/sounds/new-order-notification.mp3';
const canceledOrderPath = data.canceledOrderSoundPath || '/public/sounds/new-order-notification.mp3';
// Preload audio files
this.newOrderSound = new Audio(newOrderPath);
this.canceledOrderSound = new Audio(canceledOrderPath);
// Set volume if specified
if (data.soundVolume) {
const volume = parseInt(data.soundVolume, 10) / 100;
this.newOrderSound.volume = volume;
this.canceledOrderSound.volume = volume;
}
}
})
.catch(error => {
console.warn('Failed to load notification settings:', error);
// Use default sound
this.newOrderSound = new Audio('/public/sounds/new-order-notification.mp3');
this.canceledOrderSound = new Audio('/public/sounds/new-order-notification.mp3');
});
},
playNewOrderSound: function() {
if (this.enabled && this.newOrderSound) {
this.newOrderSound.currentTime = 0;
this.newOrderSound.play().catch(error => {
console.warn('Failed to play new order sound:', error);
});
}
},
playCanceledOrderSound: function() {
if (this.enabled && this.canceledOrderSound) {
this.canceledOrderSound.currentTime = 0;
this.canceledOrderSound.play().catch(error => {
console.warn('Failed to play canceled order sound:', error);
});
}
}
};
// Initialize dashboard
document.addEventListener('DOMContentLoaded', function() {
setupFilterButtons();
audioNotification.init();
refreshOrders();
// Set up auto-refresh
setInterval(refreshOrders, config.dashboardRefreshInterval || 10000);
// Initialize connection monitoring
checkConnectionStatus();
// Check connection status every 15 seconds
setInterval(checkConnectionStatus, 15000);
});
function setupFilterButtons() {
const filterButtons = document.querySelectorAll('.filter-btn');
filterButtons.forEach(button => {
button.addEventListener('click', function() {
// Remove active class from all buttons
filterButtons.forEach(btn => btn.classList.remove('active'));
// Add active class to clicked button
this.classList.add('active');
// Update filter and refresh
currentFilter = this.dataset.filter;
refreshOrders();
});
});
}
function refreshOrders() {
const syncButton = document.getElementById('syncButton');
const syncText = syncButton ? syncButton.querySelector('.sync-text') : null;
if (syncButton) {
syncButton.classList.add('loading');
syncButton.disabled = true;
if (syncText) {
syncText.textContent = 'Syncing...';
}
}
const statusParam = currentFilter === 'all' ? '' : currentFilter;
const url = '/api/orders' + (statusParam ? '?status=' + statusParam : '');
fetch(url)
.then(response => response.json())
.then(data => {
if (!data.error) {
updateOrderCards(data.orders);
updateStats(data.stats);
// Connection successful - reset local connection failure counter
if (connectionStatus.local.consecutiveFailures > 0) {
connectionStatus.local.consecutiveFailures = 0;
connectionStatus.local.status = 'online';
updateConnectionUI('local', 'online', 'Connected');
}
}
})
.catch(error => {
console.error('Failed to refresh orders:', error);
// Mark local connection as potentially offline
connectionStatus.local.consecutiveFailures++;
if (connectionStatus.local.consecutiveFailures >= 2) {
connectionStatus.local.status = 'offline';
updateConnectionUI('local', 'offline', 'Disconnected');
}
})
.finally(() => {
if (syncButton) {
syncButton.classList.remove('loading');
syncButton.disabled = false;
if (syncText) {
syncText.textContent = 'Sync Now';
}
}
});
}
function updateStats(stats) {
if (!stats) return;
const totalEl = document.getElementById('stat-total');
const newEl = document.getElementById('stat-new');
const preparingEl = document.getElementById('stat-preparing');
const readyEl = document.getElementById('stat-ready');
if (totalEl) totalEl.textContent = stats.total || 0;
if (newEl) newEl.textContent = stats.new || 0;
if (preparingEl) preparingEl.textContent = stats.preparing || 0;
if (readyEl) readyEl.textContent = stats.ready || 0;
}
function updateOrderCards(orders) {
const container = document.getElementById('ordersContainer');
if (!orders || orders.length === 0) {
container.innerHTML = '
No orders to display
';
return;
}
// Detect new orders and canceled orders for sound notifications
if (!isFirstLoad) {
const currentOrderIds = new Set();
const currentCanceledOrderIds = new Set();
orders.forEach(order => {
const orderId = order.id;
const status = order.localStatus || order.status || 'new';
currentOrderIds.add(orderId);
if (status === 'canceled') {
currentCanceledOrderIds.add(orderId);
}
// Check for new orders (not in previous set)
if (!previousOrderIds.has(orderId) && status === 'new') {
console.log('New order detected:', orderId);
audioNotification.playNewOrderSound();
}
// Check for newly canceled orders
if (!previousCanceledOrderIds.has(orderId) && status === 'canceled') {
console.log('Order canceled:', orderId);
audioNotification.playCanceledOrderSound();
}
});
// Update tracking sets
previousOrderIds = currentOrderIds;
previousCanceledOrderIds = currentCanceledOrderIds;
} else {
// On first load, just populate the tracking sets without playing sounds
orders.forEach(order => {
previousOrderIds.add(order.id);
const status = order.localStatus || order.status || 'new';
if (status === 'canceled') {
previousCanceledOrderIds.add(order.id);
}
});
isFirstLoad = false;
}
container.innerHTML = orders.map(order => createOrderCard(order)).join('');
}
function createOrderCard(order) {
const status = order.localStatus || order.status || 'new';
const statusBadge = getStatusBadge(status);
const orderType = order.order.type || 'pickup';
const typeIcon = orderType === 'delivery' ? '🚚' : '🛍️';
// Determine which actions to show based on status
let actions = '';
if (status === 'new') {
actions = `
`;
} else if (status === 'preparing') {
actions = `
`;
} else if (status === 'ready') {
actions = `
`;
}
// Add cancel and reprint buttons for non-completed orders
if (status !== 'canceled' && status !== 'completed') {
actions += `
`;
}
actions += `
`;
// Build detailed items list with addons and excludes
let itemsDetailedHtml = '';
if (order.order.items && order.order.items.length > 0) {
// Sort items alphabetically by name for better kitchen organization
const sortedItems = [...order.order.items].sort((a, b) => {
const nameA = (a.itemName || a.name || 'Item').toLowerCase();
const nameB = (b.itemName || b.name || 'Item').toLowerCase();
return nameA.localeCompare(nameB);
});
itemsDetailedHtml = sortedItems.map(item => {
let modifiersHtml = '';
// Add addons
if (item.addons && item.addons.length > 0) {
const addonsList = item.addons.map(a => a.name || a).join(', ');
modifiersHtml += `+ Add: ${addonsList}
`;
}
// Add excludes
if (item.exclude && item.exclude.length > 0) {
const excludeList = item.exclude.map(e => e.name || e).join(', ');
modifiersHtml += `− NO: ${excludeList}
`;
}
return `
${item.qty}x
${item.itemName || item.name || 'Item'}
${modifiersHtml}
`;
}).join('');
}
// Food allergy warning (prominent)
let allergyWarning = '';
if (order.order.foodAllergy) {
const allergyNotes = order.order.foodAllergyNotes || 'Customer has food allergies';
allergyWarning = `
⚠️
FOOD ALLERGY ALERT
${allergyNotes}
`;
}
// Special instructions
let specialInstructionsHtml = '';
if (order.order.specialInstructions) {
specialInstructionsHtml = `
📝 Special Instructions:
${order.order.specialInstructions}
`;
}
return `
${allergyWarning}
Time:
${formatTime(order.createdAt)}
Customer:
${order.customer.name || 'N/A'}
Type:
${typeIcon}${orderType}
${order.order.deliveryAddress ? `
Address:${order.order.deliveryAddress}
` : ''}
Order Items:
${itemsDetailedHtml}
${specialInstructionsHtml}
Total:
${formatCurrency(order.totalAmount)}
${actions}
`;
}
function getStatusBadge(status) {
const badges = {
'new': 'New',
'preparing': 'Preparing',
'ready': 'Ready',
'completed': 'Completed',
'canceled': 'Canceled'
};
return badges[status] || status;
}
function changeStatus(orderId, newStatus) {
// Fetch the order data first to show in the confirmation modal
fetch(`/api/orders/${orderId}`)
.then(response => response.json())
.then(data => {
if (!data.error && data.order) {
openStatusChangeModal(orderId, newStatus, data.order);
} else {
showToast('Failed to fetch order details', 'error');
}
})
.catch(error => {
console.error('Error fetching order:', error);
showToast('Failed to fetch order details', 'error');
});
}
function openStatusChangeModal(orderId, newStatus, orderData) {
const currentStatus = orderData.localStatus || orderData.status || 'new';
// Store pending status change
pendingStatusChange = {
orderId: orderId,
newStatus: newStatus,
currentStatus: currentStatus,
orderData: orderData
};
// Get status information
const statusInfo = getStatusInfo(newStatus);
// Update modal icon
const iconEl = document.getElementById('statusModalIcon');
iconEl.textContent = statusInfo.icon;
iconEl.className = 'status-modal-icon ' + newStatus;
// Update modal title
document.getElementById('statusModalTitle').textContent = statusInfo.title;
// Update order details
document.getElementById('statusModalOrderId').textContent = orderId;
document.getElementById('statusModalCustomer').textContent = orderData.customer.name || 'N/A';
// Update status badges
const fromBadge = document.getElementById('statusModalFrom');
const toBadge = document.getElementById('statusModalTo');
fromBadge.textContent = getStatusBadge(currentStatus);
fromBadge.className = 'status-badge badge-' + currentStatus;
toBadge.textContent = getStatusBadge(newStatus);
toBadge.className = 'status-badge badge-' + newStatus;
// Update message
document.getElementById('statusModalMessage').textContent = statusInfo.message;
// Update confirm button
const confirmBtn = document.getElementById('statusConfirmBtn');
confirmBtn.textContent = statusInfo.buttonText;
// Show modal
document.getElementById('statusChangeModal').classList.add('visible');
}
function getStatusInfo(status) {
const statusMap = {
'preparing': {
icon: '👨🍳',
title: 'Start Preparing Order?',
message: 'This will mark the order as being prepared in the kitchen.',
buttonText: 'Start Preparing'
},
'ready': {
icon: '✅',
title: 'Mark Order as Ready?',
message: 'This will notify that the order is ready for pickup or delivery.',
buttonText: 'Mark Ready'
},
'completed': {
icon: '🎉',
title: 'Complete Order?',
message: 'This will mark the order as completed and it will be archived.',
buttonText: 'Complete Order'
}
};
return statusMap[status] || {
icon: '📋',
title: 'Change Order Status?',
message: 'This will update the order status.',
buttonText: 'Confirm'
};
}
function closeStatusChangeModal() {
document.getElementById('statusChangeModal').classList.remove('visible');
pendingStatusChange = {
orderId: null,
newStatus: null,
currentStatus: null,
orderData: null
};
}
function confirmStatusChange() {
if (!pendingStatusChange.orderId || !pendingStatusChange.newStatus) {
return;
}
const orderId = pendingStatusChange.orderId;
const newStatus = pendingStatusChange.newStatus;
// Close modal immediately for better UX
closeStatusChangeModal();
// Make the API call
fetch(`/api/orders/${orderId}/status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
})
.then(response => response.json())
.then(data => {
if (!data.error) {
showToast(`Order #${orderId} updated to ${newStatus}`, 'success');
refreshOrders();
} else {
showToast(data.message || 'Failed to update order', 'error');
}
})
.catch(error => {
console.error('Error updating order:', error);
showToast('Failed to update order', 'error');
});
}
function openCancelModal(orderId) {
currentOrderIdForCancel = orderId;
document.getElementById('cancelReason').value = '';
document.getElementById('cancelModal').classList.add('visible');
}
function closeCancelModal() {
currentOrderIdForCancel = null;
document.getElementById('cancelModal').classList.remove('visible');
}
function confirmCancel() {
if (!currentOrderIdForCancel) return;
const reason = document.getElementById('cancelReason').value;
fetch(`/api/orders/${currentOrderIdForCancel}/cancel`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: reason || 'Canceled by kitchen' })
})
.then(response => response.json())
.then(data => {
if (!data.error) {
showToast(`Order #${currentOrderIdForCancel} canceled`, 'success');
closeCancelModal();
refreshOrders();
} else {
showToast(data.message || 'Failed to cancel order', 'error');
}
})
.catch(error => {
console.error('Error canceling order:', error);
showToast('Failed to cancel order', 'error');
});
}
let reprintInProgressByOrder = {};
function reprintOrder(orderId) {
if (reprintInProgressByOrder[orderId]) {
return; // guard against rapid double clicks
}
reprintInProgressByOrder[orderId] = true;
fetch(`/api/orders/${orderId}/reprint`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (!data.error) {
showToast((data && data.message) ? data.message : 'Receipt sent to printer', 'success');
} else {
showToast(data.message || 'Failed to print receipt', 'error');
}
})
.catch(error => {
console.error('Error reprinting order:', error);
showToast('Failed to print receipt', 'error');
})
.finally(() => {
reprintInProgressByOrder[orderId] = false;
});
}
function showOrderDetails(orderId) {
fetch(`/api/orders/${orderId}`)
.then(response => response.json())
.then(data => {
if (!data.error && data.order) {
displayOrderDetails(data.order);
}
})
.catch(error => {
console.error('Error fetching order details:', error);
});
}
function displayOrderDetails(order) {
const modal = document.getElementById('detailsModal');
const content = document.getElementById('orderDetailsContent');
let itemsHtml = '';
if (order.order.items && order.order.items.length > 0) {
itemsHtml = order.order.items.map(item => {
let modifiersHtml = '';
// Add addons with prices if available
if (item.addons && item.addons.length > 0) {
item.addons.forEach(addon => {
const addonName = addon.name || addon;
const addonPrice = typeof addon === 'object' && addon.price != null ? addon.price : null;
if (addonPrice != null && addonPrice > 0) {
modifiersHtml += `
+ ${addonName} (+${formatCurrency(addonPrice)})`;
} else if (addonPrice === 0) {
modifiersHtml += `
+ ${addonName} (Free)`;
} else {
modifiersHtml += `
+ ${addonName}`;
}
});
}
// Add excludes (no price)
if (item.exclude && item.exclude.length > 0) {
item.exclude.forEach(exc => {
const excName = exc.name || exc;
modifiersHtml += `
− NO ${excName}`;
});
}
return `
| ${item.qty}x |
${item.itemName || item.name}
${modifiersHtml}
|
${formatCurrency(item.price * item.qty)} |
`;
}).join('');
}
content.innerHTML = `
Order Information
Order ID: ${order.id}
Status: ${getStatusBadge(order.localStatus || order.status)}
Time: ${formatDateTime(order.createdAt)}
Type: ${order.order.type || 'N/A'}
Customer Information
Name: ${order.customer.name || 'N/A'}
Phone: ${order.customer.phoneNumber || 'N/A'}
${order.customer.email ? '
Email: ' + order.customer.email + '
' : ''}
${order.order.deliveryAddress ? '
Address: ' + order.order.deliveryAddress + '
' : ''}
Order Items
| Qty |
Item |
Price |
${itemsHtml}
| Subtotal: |
${formatCurrency(order.order.amount)} |
${order.order.taxAmount ? `
| Tax (${order.order.taxRate || 0}%): |
${formatCurrency(order.order.taxAmount)} |
` : ''}
${order.order.deliveryFee ? `
| Delivery Fee: |
${formatCurrency(order.order.deliveryFee)} |
` : ''}
| TOTAL: |
${formatCurrency(order.totalAmount)} |
${order.order.specialInstructions ? `
Special Instructions
${order.order.specialInstructions}
` : ''}
${order.order.deliveryInstructions ? `
Delivery Instructions
${order.order.deliveryInstructions}
` : ''}
${order.order.foodAllergy ? `
⚠ Food Allergy Warning
${order.order.foodAllergyNotes || 'Customer has food allergies'}
` : ''}
`;
modal.classList.add('visible');
}
function closeDetailsModal() {
document.getElementById('detailsModal').classList.remove('visible');
}
// Close modals when clicking outside
window.addEventListener('click', function(event) {
const statusChangeModal = document.getElementById('statusChangeModal');
const cancelModal = document.getElementById('cancelModal');
const detailsModal = document.getElementById('detailsModal');
if (event.target === statusChangeModal) {
closeStatusChangeModal();
}
if (event.target === cancelModal) {
closeCancelModal();
}
if (event.target === detailsModal) {
closeDetailsModal();
}
});
// Close modals with Escape key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeStatusChangeModal();
closeCancelModal();
closeDetailsModal();
}
});
// Connection status monitoring
function checkConnectionStatus() {
// Check local connection (dashboard server)
checkLocalConnection();
// Check external API connection
checkAPIConnection();
}
function checkLocalConnection() {
const startTime = Date.now();
fetch('/api/health/local')
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Server returned error status');
}
})
.then(data => {
connectionStatus.local.status = 'online';
connectionStatus.local.lastCheck = new Date();
connectionStatus.local.consecutiveFailures = 0;
updateConnectionUI('local', 'online', 'Connected');
})
.catch(error => {
console.error('Local connection check failed:', error);
connectionStatus.local.consecutiveFailures++;
if (connectionStatus.local.consecutiveFailures >= 2) {
connectionStatus.local.status = 'offline';
updateConnectionUI('local', 'offline', 'Disconnected');
// Show warning toast on first detection of offline
if (connectionStatus.local.consecutiveFailures === 2) {
showToast('Dashboard server connection lost', 'error');
}
}
});
}
function checkAPIConnection() {
const startTime = Date.now();
fetch('/api/health/external')
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Server returned error status');
}
})
.then(data => {
connectionStatus.api.lastCheck = new Date();
if (data.status === 'online') {
// Was offline, now online - notify user
if (connectionStatus.api.status === 'offline' && connectionStatus.api.consecutiveFailures >= 2) {
showToast('API server connection restored', 'success');
}
connectionStatus.api.status = 'online';
connectionStatus.api.consecutiveFailures = 0;
connectionStatus.api.responseTime = data.responseTime;
let label = 'Connected';
if (data.responseTime) {
label = `${data.responseTime}ms`;
}
updateConnectionUI('api', 'online', label);
} else if (data.status === 'unconfigured') {
connectionStatus.api.status = 'unconfigured';
connectionStatus.api.consecutiveFailures = 0;
updateConnectionUI('api', 'unconfigured', 'Not Configured');
} else {
connectionStatus.api.consecutiveFailures++;
if (connectionStatus.api.consecutiveFailures >= 2) {
connectionStatus.api.status = 'offline';
updateConnectionUI('api', 'offline', data.message || 'Disconnected');
// Show warning toast on first detection of offline
if (connectionStatus.api.consecutiveFailures === 2) {
showToast('API server connection lost: ' + (data.message || 'Disconnected'), 'error');
}
}
}
})
.catch(error => {
console.error('API connection check failed:', error);
connectionStatus.api.consecutiveFailures++;
if (connectionStatus.api.consecutiveFailures >= 2) {
connectionStatus.api.status = 'offline';
updateConnectionUI('api', 'offline', 'Check Failed');
}
});
}
function updateConnectionUI(type, status, label) {
let statusItem, statusIndicator, statusLabel;
if (type === 'local') {
statusItem = document.getElementById('localConnectionStatus');
statusLabel = document.getElementById('localStatusLabel');
} else if (type === 'api') {
statusItem = document.getElementById('apiConnectionStatus');
statusLabel = document.getElementById('apiStatusLabel');
}
if (!statusItem || !statusLabel) return;
statusIndicator = statusItem.querySelector('.status-indicator');
// Remove all status classes
statusIndicator.classList.remove('status-online', 'status-offline', 'status-checking', 'status-unconfigured');
// Add new status class
statusIndicator.classList.add('status-' + status);
// Update label text
statusLabel.textContent = label;
// Update title attribute for tooltip
let tooltipText = '';
if (type === 'local') {
tooltipText = 'Dashboard Server: ';
} else {
tooltipText = 'API Server (api.thinklink.ai): ';
}
tooltipText += label;
if (connectionStatus[type].lastCheck) {
const lastCheckTime = connectionStatus[type].lastCheck.toLocaleTimeString();
tooltipText += ' (Last checked: ' + lastCheckTime + ')';
}
statusItem.title = tooltipText;
}
// Manual sync trigger
function manualSync() {
const syncButton = document.getElementById('syncButton');
const syncText = syncButton ? syncButton.querySelector('.sync-text') : null;
// Prevent multiple simultaneous sync requests
if (syncButton && syncButton.classList.contains('loading')) {
return;
}
if (syncButton) {
syncButton.classList.add('loading');
syncButton.disabled = true;
if (syncText) {
syncText.textContent = 'Syncing...';
}
}
fetch('/api/sync-now', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (!data.error) {
showToast('Checking for new orders...', 'success');
// Refresh after a short delay to allow sync to complete
setTimeout(() => {
refreshOrders();
}, 2000);
} else {
showToast(data.message || 'Sync failed', 'error');
// Remove loading state on error
if (syncButton) {
syncButton.classList.remove('loading');
syncButton.disabled = false;
if (syncText) {
syncText.textContent = 'Sync Now';
}
}
}
})
.catch(error => {
console.error('Manual sync error:', error);
showToast('Sync failed', 'error');
// Remove loading state on error
if (syncButton) {
syncButton.classList.remove('loading');
syncButton.disabled = false;
if (syncText) {
syncText.textContent = 'Sync Now';
}
}
});
}