Files
kitchen-agent/public/js/dashboard.js
2025-10-23 19:02:56 -04:00

954 lines
30 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.
// 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 = '<div class="no-orders">No orders to display</div>';
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 = `
<button class="btn btn-success" onclick="changeStatus(${order.id}, 'preparing')">Start Preparing</button>
`;
} else if (status === 'preparing') {
actions = `
<button class="btn btn-success" onclick="changeStatus(${order.id}, 'ready')">Mark Ready</button>
`;
} else if (status === 'ready') {
actions = `
<button class="btn btn-success" onclick="changeStatus(${order.id}, 'completed')">Complete</button>
`;
}
// Add cancel and reprint buttons for non-completed orders
if (status !== 'canceled' && status !== 'completed') {
actions += `
<button class="btn btn-danger" onclick="openCancelModal(${order.id})">Cancel</button>
`;
}
actions += `
<button class="btn btn-secondary" onclick="reprintOrder(${order.id})">Reprint</button>
<button class="btn btn-secondary" onclick="showOrderDetails(${order.id})">Details</button>
`;
// 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 += `<div class="item-modifier item-addon">+ Add: ${addonsList}</div>`;
}
// Add excludes
if (item.exclude && item.exclude.length > 0) {
const excludeList = item.exclude.map(e => e.name || e).join(', ');
modifiersHtml += `<div class="item-modifier item-exclude"> NO: ${excludeList}</div>`;
}
return `
<div class="order-item">
<div class="item-qty-name">
<span class="item-qty">${item.qty}x</span>
<span class="item-name">${item.itemName || item.name || 'Item'}</span>
</div>
${modifiersHtml}
</div>
`;
}).join('');
}
// Food allergy warning (prominent)
let allergyWarning = '';
if (order.order.foodAllergy) {
const allergyNotes = order.order.foodAllergyNotes || 'Customer has food allergies';
allergyWarning = `
<div class="allergy-warning">
<div class="allergy-icon">⚠️</div>
<div class="allergy-content">
<div class="allergy-title">FOOD ALLERGY ALERT</div>
<div class="allergy-notes">${allergyNotes}</div>
</div>
</div>
`;
}
// Special instructions
let specialInstructionsHtml = '';
if (order.order.specialInstructions) {
specialInstructionsHtml = `
<div class="special-instructions">
<div class="special-instructions-label">📝 Special Instructions:</div>
<div class="special-instructions-text">${order.order.specialInstructions}</div>
</div>
`;
}
return `
<div class="order-card status-${status}">
<div class="order-header">
<div class="order-number">#${order.id}</div>
<div class="order-badge badge-${status}">${statusBadge}</div>
</div>
${allergyWarning}
<div class="order-info">
<div class="order-info-row">
<span class="order-info-label">Time:</span>
<span class="order-info-value">${formatTime(order.createdAt)}</span>
</div>
<div class="order-info-row">
<span class="order-info-label">Customer:</span>
<span class="order-info-value">${order.customer.name || 'N/A'}</span>
</div>
<div class="order-info-row">
<span class="order-info-label">Type:</span>
<span class="order-info-value"><span class="order-type-icon">${typeIcon}</span>${orderType}</span>
</div>
${order.order.deliveryAddress ? `<div class="order-info-row"><span class="order-info-label">Address:</span><span class="order-info-value">${order.order.deliveryAddress}</span></div>` : ''}
</div>
<div class="order-items-section">
<div class="order-items-title">Order Items:</div>
${itemsDetailedHtml}
</div>
${specialInstructionsHtml}
<div class="order-total">
<span class="order-total-label">Total:</span>
<span class="order-total-value">${formatCurrency(order.totalAmount)}</span>
</div>
<div class="order-actions">
${actions}
</div>
</div>
`;
}
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 += `<br><small style="color: #28a745; margin-left: 20px;">+ ${addonName} <span style="color: #666;">(+${formatCurrency(addonPrice)})</span></small>`;
} else if (addonPrice === 0) {
modifiersHtml += `<br><small style="color: #28a745; margin-left: 20px;">+ ${addonName} <span style="color: #666;">(Free)</span></small>`;
} else {
modifiersHtml += `<br><small style="color: #28a745; margin-left: 20px;">+ ${addonName}</small>`;
}
});
}
// Add excludes (no price)
if (item.exclude && item.exclude.length > 0) {
item.exclude.forEach(exc => {
const excName = exc.name || exc;
modifiersHtml += `<br><small style="color: #dc3545; margin-left: 20px;"> NO ${excName}</small>`;
});
}
return `
<tr>
<td style="vertical-align: top; padding: 8px;">${item.qty}x</td>
<td style="padding: 8px;">
<strong>${item.itemName || item.name}</strong>
${modifiersHtml}
</td>
<td style="text-align: right; vertical-align: top; padding: 8px;">${formatCurrency(item.price * item.qty)}</td>
</tr>
`;
}).join('');
}
content.innerHTML = `
<div class="order-details">
<div class="detail-section">
<h3>Order Information</h3>
<p><strong>Order ID:</strong> ${order.id}</p>
<p><strong>Status:</strong> ${getStatusBadge(order.localStatus || order.status)}</p>
<p><strong>Time:</strong> ${formatDateTime(order.createdAt)}</p>
<p><strong>Type:</strong> ${order.order.type || 'N/A'}</p>
</div>
<div class="detail-section">
<h3>Customer Information</h3>
<p><strong>Name:</strong> ${order.customer.name || 'N/A'}</p>
<p><strong>Phone:</strong> ${order.customer.phoneNumber || 'N/A'}</p>
${order.customer.email ? '<p><strong>Email:</strong> ' + order.customer.email + '</p>' : ''}
${order.order.deliveryAddress ? '<p><strong>Address:</strong> ' + order.order.deliveryAddress + '</p>' : ''}
</div>
<div class="detail-section">
<h3>Order Items</h3>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="border-bottom: 2px solid #ddd;">
<th style="text-align: left; padding: 8px;">Qty</th>
<th style="text-align: left; padding: 8px;">Item</th>
<th style="text-align: right; padding: 8px;">Price</th>
</tr>
</thead>
<tbody>
${itemsHtml}
</tbody>
<tfoot>
<tr style="border-top: 2px solid #ddd;">
<td colspan="2" style="text-align: right; padding: 8px;"><strong>Subtotal:</strong></td>
<td style="text-align: right; padding: 8px;"><strong>${formatCurrency(order.order.amount)}</strong></td>
</tr>
${order.order.taxAmount ? `
<tr>
<td colspan="2" style="text-align: right; padding: 8px;">Tax (${order.order.taxRate || 0}%):</td>
<td style="text-align: right; padding: 8px;">${formatCurrency(order.order.taxAmount)}</td>
</tr>` : ''}
${order.order.deliveryFee ? `
<tr>
<td colspan="2" style="text-align: right; padding: 8px;">Delivery Fee:</td>
<td style="text-align: right; padding: 8px;">${formatCurrency(order.order.deliveryFee)}</td>
</tr>` : ''}
<tr style="border-top: 2px solid #333;">
<td colspan="2" style="text-align: right; padding: 8px;"><strong>TOTAL:</strong></td>
<td style="text-align: right; padding: 8px;"><strong>${formatCurrency(order.totalAmount)}</strong></td>
</tr>
</tfoot>
</table>
</div>
${order.order.specialInstructions ? `
<div class="detail-section">
<h3>Special Instructions</h3>
<p>${order.order.specialInstructions}</p>
</div>` : ''}
${order.order.deliveryInstructions ? `
<div class="detail-section">
<h3>Delivery Instructions</h3>
<p>${order.order.deliveryInstructions}</p>
</div>` : ''}
${order.order.foodAllergy ? `
<div class="detail-section" style="background: #ffe6e6; padding: 15px; border-radius: 4px;">
<h3 style="color: #dc3545;">⚠ Food Allergy Warning</h3>
<p><strong>${order.order.foodAllergyNotes || 'Customer has food allergies'}</strong></p>
</div>` : ''}
</div>
`;
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';
}
}
});
}