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() ?>">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
body { font-family: 'Inter', sans-serif; background-color: #f8f9fa; }
.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; }
.sidebar-header { padding: 1rem 1.5rem; border-bottom: 1px solid #eee; position: sticky; top: 0; background: #fff; z-index: 10; }
.sidebar .nav-link { color: #555; padding: 0.6rem 1.5rem; font-weight: 500; font-size: 0.95rem; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { color: #FF6B6B; background: #FFF0F0; border-right: 3px solid #FF6B6B; }
.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; }
/* Base styles using CSS variables for theming */
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg-body);
color: var(--text-primary);
}
.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; }
@media (max-width: 768px) {
.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; }
.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); }
.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>
<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>
<body>
<!-- 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">
<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): ?>
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="height: 30px;">
<?php else: ?>
<?= htmlspecialchars($companyName) ?><span class="text-primary">Admin</span>
<?= htmlspecialchars($companyName) ?><span style="color: var(--accent-color);">Admin</span>
<?php endif; ?>
</a>
<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): ?>
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="max-height: 50px; max-width: 100%;">
<?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; ?>
</a>
</div>
<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 class="sidebar-content">
@ -97,7 +198,7 @@ function isActive($page) {
</li>
</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">
<li class="nav-item">
<a class="nav-link" href="../pos.php" target="_blank">
@ -116,7 +217,7 @@ function isActive($page) {
</li>
</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">
<li class="nav-item">
<a class="nav-link <?= isActive('products.php') ?>" href="products.php">
@ -130,7 +231,7 @@ function isActive($page) {
</li>
</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">
<li class="nav-item">
<a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
@ -149,7 +250,7 @@ function isActive($page) {
</li>
</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">
<li class="nav-item">
<a class="nav-link <?= isActive('customers.php') ?>" href="customers.php">
@ -168,7 +269,7 @@ function isActive($page) {
</li>
</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">
<li class="nav-item">
<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
</a>
</li>
<li class="nav-item border-top mt-2 pt-2">
<a class="nav-link text-muted" href="../index.php" target="_blank">
<i class="bi bi-box-arrow-up-right me-2"></i> View Site
@ -194,4 +296,39 @@ 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) : '';
// 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
FROM orders o
LEFT JOIN outlets ot ON o.outlet_id = ot.id
LEFT JOIN payment_types pt ON o.payment_type_id = pt.id
$where_clause
ORDER BY o.created_at DESC";
@ -128,6 +130,7 @@ include 'includes/header.php';
<th>Source</th>
<th>Items</th>
<th>Total</th>
<th>Payment</th>
<th>Status</th>
<th>Time</th>
<th>Action</th>
@ -136,7 +139,7 @@ include 'includes/header.php';
<tbody>
<?php foreach ($orders as $order): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $order['id'] ?></td>
<td class="ps-4">#<?= $order['id'] ?></td>
<td>
<span class="badge bg-white text-dark border">
<i class="bi bi-shop me-1"></i>
@ -145,7 +148,7 @@ include 'includes/header.php';
</td>
<td>
<?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'])): ?>
<small class="text-muted"><i class="bi bi-telephone me-1"></i><?= htmlspecialchars($order['customer_phone']) ?></small>
<?php endif; ?>
@ -173,7 +176,22 @@ include 'includes/header.php';
<?php endif; ?>
</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>
<span class="badge rounded-pill status-<?= $order['status'] ?>">
<?= ucfirst($order['status']) ?>
@ -187,10 +205,6 @@ include 'includes/header.php';
<form method="POST" class="d-flex gap-2">
<input type="hidden" name="order_id" value="<?= $order['id'] ?>">
<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'): ?>
<button type="submit" name="status" value="preparing" class="btn btn-sm btn-primary">
@ -216,7 +230,7 @@ include 'includes/header.php';
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<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>
No active orders found matching your criteria.
</td>

View File

@ -125,10 +125,51 @@ try {
$discount = isset($data['discount']) ? floatval($data['discount']) : 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')");
$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();
// Check for Existing Order ID (Update Mode)
$order_id = isset($data['order_id']) ? intval($data['order_id']) : null;
$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 (?, ?, ?, ?, ?)");
// Pre-prepare statements for name fetching
@ -247,4 +288,4 @@ You've earned *{points_earned} points* with this order.
}
error_log("Order Error: " . $e->getMessage());
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}

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 {
/* Default Theme (Light/Minimal) */
--primary-color: #1A1A1A;
--accent-color: #E63946;
--secondary-bg: #F5F5F5;
--bg-body: #F5F5F5;
--bg-card: #FFFFFF;
--bg-sidebar: #FFFFFF;
--text-primary: #1A1A1A;
--text-secondary: #666666;
--white: #FFFFFF;
--text-heading: #999999;
--border-color: #EEEEEE;
--border-radius: 8px;
--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 {
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);
line-height: 1.5;
margin: 0;
padding: 0;
transition: background-color 0.3s, color 0.3s;
}
.navbar {
background-color: var(--white);
border-bottom: 1px solid #EAEAEA;
background-color: var(--bg-card);
border-bottom: 1px solid var(--border-color);
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 {
font-weight: 700;
font-size: 1.5rem;
@ -38,15 +142,16 @@ body {
font-size: 1.25rem;
border-bottom: 2px solid var(--accent-color);
display: inline-block;
color: var(--text-primary);
}
.product-card {
background: var(--white);
background: var(--bg-card);
border-radius: var(--border-radius);
overflow: hidden;
box-shadow: var(--shadow);
transition: transform 0.2s ease;
border: 1px solid #EAEAEA;
border: 1px solid var(--border-color);
height: 100%;
}
@ -57,7 +162,7 @@ body {
.product-image {
width: 100%;
height: 180px;
background-color: #EEEEEE;
background-color: var(--border-color);
object-fit: cover;
}
@ -69,6 +174,7 @@ body {
font-weight: 600;
font-size: 1.1rem;
margin-bottom: 0.5rem;
color: var(--text-primary);
}
.product-desc {
@ -86,7 +192,7 @@ body {
.btn-add {
background-color: var(--primary-color);
color: var(--white);
color: #FFFFFF;
border-radius: var(--border-radius);
padding: 0.5rem 1rem;
font-weight: 600;
@ -94,15 +200,15 @@ body {
}
.btn-add:hover {
background-color: #333333;
opacity: 0.9;
}
.cart-sidebar {
background: var(--white);
background: var(--bg-card);
height: 100vh;
position: sticky;
top: 0;
border-left: 1px solid #EAEAEA;
border-left: 1px solid var(--border-color);
padding: 2rem;
display: flex;
flex-direction: column;
@ -113,17 +219,19 @@ body {
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #EEEEEE;
border-bottom: 1px solid var(--border-color);
}
.cart-item-name {
font-weight: 600;
font-size: 0.9rem;
color: var(--text-primary);
}
.cart-item-price {
font-weight: 700;
font-size: 0.9rem;
color: var(--text-primary);
}
.cart-total {
@ -134,6 +242,7 @@ body {
font-size: 1.25rem;
display: flex;
justify-content: space-between;
color: var(--text-primary);
}
.order-badge {
@ -147,7 +256,7 @@ body {
/* Admin Styles */
.admin-table-container {
background: var(--white);
background: var(--bg-card);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
padding: 1.5rem;
@ -166,10 +275,10 @@ body {
/* Friendly Table & UI Enhancements */
.filter-bar {
background: #fff;
background: var(--bg-card);
border-radius: 12px;
padding: 1rem 1.5rem;
box-shadow: 0 2px 12px rgba(0,0,0,0.03);
box-shadow: var(--shadow);
margin-bottom: 1.5rem;
}
@ -183,13 +292,13 @@ body {
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
color: #999;
color: var(--text-heading);
letter-spacing: 0.8px;
padding: 0 1.5rem 0.5rem 1.5rem;
}
.friendly-table tbody tr {
background: #fff;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
background: var(--bg-card);
box-shadow: var(--shadow);
border-radius: 12px;
transition: all 0.2s ease;
}
@ -201,7 +310,8 @@ body {
border: none;
padding: 1rem 1.5rem;
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 {
border-top-left-radius: 12px;
@ -219,8 +329,8 @@ body {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.badge-soft {
background-color: #f3f4f6;
color: #4b5563;
background-color: var(--border-color);
color: var(--text-secondary);
font-weight: 500;
padding: 0.4em 0.8em;
border-radius: 6px;
@ -233,13 +343,13 @@ body {
justify-content: center;
border-radius: 8px;
transition: all 0.2s;
background: #f8f9fa;
color: #6c757d;
background: var(--border-color);
color: var(--text-secondary);
border: none;
}
.btn-icon-soft:hover {
background: #e9ecef;
color: #495057;
background: var(--accent-color);
color: #FFFFFF;
}
.btn-icon-soft.delete:hover {
background: #fee2e2;
@ -252,5 +362,35 @@ body {
.text-price {
font-family: 'Inter', sans-serif;
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 currentOrderId = null; // Track order ID for updates
const cartItemsContainer = document.getElementById('cart-items');
const cartTotalPrice = document.getElementById('cart-total-price');
const cartSubtotal = document.getElementById('cart-subtotal');
@ -21,6 +22,12 @@ document.addEventListener('DOMContentLoaded', () => {
// Updated Button References
const quickOrderBtn = document.getElementById('quick-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
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 ---
let searchTimeout;
if (customerSearchInput) {
@ -418,6 +512,14 @@ document.addEventListener('DOMContentLoaded', () => {
cartTotalPrice.innerText = formatCurrency(0);
if (quickOrderBtn) quickOrderBtn.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;
}
@ -564,6 +666,7 @@ document.addEventListener('DOMContentLoaded', () => {
const custId = selectedCustomerId.value;
const orderData = {
order_id: currentOrderId, // Include ID if updating
table_number: (orderType === 'dine-in') ? currentTableId : null,
order_type: orderType,
customer_id: custId || null,
@ -621,6 +724,7 @@ document.addEventListener('DOMContentLoaded', () => {
cart = [];
cartDiscountInput.value = 0;
currentOrderId = null; // Reset
isLoyaltyRedemption = false; // Reset
updateCart();
if (clearCustomerBtn) clearCustomerBtn.click();

View File

@ -116,6 +116,8 @@ function render_pagination_controls($pagination, $extra_params = []) {
// Limit Selector
$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 align-items-center">';
echo '<form method="GET" class="d-flex align-items-center mb-0">';
// Preserve other GET params
foreach ($params as $key => $val) {
@ -131,6 +133,10 @@ function render_pagination_controls($pagination, $extra_params = []) {
echo '</select>';
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
if ($total_pages > 1) {
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 '</ul></nav>';
} else {
echo '<small class="text-muted">Total: ' . $pagination['total_rows'] . '</small>';
}
echo '</div>';
}

18
pos.php
View File

@ -72,6 +72,7 @@ foreach ($outlets as $o) {
<div class="d-flex align-items-center gap-3">
<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="admin/orders.php" class="btn btn-sm btn-outline-secondary">Current Orders</a>
@ -264,6 +265,23 @@ foreach ($outlets as $o) {
<!-- 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>
<!-- 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 -->
<div class="modal fade" id="tableSelectionModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-lg modal-dialog-centered">