Autosave: 20260223-034341
This commit is contained in:
parent
6c8b522da6
commit
e5617b6c15
@ -37,12 +37,61 @@ function isActive($page) {
|
|||||||
<link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>">
|
<link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||||
<style>
|
<style>
|
||||||
body { font-family: 'Inter', sans-serif; background-color: #f8f9fa; }
|
/* Base styles using CSS variables for theming */
|
||||||
.sidebar { height: 100vh; position: fixed; top: 0; left: 0; width: 250px; background: #fff; border-right: 1px solid #eee; padding-top: 0; z-index: 1000; overflow-y: auto; }
|
body {
|
||||||
.sidebar-header { padding: 1rem 1.5rem; border-bottom: 1px solid #eee; position: sticky; top: 0; background: #fff; z-index: 10; }
|
font-family: 'Inter', sans-serif;
|
||||||
.sidebar .nav-link { color: #555; padding: 0.6rem 1.5rem; font-weight: 500; font-size: 0.95rem; }
|
background-color: var(--bg-body);
|
||||||
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #FF6B6B; background: #FFF0F0; border-right: 3px solid #FF6B6B; }
|
color: var(--text-primary);
|
||||||
.sidebar-heading { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: #999; padding: 1.25rem 1.5rem 0.5rem; font-weight: 700; }
|
}
|
||||||
|
.sidebar {
|
||||||
|
height: 100vh;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 250px;
|
||||||
|
background: var(--bg-sidebar);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
padding-top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--bg-sidebar);
|
||||||
|
z-index: 10;
|
||||||
|
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||||
|
}
|
||||||
|
.sidebar .nav-link {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 0.6rem 1.5rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
.sidebar .nav-link:hover, .sidebar .nav-link.active {
|
||||||
|
color: var(--sidebar-active-color);
|
||||||
|
background: var(--sidebar-active-bg);
|
||||||
|
border-right: 3px solid var(--sidebar-active-border);
|
||||||
|
}
|
||||||
|
.sidebar-heading {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--text-heading);
|
||||||
|
padding: 1.25rem 1.5rem 0.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.sidebar-heading i {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
.main-content { margin-left: 250px; padding: 2rem; }
|
.main-content { margin-left: 250px; padding: 2rem; }
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.sidebar { transform: translateX(-100%); transition: transform 0.3s ease; top: 56px; height: calc(100vh - 56px); }
|
.sidebar { transform: translateX(-100%); transition: transform 0.3s ease; top: 56px; height: calc(100vh - 56px); }
|
||||||
@ -50,21 +99,73 @@ function isActive($page) {
|
|||||||
.main-content { margin-left: 0; }
|
.main-content { margin-left: 0; }
|
||||||
.sidebar-header { display: none !important; } /* Hide duplicate logo on mobile */
|
.sidebar-header { display: none !important; } /* Hide duplicate logo on mobile */
|
||||||
}
|
}
|
||||||
.stat-card { border: none; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.04); transition: transform 0.2s; }
|
.stat-card {
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
transition: transform 0.2s, background-color 0.3s;
|
||||||
|
}
|
||||||
.stat-card:hover { transform: translateY(-3px); }
|
.stat-card:hover { transform: translateY(-3px); }
|
||||||
.icon-box { width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 1.25rem; }
|
.icon-box {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
background-color: var(--bg-body); /* subtle background for icons */
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown theming override */
|
||||||
|
.dropdown-menu {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
.dropdown-item {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: var(--bg-body);
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
.dropdown-header {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
// Theme Switcher Logic
|
||||||
|
function setTheme(themeName) {
|
||||||
|
if (themeName === 'default') {
|
||||||
|
document.documentElement.removeAttribute('data-theme');
|
||||||
|
localStorage.removeItem('theme');
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-theme', themeName);
|
||||||
|
localStorage.setItem('theme', themeName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply theme immediately on load to prevent flash
|
||||||
|
(function() {
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) {
|
||||||
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Mobile Header -->
|
<!-- Mobile Header -->
|
||||||
<nav class="navbar navbar-light bg-white border-bottom d-md-none fixed-top">
|
<nav class="navbar navbar-light border-bottom d-md-none fixed-top" style="background-color: var(--bg-card);">
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<a class="navbar-brand fw-bold text-dark" href="index.php">
|
<a class="navbar-brand fw-bold" href="index.php" style="color: var(--text-primary);">
|
||||||
<?php if ($logoUrl): ?>
|
<?php if ($logoUrl): ?>
|
||||||
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="height: 30px;">
|
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="height: 30px;">
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<?= htmlspecialchars($companyName) ?><span class="text-primary">Admin</span>
|
<?= htmlspecialchars($companyName) ?><span style="color: var(--accent-color);">Admin</span>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</a>
|
</a>
|
||||||
<button class="navbar-toggler border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu">
|
<button class="navbar-toggler border-0" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu">
|
||||||
@ -80,12 +181,12 @@ function isActive($page) {
|
|||||||
<?php if ($logoUrl): ?>
|
<?php if ($logoUrl): ?>
|
||||||
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="max-height: 50px; max-width: 100%;">
|
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="max-height: 50px; max-width: 100%;">
|
||||||
<?php else: ?>
|
<?php else: ?>
|
||||||
<h4 class="fw-bold m-0 text-dark"><?= htmlspecialchars($companyName) ?><span style="color: #FF6B6B;">.</span></h4>
|
<h4 class="fw-bold m-0" style="color: var(--text-primary);"><?= htmlspecialchars($companyName) ?><span style="color: var(--accent-color);">.</span></h4>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="px-4 py-3 d-md-none">
|
<div class="px-4 py-3 d-md-none">
|
||||||
<h5 class="fw-bold">Menu</h5>
|
<h5 class="fw-bold" style="color: var(--text-primary);">Menu</h5>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="sidebar-content">
|
<div class="sidebar-content">
|
||||||
@ -97,7 +198,7 @@ function isActive($page) {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading">POS & Operations</h6>
|
<h6 class="sidebar-heading"><i class="bi bi-shop"></i> POS & Operations</h6>
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="../pos.php" target="_blank">
|
<a class="nav-link" href="../pos.php" target="_blank">
|
||||||
@ -116,7 +217,7 @@ function isActive($page) {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading">Menu Management</h6>
|
<h6 class="sidebar-heading"><i class="bi bi-menu-button-wide"></i> Menu Management</h6>
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= isActive('products.php') ?>" href="products.php">
|
<a class="nav-link <?= isActive('products.php') ?>" href="products.php">
|
||||||
@ -130,7 +231,7 @@ function isActive($page) {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading">Restaurant Setup</h6>
|
<h6 class="sidebar-heading"><i class="bi bi-gear-wide-connected"></i> Restaurant Setup</h6>
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
|
<a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
|
||||||
@ -149,7 +250,7 @@ function isActive($page) {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading">People & Partners</h6>
|
<h6 class="sidebar-heading"><i class="bi bi-people"></i> People & Partners</h6>
|
||||||
<ul class="nav flex-column">
|
<ul class="nav flex-column">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= isActive('customers.php') ?>" href="customers.php">
|
<a class="nav-link <?= isActive('customers.php') ?>" href="customers.php">
|
||||||
@ -168,7 +269,7 @@ function isActive($page) {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h6 class="sidebar-heading">Settings</h6>
|
<h6 class="sidebar-heading"><i class="bi bi-sliders"></i> Settings</h6>
|
||||||
<ul class="nav flex-column mb-5">
|
<ul class="nav flex-column mb-5">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link <?= isActive('payment_types.php') ?>" href="payment_types.php">
|
<a class="nav-link <?= isActive('payment_types.php') ?>" href="payment_types.php">
|
||||||
@ -185,6 +286,7 @@ function isActive($page) {
|
|||||||
<i class="bi bi-building me-2"></i> Company
|
<i class="bi bi-building me-2"></i> Company
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li class="nav-item border-top mt-2 pt-2">
|
<li class="nav-item border-top mt-2 pt-2">
|
||||||
<a class="nav-link text-muted" href="../index.php" target="_blank">
|
<a class="nav-link text-muted" href="../index.php" target="_blank">
|
||||||
<i class="bi bi-box-arrow-up-right me-2"></i> View Site
|
<i class="bi bi-box-arrow-up-right me-2"></i> View Site
|
||||||
@ -195,3 +297,38 @@ function isActive($page) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="main-content pt-5 pt-md-4">
|
<div class="main-content pt-5 pt-md-4">
|
||||||
|
<!-- Top Header -->
|
||||||
|
<header class="d-flex justify-content-end align-items-center mb-4 pb-3 border-bottom">
|
||||||
|
<div class="d-flex align-items-center gap-3">
|
||||||
|
<!-- Theme Switcher -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-link text-decoration-none dropdown-toggle d-flex align-items-center gap-2" type="button" id="topThemeDropdown" data-bs-toggle="dropdown" aria-expanded="false" style="color: var(--text-primary);">
|
||||||
|
<i class="bi bi-palette"></i>
|
||||||
|
<span class="d-none d-sm-inline">Theme</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="topThemeDropdown">
|
||||||
|
<li><h6 class="dropdown-header">Select Theme</h6></li>
|
||||||
|
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('default')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#eee;border:1px solid #ddd"></span> Default</button></li>
|
||||||
|
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('dark')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#333"></span> Dark</button></li>
|
||||||
|
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('ocean')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#0077B6"></span> Ocean</button></li>
|
||||||
|
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('forest')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#2D6A4F"></span> Forest</button></li>
|
||||||
|
<li><button class="dropdown-item d-flex align-items-center gap-2" onclick="setTheme('grape')"><span class="d-inline-block rounded-circle" style="width:12px;height:12px;background:#7B1FA2"></span> Grape</button></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User Profile -->
|
||||||
|
<div class="dropdown">
|
||||||
|
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle" id="userDropdown" data-bs-toggle="dropdown" aria-expanded="false" style="color: var(--text-primary);">
|
||||||
|
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center text-white me-2 shadow-sm" style="width:38px;height:38px; font-weight:600;">A</div>
|
||||||
|
<span class="d-none d-sm-inline fw-medium">Admin</span>
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="userDropdown">
|
||||||
|
<li><span class="dropdown-item-text text-muted small">Signed in as Admin</span></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item" href="company.php"><i class="bi bi-building me-2"></i> Company Settings</a></li>
|
||||||
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
<li><a class="dropdown-item text-danger" href="#"><i class="bi bi-box-arrow-right me-2"></i> Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
@ -50,10 +50,12 @@ if (!empty($_GET['search'])) {
|
|||||||
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
|
||||||
|
|
||||||
// Changed alias 'out' to 'ot' to avoid reserved keyword conflict
|
// Changed alias 'out' to 'ot' to avoid reserved keyword conflict
|
||||||
$query = "SELECT o.*, ot.name as outlet_name,
|
// Added join to payment_types to get payment name
|
||||||
|
$query = "SELECT o.*, ot.name as outlet_name, pt.name as payment_type_name,
|
||||||
(SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary
|
(SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary
|
||||||
FROM orders o
|
FROM orders o
|
||||||
LEFT JOIN outlets ot ON o.outlet_id = ot.id
|
LEFT JOIN outlets ot ON o.outlet_id = ot.id
|
||||||
|
LEFT JOIN payment_types pt ON o.payment_type_id = pt.id
|
||||||
$where_clause
|
$where_clause
|
||||||
ORDER BY o.created_at DESC";
|
ORDER BY o.created_at DESC";
|
||||||
|
|
||||||
@ -128,6 +130,7 @@ include 'includes/header.php';
|
|||||||
<th>Source</th>
|
<th>Source</th>
|
||||||
<th>Items</th>
|
<th>Items</th>
|
||||||
<th>Total</th>
|
<th>Total</th>
|
||||||
|
<th>Payment</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Time</th>
|
<th>Time</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
@ -136,7 +139,7 @@ include 'includes/header.php';
|
|||||||
<tbody>
|
<tbody>
|
||||||
<?php foreach ($orders as $order): ?>
|
<?php foreach ($orders as $order): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ps-4 fw-medium">#<?= $order['id'] ?></td>
|
<td class="ps-4">#<?= $order['id'] ?></td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-white text-dark border">
|
<span class="badge bg-white text-dark border">
|
||||||
<i class="bi bi-shop me-1"></i>
|
<i class="bi bi-shop me-1"></i>
|
||||||
@ -145,7 +148,7 @@ include 'includes/header.php';
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<?php if (!empty($order['customer_name'])): ?>
|
<?php if (!empty($order['customer_name'])): ?>
|
||||||
<div class="fw-medium"><?= htmlspecialchars($order['customer_name']) ?></div>
|
<div><?= htmlspecialchars($order['customer_name']) ?></div>
|
||||||
<?php if (!empty($order['customer_phone'])): ?>
|
<?php if (!empty($order['customer_phone'])): ?>
|
||||||
<small class="text-muted"><i class="bi bi-telephone me-1"></i><?= htmlspecialchars($order['customer_phone']) ?></small>
|
<small class="text-muted"><i class="bi bi-telephone me-1"></i><?= htmlspecialchars($order['customer_phone']) ?></small>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@ -173,7 +176,22 @@ include 'includes/header.php';
|
|||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
<td><small class="text-muted"><?= htmlspecialchars($order['items_summary']) ?></small></td>
|
<td><small class="text-muted"><?= htmlspecialchars($order['items_summary']) ?></small></td>
|
||||||
<td class="fw-bold"><?= format_currency($order['total_amount']) ?></td>
|
<td><?= format_currency($order['total_amount']) ?></td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$payment_name = $order['payment_type_name'] ?? 'Unpaid';
|
||||||
|
$payment_badge = match(strtolower($payment_name)) {
|
||||||
|
'cash' => 'bg-success',
|
||||||
|
'credit card' => 'bg-primary',
|
||||||
|
'loyalty redeem' => 'bg-warning',
|
||||||
|
'unpaid' => 'bg-secondary',
|
||||||
|
default => 'bg-secondary'
|
||||||
|
};
|
||||||
|
?>
|
||||||
|
<span class="badge <?= $payment_badge ?> text-dark bg-opacity-25 border border-<?= str_replace('bg-', '', $payment_badge) ?>">
|
||||||
|
<?= htmlspecialchars($payment_name) ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge rounded-pill status-<?= $order['status'] ?>">
|
<span class="badge rounded-pill status-<?= $order['status'] ?>">
|
||||||
<?= ucfirst($order['status']) ?>
|
<?= ucfirst($order['status']) ?>
|
||||||
@ -187,10 +205,6 @@ include 'includes/header.php';
|
|||||||
<form method="POST" class="d-flex gap-2">
|
<form method="POST" class="d-flex gap-2">
|
||||||
<input type="hidden" name="order_id" value="<?= $order['id'] ?>">
|
<input type="hidden" name="order_id" value="<?= $order['id'] ?>">
|
||||||
<input type="hidden" name="action" value="update_status">
|
<input type="hidden" name="action" value="update_status">
|
||||||
<!-- Preserve filter params in form action? No, form POSTs to same URL.
|
|
||||||
We can add hidden inputs to redirect back with params, but header location above handles it if we pass GET params.
|
|
||||||
The POST form action will just be "orders.php".
|
|
||||||
Ideally we should append query string to action. -->
|
|
||||||
|
|
||||||
<?php if ($order['status'] === 'pending'): ?>
|
<?php if ($order['status'] === 'pending'): ?>
|
||||||
<button type="submit" name="status" value="preparing" class="btn btn-sm btn-primary">
|
<button type="submit" name="status" value="preparing" class="btn btn-sm btn-primary">
|
||||||
@ -216,7 +230,7 @@ include 'includes/header.php';
|
|||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
<?php if (empty($orders)): ?>
|
<?php if (empty($orders)): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="10" class="text-center py-5 text-muted">
|
<td colspan="11" class="text-center py-5 text-muted">
|
||||||
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
|
||||||
No active orders found matching your criteria.
|
No active orders found matching your criteria.
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -125,10 +125,51 @@ try {
|
|||||||
$discount = isset($data['discount']) ? floatval($data['discount']) : 0.00;
|
$discount = isset($data['discount']) ? floatval($data['discount']) : 0.00;
|
||||||
$total_amount = isset($data['total_amount']) ? floatval($data['total_amount']) : 0.00;
|
$total_amount = isset($data['total_amount']) ? floatval($data['total_amount']) : 0.00;
|
||||||
|
|
||||||
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, payment_type_id, total_amount, discount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
|
// Check for Existing Order ID (Update Mode)
|
||||||
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $total_amount, $discount]);
|
$order_id = isset($data['order_id']) ? intval($data['order_id']) : null;
|
||||||
$order_id = $pdo->lastInsertId();
|
$is_update = false;
|
||||||
|
|
||||||
|
if ($order_id) {
|
||||||
|
// Verify existence and status (optional: only update pending orders?)
|
||||||
|
$checkStmt = $pdo->prepare("SELECT id FROM orders WHERE id = ?");
|
||||||
|
$checkStmt->execute([$order_id]);
|
||||||
|
if ($checkStmt->fetch()) {
|
||||||
|
$is_update = true;
|
||||||
|
} else {
|
||||||
|
// If ID sent but not found, create new? Or error?
|
||||||
|
// Let's treat as new if not found to avoid errors, or maybe user meant to update.
|
||||||
|
// Safe bet: error if explicit update requested but failed.
|
||||||
|
// But for now, let's just create new if ID is invalid, or better, stick to the plan: Update if ID present.
|
||||||
|
$order_id = null; // Reset to create new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($is_update) {
|
||||||
|
// UPDATE Existing Order
|
||||||
|
$stmt = $pdo->prepare("UPDATE orders SET
|
||||||
|
outlet_id = ?, table_id = ?, table_number = ?, order_type = ?,
|
||||||
|
customer_id = ?, customer_name = ?, customer_phone = ?,
|
||||||
|
payment_type_id = ?, total_amount = ?, discount = ?, status = 'pending'
|
||||||
|
WHERE id = ?");
|
||||||
|
$stmt->execute([
|
||||||
|
$outlet_id, $table_id, $table_number, $order_type,
|
||||||
|
$customer_id, $customer_name, $customer_phone,
|
||||||
|
$payment_type_id, $total_amount, $discount,
|
||||||
|
$order_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Clear existing items to replace them
|
||||||
|
$delStmt = $pdo->prepare("DELETE FROM order_items WHERE order_id = ?");
|
||||||
|
$delStmt->execute([$order_id]);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// INSERT New Order
|
||||||
|
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, payment_type_id, total_amount, discount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
|
||||||
|
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $total_amount, $discount]);
|
||||||
|
$order_id = $pdo->lastInsertId();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert Items (Common for both Insert and Update)
|
||||||
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");
|
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");
|
||||||
|
|
||||||
// Pre-prepare statements for name fetching
|
// Pre-prepare statements for name fetching
|
||||||
|
|||||||
96
api/recall_orders.php
Normal file
96
api/recall_orders.php
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
require_once __DIR__ . '/../db/config.php';
|
||||||
|
|
||||||
|
$pdo = db();
|
||||||
|
$action = $_GET['action'] ?? 'list';
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($action === 'list') {
|
||||||
|
$outlet_id = $_GET['outlet_id'] ?? 1;
|
||||||
|
|
||||||
|
$stmt = $pdo->prepare("
|
||||||
|
SELECT o.id, o.customer_name, o.customer_phone, o.total_amount, o.created_at, o.table_number, o.order_type,
|
||||||
|
(SELECT COUNT(*) FROM order_items WHERE order_id = o.id) as item_count
|
||||||
|
FROM orders o
|
||||||
|
WHERE o.outlet_id = ?
|
||||||
|
AND o.status = 'pending'
|
||||||
|
AND (o.payment_type_id IS NULL OR o.payment_type_id = 0)
|
||||||
|
ORDER BY o.created_at DESC
|
||||||
|
");
|
||||||
|
$stmt->execute([$outlet_id]);
|
||||||
|
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Format date for JS
|
||||||
|
foreach ($orders as &$o) {
|
||||||
|
$o['time_formatted'] = date('H:i', strtotime($o['created_at']));
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'orders' => $orders]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'details') {
|
||||||
|
$order_id = $_GET['id'] ?? null;
|
||||||
|
if (!$order_id) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Missing ID']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch Order
|
||||||
|
$stmt = $pdo->prepare("SELECT * FROM orders WHERE id = ?");
|
||||||
|
$stmt->execute([$order_id]);
|
||||||
|
$order = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
if (!$order) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Order not found']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch Items
|
||||||
|
$stmtItems = $pdo->prepare("
|
||||||
|
SELECT oi.*, p.name as product_name, p.price as base_price, v.name as variant_name, v.price_adjustment
|
||||||
|
FROM order_items oi
|
||||||
|
JOIN products p ON oi.product_id = p.id
|
||||||
|
LEFT JOIN product_variants v ON oi.variant_id = v.id
|
||||||
|
WHERE oi.order_id = ?
|
||||||
|
");
|
||||||
|
$stmtItems->execute([$order_id]);
|
||||||
|
$items = $stmtItems->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
// Format items for JS cart
|
||||||
|
$cartItems = [];
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$cartItems[] = [
|
||||||
|
'id' => $item['product_id'],
|
||||||
|
'name' => $item['product_name'],
|
||||||
|
'price' => floatval($item['unit_price']),
|
||||||
|
'base_price' => floatval($item['base_price']), // Note: this might be different from current DB price if changed, but for recall we usually want the ORIGINAL price or CURRENT?
|
||||||
|
// Ideally, if we edit an order, we might want to keep original prices OR update to current.
|
||||||
|
// For simplicity, let's use the price stored in order_items (unit_price) as the effective price.
|
||||||
|
'quantity' => intval($item['quantity']),
|
||||||
|
'variant_id' => $item['variant_id'],
|
||||||
|
'variant_name' => $item['variant_name'],
|
||||||
|
'hasVariants' => !empty($item['variant_id'])
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch Customer
|
||||||
|
$customer = null;
|
||||||
|
if ($order['customer_id']) {
|
||||||
|
$cStmt = $pdo->prepare("SELECT * FROM customers WHERE id = ?");
|
||||||
|
$cStmt->execute([$order['customer_id']]);
|
||||||
|
$customer = $cStmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'order' => $order,
|
||||||
|
'items' => $cartItems,
|
||||||
|
'customer' => $customer
|
||||||
|
]);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
@ -1,29 +1,133 @@
|
|||||||
:root {
|
:root {
|
||||||
|
/* Default Theme (Light/Minimal) */
|
||||||
--primary-color: #1A1A1A;
|
--primary-color: #1A1A1A;
|
||||||
--accent-color: #E63946;
|
--accent-color: #E63946;
|
||||||
--secondary-bg: #F5F5F5;
|
--bg-body: #F5F5F5;
|
||||||
|
--bg-card: #FFFFFF;
|
||||||
|
--bg-sidebar: #FFFFFF;
|
||||||
--text-primary: #1A1A1A;
|
--text-primary: #1A1A1A;
|
||||||
--text-secondary: #666666;
|
--text-secondary: #666666;
|
||||||
--white: #FFFFFF;
|
--text-heading: #999999;
|
||||||
|
--border-color: #EEEEEE;
|
||||||
--border-radius: 8px;
|
--border-radius: 8px;
|
||||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
--shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
--sidebar-active-bg: #FFF0F0;
|
||||||
|
--sidebar-active-color: #FF6B6B;
|
||||||
|
--sidebar-active-border: #FF6B6B;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Theme */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-color: #E0E0E0;
|
||||||
|
--accent-color: #FF6B6B;
|
||||||
|
--bg-body: #121212;
|
||||||
|
--bg-card: #1E1E1E;
|
||||||
|
--bg-sidebar: #1E1E1E;
|
||||||
|
--text-primary: #E0E0E0;
|
||||||
|
--text-secondary: #A0A0A0;
|
||||||
|
--text-heading: #888888;
|
||||||
|
--border-color: #333333;
|
||||||
|
--shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
|
||||||
|
--sidebar-active-bg: #2C2C2C;
|
||||||
|
--sidebar-active-color: #FF6B6B;
|
||||||
|
--sidebar-active-border: #FF6B6B;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ocean Theme */
|
||||||
|
[data-theme="ocean"] {
|
||||||
|
--primary-color: #003049;
|
||||||
|
--accent-color: #0077B6;
|
||||||
|
--bg-body: #E0F7FA;
|
||||||
|
--bg-card: #FFFFFF;
|
||||||
|
--bg-sidebar: #FFFFFF;
|
||||||
|
--text-primary: #003049;
|
||||||
|
--text-secondary: #546E7A;
|
||||||
|
--text-heading: #0288D1;
|
||||||
|
--border-color: #B2EBF2;
|
||||||
|
--sidebar-active-bg: #E1F5FE;
|
||||||
|
--sidebar-active-color: #0288D1;
|
||||||
|
--sidebar-active-border: #0288D1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forest Theme */
|
||||||
|
[data-theme="forest"] {
|
||||||
|
--primary-color: #1B4332;
|
||||||
|
--accent-color: #2D6A4F;
|
||||||
|
--bg-body: #F1F8E9;
|
||||||
|
--bg-card: #FFFFFF;
|
||||||
|
--bg-sidebar: #FFFFFF;
|
||||||
|
--text-primary: #1B4332;
|
||||||
|
--text-secondary: #558B2F;
|
||||||
|
--text-heading: #33691E;
|
||||||
|
--border-color: #C8E6C9;
|
||||||
|
--sidebar-active-bg: #E8F5E9;
|
||||||
|
--sidebar-active-color: #2E7D32;
|
||||||
|
--sidebar-active-border: #2E7D32;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grape Theme */
|
||||||
|
[data-theme="grape"] {
|
||||||
|
--primary-color: #4A148C;
|
||||||
|
--accent-color: #7B1FA2;
|
||||||
|
--bg-body: #F3E5F5;
|
||||||
|
--bg-card: #FFFFFF;
|
||||||
|
--bg-sidebar: #FFFFFF;
|
||||||
|
--text-primary: #4A148C;
|
||||||
|
--text-secondary: #7B1FA2;
|
||||||
|
--text-heading: #8E24AA;
|
||||||
|
--border-color: #E1BEE7;
|
||||||
|
--sidebar-active-bg: #F3E5F5;
|
||||||
|
--sidebar-active-color: #8E24AA;
|
||||||
|
--sidebar-active-border: #8E24AA;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
background-color: var(--secondary-bg);
|
background-color: var(--bg-body);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navbar {
|
.navbar {
|
||||||
background-color: var(--white);
|
background-color: var(--bg-card);
|
||||||
border-bottom: 1px solid #EAEAEA;
|
border-bottom: 1px solid var(--border-color);
|
||||||
padding: 1rem 0;
|
padding: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
background: var(--bg-sidebar) !important;
|
||||||
|
border-right: 1px solid var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
background: var(--bg-sidebar) !important;
|
||||||
|
border-bottom: 1px solid var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link {
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar .nav-link:hover, .sidebar .nav-link.active {
|
||||||
|
color: var(--sidebar-active-color) !important;
|
||||||
|
background: var(--sidebar-active-bg) !important;
|
||||||
|
border-right: 3px solid var(--sidebar-active-border) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-heading {
|
||||||
|
color: var(--text-heading) !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-heading i {
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
.brand-logo {
|
.brand-logo {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
@ -38,15 +142,16 @@ body {
|
|||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
border-bottom: 2px solid var(--accent-color);
|
border-bottom: 2px solid var(--accent-color);
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-card {
|
.product-card {
|
||||||
background: var(--white);
|
background: var(--bg-card);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
border: 1px solid #EAEAEA;
|
border: 1px solid var(--border-color);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,7 +162,7 @@ body {
|
|||||||
.product-image {
|
.product-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 180px;
|
height: 180px;
|
||||||
background-color: #EEEEEE;
|
background-color: var(--border-color);
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +174,7 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-desc {
|
.product-desc {
|
||||||
@ -86,7 +192,7 @@ body {
|
|||||||
|
|
||||||
.btn-add {
|
.btn-add {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
color: var(--white);
|
color: #FFFFFF;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -94,15 +200,15 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.btn-add:hover {
|
.btn-add:hover {
|
||||||
background-color: #333333;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-sidebar {
|
.cart-sidebar {
|
||||||
background: var(--white);
|
background: var(--bg-card);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-left: 1px solid #EAEAEA;
|
border-left: 1px solid var(--border-color);
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -113,17 +219,19 @@ body {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
padding-bottom: 1rem;
|
padding-bottom: 1rem;
|
||||||
border-bottom: 1px solid #EEEEEE;
|
border-bottom: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-item-name {
|
.cart-item-name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-item-price {
|
.cart-item-price {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cart-total {
|
.cart-total {
|
||||||
@ -134,6 +242,7 @@ body {
|
|||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-badge {
|
.order-badge {
|
||||||
@ -147,7 +256,7 @@ body {
|
|||||||
|
|
||||||
/* Admin Styles */
|
/* Admin Styles */
|
||||||
.admin-table-container {
|
.admin-table-container {
|
||||||
background: var(--white);
|
background: var(--bg-card);
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
@ -166,10 +275,10 @@ body {
|
|||||||
|
|
||||||
/* Friendly Table & UI Enhancements */
|
/* Friendly Table & UI Enhancements */
|
||||||
.filter-bar {
|
.filter-bar {
|
||||||
background: #fff;
|
background: var(--bg-card);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
box-shadow: 0 2px 12px rgba(0,0,0,0.03);
|
box-shadow: var(--shadow);
|
||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,13 +292,13 @@ body {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
color: #999;
|
color: var(--text-heading);
|
||||||
letter-spacing: 0.8px;
|
letter-spacing: 0.8px;
|
||||||
padding: 0 1.5rem 0.5rem 1.5rem;
|
padding: 0 1.5rem 0.5rem 1.5rem;
|
||||||
}
|
}
|
||||||
.friendly-table tbody tr {
|
.friendly-table tbody tr {
|
||||||
background: #fff;
|
background: var(--bg-card);
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
|
box-shadow: var(--shadow);
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
}
|
}
|
||||||
@ -201,7 +310,8 @@ body {
|
|||||||
border: none;
|
border: none;
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
background-color: #fff; /* Ensure bg is applied to cells for radius to work if needed */
|
background-color: var(--bg-card);
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.friendly-table td:first-child {
|
.friendly-table td:first-child {
|
||||||
border-top-left-radius: 12px;
|
border-top-left-radius: 12px;
|
||||||
@ -219,8 +329,8 @@ body {
|
|||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
.badge-soft {
|
.badge-soft {
|
||||||
background-color: #f3f4f6;
|
background-color: var(--border-color);
|
||||||
color: #4b5563;
|
color: var(--text-secondary);
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 0.4em 0.8em;
|
padding: 0.4em 0.8em;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@ -233,13 +343,13 @@ body {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
background: #f8f9fa;
|
background: var(--border-color);
|
||||||
color: #6c757d;
|
color: var(--text-secondary);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
.btn-icon-soft:hover {
|
.btn-icon-soft:hover {
|
||||||
background: #e9ecef;
|
background: var(--accent-color);
|
||||||
color: #495057;
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
.btn-icon-soft.delete:hover {
|
.btn-icon-soft.delete:hover {
|
||||||
background: #fee2e2;
|
background: #fee2e2;
|
||||||
@ -252,5 +362,35 @@ body {
|
|||||||
.text-price {
|
.text-price {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #111827;
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bootstrap Utility Overrides for Dark Mode */
|
||||||
|
[data-theme="dark"] .bg-white {
|
||||||
|
background-color: var(--bg-card) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .bg-light {
|
||||||
|
background-color: var(--bg-body) !important;
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .text-muted {
|
||||||
|
color: var(--text-secondary) !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .text-dark {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .border-bottom,
|
||||||
|
[data-theme="dark"] .border-top,
|
||||||
|
[data-theme="dark"] .border-end,
|
||||||
|
[data-theme="dark"] .border-start,
|
||||||
|
[data-theme="dark"] .border {
|
||||||
|
border-color: var(--border-color) !important;
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .card {
|
||||||
|
background-color: var(--bg-card);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
[data-theme="dark"] .table {
|
||||||
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
@ -13,6 +13,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let cart = [];
|
let cart = [];
|
||||||
|
let currentOrderId = null; // Track order ID for updates
|
||||||
const cartItemsContainer = document.getElementById('cart-items');
|
const cartItemsContainer = document.getElementById('cart-items');
|
||||||
const cartTotalPrice = document.getElementById('cart-total-price');
|
const cartTotalPrice = document.getElementById('cart-total-price');
|
||||||
const cartSubtotal = document.getElementById('cart-subtotal');
|
const cartSubtotal = document.getElementById('cart-subtotal');
|
||||||
@ -21,6 +22,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
// Updated Button References
|
// Updated Button References
|
||||||
const quickOrderBtn = document.getElementById('quick-order-btn');
|
const quickOrderBtn = document.getElementById('quick-order-btn');
|
||||||
const placeOrderBtn = document.getElementById('place-order-btn');
|
const placeOrderBtn = document.getElementById('place-order-btn');
|
||||||
|
const recallBtn = document.getElementById('recall-bill-btn');
|
||||||
|
|
||||||
|
// Recall Modal
|
||||||
|
const recallModalEl = document.getElementById('recallOrderModal');
|
||||||
|
const recallModal = recallModalEl ? new bootstrap.Modal(recallModalEl) : null;
|
||||||
|
const recallList = document.getElementById('recall-orders-list');
|
||||||
|
|
||||||
// Loyalty State
|
// Loyalty State
|
||||||
let isLoyaltyRedemption = false;
|
let isLoyaltyRedemption = false;
|
||||||
@ -105,6 +112,93 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Recall Order Logic ---
|
||||||
|
if (recallBtn) {
|
||||||
|
recallBtn.addEventListener('click', () => {
|
||||||
|
fetchRecallOrders();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchRecallOrders() {
|
||||||
|
if (!recallList) return;
|
||||||
|
recallList.innerHTML = '<div class="text-center py-5"><div class="spinner-border text-primary" role="status"></div></div>';
|
||||||
|
if (recallModal) recallModal.show();
|
||||||
|
|
||||||
|
const outletId = new URLSearchParams(window.location.search).get('outlet_id') || 1;
|
||||||
|
fetch(`api/recall_orders.php?action=list&outlet_id=${outletId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
recallList.innerHTML = '';
|
||||||
|
if (data.success && data.orders.length > 0) {
|
||||||
|
data.orders.forEach(order => {
|
||||||
|
const item = document.createElement('button');
|
||||||
|
item.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
|
||||||
|
item.innerHTML = `
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold">Order #${order.id} <span class="badge bg-secondary ms-1">${order.order_type}</span></div>
|
||||||
|
<small class="text-muted">
|
||||||
|
${order.customer_name || 'Guest'}
|
||||||
|
${order.table_number ? ' • Table ' + order.table_number : ''}
|
||||||
|
• ${order.time_formatted}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<div class="fw-bold text-primary">${formatCurrency(order.total_amount)}</div>
|
||||||
|
<small class="text-muted">${order.item_count} items</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
item.onclick = () => loadRecalledOrder(order.id);
|
||||||
|
recallList.appendChild(item);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
recallList.innerHTML = '<div class="p-4 text-center text-muted">No unpaid bills found.</div>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
recallList.innerHTML = '<div class="alert alert-danger">Error fetching orders.</div>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadRecalledOrder(orderId) {
|
||||||
|
fetch(`api/recall_orders.php?action=details&id=${orderId}`)
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Set Order ID
|
||||||
|
currentOrderId = data.order.id;
|
||||||
|
|
||||||
|
// Set Customer
|
||||||
|
if (data.customer) {
|
||||||
|
selectCustomer(data.customer);
|
||||||
|
} else {
|
||||||
|
if (clearCustomerBtn) clearCustomerBtn.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set Table/Order Type
|
||||||
|
const otInput = document.querySelector(`input[name="order_type"][value="${data.order.order_type}"]`);
|
||||||
|
if (otInput) {
|
||||||
|
otInput.checked = true;
|
||||||
|
if (data.order.order_type === 'dine-in' && data.order.table_id) {
|
||||||
|
selectTable(data.order.table_id, data.order.table_number);
|
||||||
|
} else {
|
||||||
|
checkOrderType();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate Cart
|
||||||
|
cart = data.items; // Assuming format matches
|
||||||
|
cartDiscountInput.value = data.order.discount || 0;
|
||||||
|
|
||||||
|
updateCart();
|
||||||
|
if (recallModal) recallModal.hide();
|
||||||
|
showToast(`Order #${orderId} loaded!`, 'success');
|
||||||
|
} else {
|
||||||
|
showToast(data.error || 'Failed to load order', 'danger');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => showToast('Error loading order details', 'danger'));
|
||||||
|
}
|
||||||
|
|
||||||
// --- Customer Search ---
|
// --- Customer Search ---
|
||||||
let searchTimeout;
|
let searchTimeout;
|
||||||
if (customerSearchInput) {
|
if (customerSearchInput) {
|
||||||
@ -418,6 +512,14 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
cartTotalPrice.innerText = formatCurrency(0);
|
cartTotalPrice.innerText = formatCurrency(0);
|
||||||
if (quickOrderBtn) quickOrderBtn.disabled = true;
|
if (quickOrderBtn) quickOrderBtn.disabled = true;
|
||||||
if (placeOrderBtn) placeOrderBtn.disabled = true;
|
if (placeOrderBtn) placeOrderBtn.disabled = true;
|
||||||
|
|
||||||
|
// RESET current Order ID if cart becomes empty?
|
||||||
|
// Actually, if we empty the cart, we might want to "cancel" the update?
|
||||||
|
// No, user can add items back. But if they leave it empty, we can't submit.
|
||||||
|
// If they start adding items, it's still the recalled order.
|
||||||
|
// What if they want to START NEW? They should reload or we should provide a Clear button.
|
||||||
|
// For now, let's assume they continue working on it.
|
||||||
|
// If they want new, they can refresh or we can add a "New Order" button later.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -564,6 +666,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const custId = selectedCustomerId.value;
|
const custId = selectedCustomerId.value;
|
||||||
|
|
||||||
const orderData = {
|
const orderData = {
|
||||||
|
order_id: currentOrderId, // Include ID if updating
|
||||||
table_number: (orderType === 'dine-in') ? currentTableId : null,
|
table_number: (orderType === 'dine-in') ? currentTableId : null,
|
||||||
order_type: orderType,
|
order_type: orderType,
|
||||||
customer_id: custId || null,
|
customer_id: custId || null,
|
||||||
@ -621,6 +724,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
cart = [];
|
cart = [];
|
||||||
cartDiscountInput.value = 0;
|
cartDiscountInput.value = 0;
|
||||||
|
currentOrderId = null; // Reset
|
||||||
isLoyaltyRedemption = false; // Reset
|
isLoyaltyRedemption = false; // Reset
|
||||||
updateCart();
|
updateCart();
|
||||||
if (clearCustomerBtn) clearCustomerBtn.click();
|
if (clearCustomerBtn) clearCustomerBtn.click();
|
||||||
|
|||||||
@ -116,6 +116,8 @@ function render_pagination_controls($pagination, $extra_params = []) {
|
|||||||
// Limit Selector
|
// Limit Selector
|
||||||
$limits = [20, 50, 100, -1];
|
$limits = [20, 50, 100, -1];
|
||||||
echo '<div class="d-flex justify-content-between align-items-center mb-3 bg-white p-2 rounded border">';
|
echo '<div class="d-flex justify-content-between align-items-center mb-3 bg-white p-2 rounded border">';
|
||||||
|
|
||||||
|
echo '<div class="d-flex align-items-center">';
|
||||||
echo '<form method="GET" class="d-flex align-items-center mb-0">';
|
echo '<form method="GET" class="d-flex align-items-center mb-0">';
|
||||||
// Preserve other GET params
|
// Preserve other GET params
|
||||||
foreach ($params as $key => $val) {
|
foreach ($params as $key => $val) {
|
||||||
@ -131,6 +133,10 @@ function render_pagination_controls($pagination, $extra_params = []) {
|
|||||||
echo '</select>';
|
echo '</select>';
|
||||||
echo '</form>';
|
echo '</form>';
|
||||||
|
|
||||||
|
// Total Count
|
||||||
|
echo '<span class="text-muted small ms-3 border-start ps-3">Total: <strong>' . $pagination['total_rows'] . '</strong></span>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
// Pagination Links
|
// Pagination Links
|
||||||
if ($total_pages > 1) {
|
if ($total_pages > 1) {
|
||||||
echo '<nav><ul class="pagination pagination-sm mb-0">';
|
echo '<nav><ul class="pagination pagination-sm mb-0">';
|
||||||
@ -176,8 +182,6 @@ function render_pagination_controls($pagination, $extra_params = []) {
|
|||||||
echo "<li class='page-item $next_disabled'><a class='page-link' href='$next_url'>»</a></li>";
|
echo "<li class='page-item $next_disabled'><a class='page-link' href='$next_url'>»</a></li>";
|
||||||
|
|
||||||
echo '</ul></nav>';
|
echo '</ul></nav>';
|
||||||
} else {
|
|
||||||
echo '<small class="text-muted">Total: ' . $pagination['total_rows'] . '</small>';
|
|
||||||
}
|
}
|
||||||
echo '</div>';
|
echo '</div>';
|
||||||
}
|
}
|
||||||
18
pos.php
18
pos.php
@ -72,6 +72,7 @@ foreach ($outlets as $o) {
|
|||||||
|
|
||||||
<div class="d-flex align-items-center gap-3">
|
<div class="d-flex align-items-center gap-3">
|
||||||
<a href="kitchen.php" class="btn btn-sm btn-outline-secondary">Kitchen View</a>
|
<a href="kitchen.php" class="btn btn-sm btn-outline-secondary">Kitchen View</a>
|
||||||
|
<button class="btn btn-sm btn-outline-danger" id="recall-bill-btn"><i class="bi bi-clock-history"></i> Recall Bill</button>
|
||||||
<a href="pos.php?order_type=dine-in" class="btn btn-sm btn-outline-secondary">Waiter View</a>
|
<a href="pos.php?order_type=dine-in" class="btn btn-sm btn-outline-secondary">Waiter View</a>
|
||||||
<a href="admin/orders.php" class="btn btn-sm btn-outline-secondary">Current Orders</a>
|
<a href="admin/orders.php" class="btn btn-sm btn-outline-secondary">Current Orders</a>
|
||||||
|
|
||||||
@ -264,6 +265,23 @@ foreach ($outlets as $o) {
|
|||||||
<!-- Toast Container -->
|
<!-- Toast Container -->
|
||||||
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x p-3" id="toast-container" style="z-index: 1060;"></div>
|
<div class="toast-container position-fixed bottom-0 start-50 translate-middle-x p-3" id="toast-container" style="z-index: 1060;"></div>
|
||||||
|
|
||||||
|
<!-- Recall Order Modal -->
|
||||||
|
<div class="modal fade" id="recallOrderModal" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title fw-bold">Recall Unpaid Bill</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body bg-light">
|
||||||
|
<div id="recall-orders-list" class="list-group">
|
||||||
|
<!-- Orders injected via JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Table Selection Modal -->
|
<!-- Table Selection Modal -->
|
||||||
<div class="modal fade" id="tableSelectionModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
<div class="modal fade" id="tableSelectionModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
|
||||||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user