payfrit-works/kds/admin.html
John Mizerek ecea71c533 Implement polling-based order status notifications
Backend changes:
- New API endpoint: checkStatusUpdate.cfm - polls order status and detects changes
- Updated admin.html: added test section for manually updating order status
- Status flow: 1(submitted) → 2(preparing) → 3(ready) → 4(completed)
- Human-readable status messages for each state

Testing interface:
- Order ID input field
- Status dropdown selector
- Direct integration with existing updateStatus.cfm endpoint

This enables real-time status notifications for customer orders with 30-second polling interval (Option 2 approach, with planned migration to self-hosted push).

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-31 16:07:09 -08:00

490 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KDS Admin - Manual Task Creation</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
padding: 30px;
}
h1 {
color: #333;
margin-bottom: 10px;
font-size: 28px;
}
.subtitle {
color: #666;
margin-bottom: 30px;
font-size: 14px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
font-size: 14px;
}
select, input, textarea {
width: 100%;
padding: 12px;
border: 2px solid #e1e8ed;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
select:focus, input:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
textarea {
resize: vertical;
min-height: 80px;
}
.items-section {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.item-row {
background: white;
padding: 15px;
border-radius: 6px;
margin-bottom: 10px;
display: grid;
grid-template-columns: 2fr 1fr auto;
gap: 10px;
align-items: center;
}
.item-row select {
width: 100%;
}
.item-row input {
width: 100%;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #6c757d;
color: white;
}
.btn-secondary:hover {
background: #5a6268;
}
.btn-success {
background: #28a745;
color: white;
width: 100%;
padding: 15px;
font-size: 16px;
}
.btn-success:hover {
background: #218838;
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(40, 167, 69, 0.4);
}
.btn-remove {
background: #dc3545;
color: white;
padding: 8px 16px;
}
.btn-remove:hover {
background: #c82333;
}
.alert {
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.loading {
text-align: center;
padding: 20px;
color: #666;
}
.actions {
display: flex;
gap: 10px;
margin-top: 30px;
}
.quick-actions {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="container">
<h1>KDS Admin - Manual Task Creation</h1>
<p class="subtitle">Create orders manually for testing and demonstration (BusinessID: 17)</p>
<div id="alert"></div>
<div class="form-group">
<label>Service Point (Table/Location)</label>
<select id="servicePointSelect">
<option value="">Loading service points...</option>
</select>
</div>
<div class="items-section">
<label>Order Items</label>
<div id="itemsContainer"></div>
<button class="btn btn-secondary" onclick="addItemRow()">+ Add Item</button>
</div>
<div class="form-group">
<label>Special Instructions / Remarks</label>
<textarea id="remarks" placeholder="e.g., Extra napkins, no ice, etc."></textarea>
</div>
<div class="actions">
<button class="btn btn-success" onclick="createOrder()">Create & Submit Order</button>
</div>
<div class="quick-actions" style="margin-top: 30px;">
<button class="btn btn-secondary" onclick="window.location.href='index.html'">View KDS Display</button>
<button class="btn btn-secondary" onclick="window.location.href='debug.html'">Debug View</button>
</div>
<!-- Test Status Updates Section -->
<div style="margin-top: 40px; padding-top: 30px; border-top: 2px solid #e1e8ed;">
<h2 style="margin-bottom: 20px;">Test Order Status Updates</h2>
<p class="subtitle">Manually update order status to test push notifications</p>
<div class="form-group">
<label>Order ID to Update</label>
<input type="number" id="testOrderId" placeholder="e.g., 318" min="1">
</div>
<div class="form-group">
<label>New Status</label>
<select id="testStatusId">
<option value="1">1 - Submitted</option>
<option value="2" selected>2 - Preparing</option>
<option value="3">3 - Ready</option>
<option value="4">4 - Completed</option>
</select>
</div>
<button class="btn btn-primary" onclick="updateOrderStatus()">Update Order Status</button>
</div>
</div>
<script>
const BUSINESS_ID = 17;
const API_BASE = '../api';
let menuItems = [];
let servicePoints = [];
async function init() {
await loadServicePoints();
await loadMenuItems();
addItemRow(); // Add first item row
}
async function loadServicePoints() {
try {
const response = await fetch(`${API_BASE}/servicepoints/list.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ BusinessID: BUSINESS_ID })
});
const data = await response.json();
if (data.OK && data.ServicePoints) {
servicePoints = data.ServicePoints;
const select = document.getElementById('servicePointSelect');
select.innerHTML = '<option value="">Select a service point...</option>';
servicePoints.forEach(sp => {
const option = document.createElement('option');
option.value = sp.ServicePointID;
option.textContent = `${sp.ServicePointName} (ID: ${sp.ServicePointID})`;
select.appendChild(option);
});
}
} catch (error) {
showAlert('Error loading service points: ' + error.message, 'error');
}
}
async function loadMenuItems() {
try {
const response = await fetch(`${API_BASE}/menu/items.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ BusinessID: BUSINESS_ID })
});
const data = await response.json();
if (data.OK && data.Items) {
// Filter only root items (no parent)
menuItems = data.Items.filter(item => !item.ItemParentItemID || item.ItemParentItemID === 0);
}
} catch (error) {
showAlert('Error loading menu items: ' + error.message, 'error');
}
}
function addItemRow() {
const container = document.getElementById('itemsContainer');
const row = document.createElement('div');
row.className = 'item-row';
const itemSelect = document.createElement('select');
itemSelect.innerHTML = '<option value="">Select item...</option>';
menuItems.forEach(item => {
const option = document.createElement('option');
option.value = item.ItemID;
option.textContent = `${item.ItemName} ($${parseFloat(item.ItemPrice).toFixed(2)})`;
option.dataset.price = item.ItemPrice;
itemSelect.appendChild(option);
});
const qtyInput = document.createElement('input');
qtyInput.type = 'number';
qtyInput.value = '1';
qtyInput.min = '1';
qtyInput.placeholder = 'Qty';
const removeBtn = document.createElement('button');
removeBtn.className = 'btn btn-remove';
removeBtn.textContent = 'Remove';
removeBtn.onclick = () => row.remove();
row.appendChild(itemSelect);
row.appendChild(qtyInput);
row.appendChild(removeBtn);
container.appendChild(row);
}
async function createOrder() {
const servicePointId = document.getElementById('servicePointSelect').value;
const remarks = document.getElementById('remarks').value;
const itemRows = document.querySelectorAll('.item-row');
if (!servicePointId) {
showAlert('Please select a service point', 'error');
return;
}
const items = [];
for (const row of itemRows) {
const select = row.querySelector('select');
const qtyInput = row.querySelector('input');
if (select.value) {
items.push({
itemId: parseInt(select.value),
quantity: parseInt(qtyInput.value) || 1
});
}
}
if (items.length === 0) {
showAlert('Please add at least one item', 'error');
return;
}
try {
// Step 1: Create cart order (using UserID=1 for admin)
const cartResponse = await fetch(`${API_BASE}/orders/getOrCreateCart.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
BusinessID: BUSINESS_ID,
OrderServicePointID: parseInt(servicePointId),
OrderTypeID: 1, // Dine-in
OrderUserID: 1 // Admin user
})
});
const cartData = await cartResponse.json();
if (!cartData.OK) {
throw new Error(cartData.MESSAGE || 'Failed to create cart');
}
const orderId = cartData.ORDER.OrderID;
// Step 2: Add items to order
for (const item of items) {
const itemResponse = await fetch(`${API_BASE}/orders/setLineItem.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
OrderID: orderId,
ParentOrderLineItemID: 0,
ItemID: item.itemId,
IsSelected: true,
Quantity: item.quantity,
Remark: ''
})
});
const itemData = await itemResponse.json();
if (!itemData.OK) {
throw new Error('Failed to add item: ' + itemData.MESSAGE);
}
}
// Step 3: Submit the order
const submitResponse = await fetch(`${API_BASE}/orders/submit.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ OrderID: orderId })
});
const submitData = await submitResponse.json();
if (!submitData.OK) {
throw new Error(submitData.MESSAGE || 'Failed to submit order');
}
showAlert(`Order #${orderId} created and submitted successfully!`, 'success');
// Reset form
document.getElementById('remarks').value = '';
document.getElementById('itemsContainer').innerHTML = '';
addItemRow();
} catch (error) {
showAlert('Error creating order: ' + error.message, 'error');
console.error(error);
}
}
async function updateOrderStatus() {
const orderId = document.getElementById('testOrderId').value;
const statusId = document.getElementById('testStatusId').value;
if (!orderId) {
showAlert('Please enter an Order ID', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/orders/updateStatus.cfm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
OrderID: parseInt(orderId),
StatusID: parseInt(statusId)
})
});
const data = await response.json();
if (!data.OK) {
throw new Error(data.ERROR || data.MESSAGE || 'Failed to update status');
}
const statusNames = {
1: 'Submitted',
2: 'Preparing',
3: 'Ready',
4: 'Completed'
};
showAlert(`Order #${orderId} updated to status: ${statusNames[statusId]}`, 'success');
} catch (error) {
showAlert('Error updating status: ' + error.message, 'error');
console.error(error);
}
}
function showAlert(message, type) {
const alertDiv = document.getElementById('alert');
alertDiv.className = `alert alert-${type}`;
alertDiv.textContent = message;
alertDiv.style.display = 'block';
setTimeout(() => {
alertDiv.style.display = 'none';
}, 5000);
}
// Initialize on page load
init();
</script>
</body>
</html>