270 lines
10 KiB
PHP
270 lines
10 KiB
PHP
<?php
|
|
require_once __DIR__ . '/db/config.php';
|
|
require_once __DIR__ . '/includes/functions.php';
|
|
|
|
require_login();
|
|
|
|
$pdo = db();
|
|
$currentUser = get_logged_user();
|
|
|
|
// Fetch outlets based on user assignment
|
|
if (has_permission('all')) {
|
|
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
|
|
} else {
|
|
$stmt = $pdo->prepare("
|
|
SELECT o.* FROM outlets o
|
|
JOIN user_outlets uo ON o.id = uo.outlet_id
|
|
WHERE uo.user_id = ?
|
|
ORDER BY o.name
|
|
");
|
|
$stmt->execute([$currentUser['id']]);
|
|
$outlets = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
$current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : (count($outlets) > 0 ? (int)$outlets[0]['id'] : 1);
|
|
|
|
// Security check: ensure user has access to this outlet
|
|
if (!has_permission('all')) {
|
|
$has_access = false;
|
|
foreach ($outlets as $o) {
|
|
if ($o['id'] == $current_outlet_id) {
|
|
$has_access = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$has_access && count($outlets) > 0) {
|
|
$current_outlet_id = (int)$outlets[0]['id'];
|
|
}
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Kitchen Display System</title>
|
|
<!-- Re-using existing CSS if any, or adding Bootstrap/Custom -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">
|
|
<link href="assets/css/custom.css" rel="stylesheet">
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<style>
|
|
body { background-color: #f4f6f9; }
|
|
.order-card {
|
|
border: none;
|
|
border-radius: 12px;
|
|
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
|
transition: transform 0.2s;
|
|
height: 100%;
|
|
}
|
|
.order-card:hover { transform: translateY(-2px); }
|
|
.card-header {
|
|
background-color: #fff;
|
|
border-bottom: 1px solid #eee;
|
|
border-radius: 12px 12px 0 0 !important;
|
|
padding: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
.status-pending { border-left: 5px solid #ffc107; }
|
|
.status-preparing { border-left: 5px solid #17a2b8; }
|
|
.status-ready { border-left: 5px solid #28a745; }
|
|
|
|
.badge-pending { background-color: #ffc107; color: #000; }
|
|
.badge-preparing { background-color: #17a2b8; }
|
|
.badge-ready { background-color: #28a745; }
|
|
|
|
.item-list { list-style: none; padding: 0; margin: 0; }
|
|
.item-list li {
|
|
padding: 0.5rem 0;
|
|
border-bottom: 1px dashed #eee;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
.item-list li:last-child { border-bottom: none; }
|
|
.item-qty { font-weight: bold; margin-right: 10px; color: #333; }
|
|
|
|
#kitchen-grid { padding: 20px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="container-fluid">
|
|
<div class="d-flex justify-content-between align-items-center py-3 px-4 bg-white shadow-sm mb-4">
|
|
<h2 class="h4 mb-0">Kitchen Display</h2>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<select id="outlet-selector" class="form-select form-select-sm" style="width: auto;">
|
|
<?php foreach ($outlets as $outlet): ?>
|
|
<option value="<?= $outlet['id'] ?>" <?= $current_outlet_id == $outlet['id'] ? 'selected' : '' ?>>
|
|
<?= htmlspecialchars($outlet['name']) ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
<span id="clock" class="text-muted d-none d-md-inline"></span>
|
|
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm btn-outline-dark dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-person-circle"></i> <?= htmlspecialchars($currentUser['username']) ?>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end shadow border-0">
|
|
<li><a class="dropdown-item" href="admin/"><i class="bi bi-shield-lock me-2"></i> Admin Panel</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item text-danger" href="logout.php"><i class="bi bi-box-arrow-right me-2"></i> Logout</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<a href="index.php" class="btn btn-outline-secondary btn-sm">Home</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="kitchen-grid" class="row g-4">
|
|
<!-- Orders will be injected here -->
|
|
<div class="col-12 text-center text-muted py-5">
|
|
Loading orders...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const OUTLET_ID = <?= $current_outlet_id ?>;
|
|
|
|
async function fetchOrders() {
|
|
try {
|
|
const response = await fetch('api/kitchen.php?outlet_id=' + OUTLET_ID);
|
|
if (!response.ok) throw new Error('Network response was not ok');
|
|
const orders = await response.json();
|
|
renderOrders(orders);
|
|
} catch (error) {
|
|
console.error('Error fetching orders:', error);
|
|
}
|
|
}
|
|
|
|
function renderOrders(orders) {
|
|
const grid = document.getElementById('kitchen-grid');
|
|
|
|
if (orders.length === 0) {
|
|
grid.innerHTML = '<div class="col-12 text-center text-muted py-5">No active orders</div>';
|
|
return;
|
|
}
|
|
|
|
// Build the HTML using a variable, so we don't clear/flash the grid if it's identical
|
|
// But for now, simple innerHTML replacement is fine for this MVP.
|
|
const newHtml = orders.map(order => {
|
|
let actionBtn = '';
|
|
let statusBadge = '';
|
|
let cardClass = 'order-card ';
|
|
|
|
if (order.status === 'pending') {
|
|
statusBadge = '<span class="badge badge-pending">Pending</span>';
|
|
cardClass += 'status-pending';
|
|
actionBtn = `<button class="btn btn-info btn-sm w-100 text-white" onclick="updateStatus(${order.id}, 'preparing')">Start Preparing</button>`;
|
|
} else if (order.status === 'preparing') {
|
|
statusBadge = '<span class="badge badge-preparing">Preparing</span>';
|
|
cardClass += 'status-preparing';
|
|
actionBtn = `<button class="btn btn-success btn-sm w-100" onclick="updateStatus(${order.id}, 'ready')">Mark Ready</button>`;
|
|
} else if (order.status === 'ready') {
|
|
statusBadge = '<span class="badge badge-ready">Ready</span>';
|
|
cardClass += 'status-ready';
|
|
actionBtn = `<button class="btn btn-secondary btn-sm w-100" onclick="updateStatus(${order.id}, 'completed')">Serve / Complete</button>`;
|
|
}
|
|
|
|
const itemsHtml = order.items.map(item => `
|
|
<li>
|
|
<span><span class="item-qty">${item.quantity}x</span> ${item.product_name} ${item.variant_name ? '<small class="text-muted">('+item.variant_name+')</small>' : ''}</span>
|
|
</li>
|
|
`).join('');
|
|
|
|
// Calculate time elapsed
|
|
const created = new Date(order.created_at);
|
|
const timeString = created.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
|
|
|
return `
|
|
<div class="col-md-4 col-lg-3">
|
|
<div class="${cardClass}">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<div>#${order.id} <small class="text-muted">${timeString}</small></div>
|
|
${statusBadge}
|
|
</div>
|
|
<div class="card-body">
|
|
<h5 class="card-title mb-3">
|
|
${order.order_type === 'dine-in' ? `Table ${order.table_number}` : order.order_type.toUpperCase()}
|
|
${order.customer_name ? `<br><small class="text-muted">${order.customer_name}</small>` : ''}
|
|
</h5>
|
|
<ul class="item-list mb-3">
|
|
${itemsHtml}
|
|
</ul>
|
|
</div>
|
|
<div class="card-footer bg-white border-top-0 pb-3">
|
|
${actionBtn}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
grid.innerHTML = newHtml;
|
|
}
|
|
|
|
function updateStatus(orderId, newStatus) {
|
|
Swal.fire({
|
|
title: 'Update Status?',
|
|
text: `Move order #${orderId} to ${newStatus}?`,
|
|
icon: 'question',
|
|
showCancelButton: true,
|
|
confirmButtonColor: '#3085d6',
|
|
cancelButtonColor: '#d33',
|
|
confirmButtonText: 'Yes, update it!'
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
performUpdate(orderId, newStatus);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function performUpdate(orderId, newStatus) {
|
|
try {
|
|
const response = await fetch('api/kitchen.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ order_id: orderId, status: newStatus })
|
|
});
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
fetchOrders(); // Refresh immediately
|
|
Swal.fire({
|
|
icon: 'success',
|
|
title: 'Updated!',
|
|
text: `Order #${orderId} moved to ${newStatus}`,
|
|
timer: 1500,
|
|
showConfirmButton: false
|
|
});
|
|
} else {
|
|
Swal.fire('Error', result.error || 'Failed to update', 'error');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating status:', error);
|
|
Swal.fire('Error', 'Failed to connect to server', 'error');
|
|
}
|
|
}
|
|
|
|
// Outlet Selector Logic
|
|
const outletSelector = document.getElementById('outlet-selector');
|
|
if (outletSelector) {
|
|
outletSelector.addEventListener('change', function() {
|
|
window.location.href = '?outlet_id=' + this.value;
|
|
});
|
|
}
|
|
|
|
// Clock
|
|
setInterval(() => {
|
|
const clock = document.getElementById('clock');
|
|
if (clock) clock.textContent = new Date().toLocaleTimeString();
|
|
}, 1000);
|
|
|
|
// Poll orders
|
|
fetchOrders();
|
|
setInterval(fetchOrders, 10000); // Poll every 10s
|
|
|
|
</script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
</body>
|
|
</html>
|