Autosave: 20260223-034341

This commit is contained in:
Flatlogic Bot 2026-02-23 03:43:41 +00:00
parent 6c8b522da6
commit e5617b6c15
8 changed files with 615 additions and 61 deletions

View File

@ -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>

View File

@ -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>

View File

@ -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
View 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()]);
}

View File

@ -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);
} }

View File

@ -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();

View File

@ -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'>&raquo;</a></li>"; echo "<li class='page-item $next_disabled'><a class='page-link' href='$next_url'>&raquo;</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
View File

@ -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">