adding qr ordering

This commit is contained in:
Flatlogic Bot 2026-02-23 08:16:43 +00:00
parent 4451897e8d
commit 7886680cd0
8 changed files with 582 additions and 19 deletions

View File

@ -23,6 +23,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title']);
$sort_order = (int)$_POST['sort_order'];
$is_active = isset($_POST['is_active']) ? 1 : 0;
$display_layout = $_POST['display_layout'] ?? 'both';
$image_path = $isEdit ? $ad['image_path'] : null;
if (isset($_FILES['image']) && $_FILES['image']['error'] === UPLOAD_ERR_OK) {
@ -56,13 +57,13 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (empty($message)) {
try {
if ($isEdit) {
$stmt = $pdo->prepare("UPDATE ads_images SET title = ?, sort_order = ?, is_active = ?, image_path = ? WHERE id = ?");
$stmt->execute([$title, $sort_order, $is_active, $image_path, $id]);
$stmt = $pdo->prepare("UPDATE ads_images SET title = ?, sort_order = ?, is_active = ?, display_layout = ?, image_path = ? WHERE id = ?");
$stmt->execute([$title, $sort_order, $is_active, $display_layout, $image_path, $id]);
header("Location: ads.php?success=updated");
exit;
} else {
$stmt = $pdo->prepare("INSERT INTO ads_images (title, sort_order, is_active, image_path) VALUES (?, ?, ?, ?)");
$stmt->execute([$title, $sort_order, $is_active, $image_path]);
$stmt = $pdo->prepare("INSERT INTO ads_images (title, sort_order, is_active, display_layout, image_path) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([$title, $sort_order, $is_active, $display_layout, $image_path]);
header("Location: ads.php?success=created");
exit;
}
@ -77,6 +78,7 @@ if (!$isEdit) {
'title' => $_POST['title'] ?? '',
'sort_order' => $_POST['sort_order'] ?? 0,
'is_active' => 1,
'display_layout' => 'both',
'image_path' => ''
];
}
@ -106,6 +108,22 @@ include 'includes/header.php';
<input type="number" name="sort_order" class="form-control" value="<?= htmlspecialchars($ad['sort_order'] ?? 0) ?>">
<div class="form-text">Lower numbers appear first in the slider.</div>
</div>
<div class="mb-3">
<label class="form-label d-block">Display Layout Preference</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="display_layout" id="layout_both" value="both" <?= ($ad['display_layout'] ?? 'both') === 'both' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary" for="layout_both">Both Layouts</label>
<input type="radio" class="btn-check" name="display_layout" id="layout_split" value="split" <?= ($ad['display_layout'] ?? 'both') === 'split' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary" for="layout_split">Split View Only</label>
<input type="radio" class="btn-check" name="display_layout" id="layout_fullscreen" value="fullscreen" <?= ($ad['display_layout'] ?? 'both') === 'fullscreen' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary" for="layout_fullscreen">Fullscreen Only</label>
</div>
<div class="form-text mt-2">Choose where this advertisement should be visible.</div>
</div>
<div class="mb-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" id="isActiveSwitch" <?= ($ad['is_active'] ?? 1) ? 'checked' : '' ?>>

View File

@ -9,6 +9,7 @@ $pdo->exec("CREATE TABLE IF NOT EXISTS ads_images (
title VARCHAR(255) DEFAULT NULL,
sort_order INT DEFAULT 0,
is_active TINYINT(1) DEFAULT 1,
display_layout ENUM('both', 'split', 'fullscreen') DEFAULT 'both',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)");
@ -53,7 +54,7 @@ include 'includes/header.php';
<i class="bi bi-info-circle-fill me-3 fs-4"></i>
<div>
These images will be displayed in a slider on the <strong><a href="../ads.php" target="_blank" class="alert-link">ads.php</a></strong> page.
You can upload up to 7 images for optimal performance.
You can now choose to show specific images in <strong>Split View</strong> or <strong>Fullscreen</strong> layout.
</div>
</div>
@ -69,6 +70,7 @@ include 'includes/header.php';
<th class="ps-4">Order</th>
<th style="width: 150px;">Preview</th>
<th>Title / Caption</th>
<th>Layout</th>
<th>Status</th>
<th class="text-end pe-4">Actions</th>
</tr>
@ -87,6 +89,20 @@ include 'includes/header.php';
<div class="fw-bold"><?= htmlspecialchars($ad['title'] ?: 'No title') ?></div>
<small class="text-muted"><?= htmlspecialchars($ad['image_path']) ?></small>
</td>
<td>
<?php
$layoutLabel = 'Both';
$layoutClass = 'bg-primary-subtle text-primary';
if ($ad['display_layout'] === 'split') {
$layoutLabel = 'Split Only';
$layoutClass = 'bg-info-subtle text-info';
} elseif ($ad['display_layout'] === 'fullscreen') {
$layoutLabel = 'Fullscreen Only';
$layoutClass = 'bg-warning-subtle text-warning';
}
?>
<span class="badge <?= $layoutClass ?> px-3"><?= $layoutLabel ?></span>
</td>
<td>
<?php if ($ad['is_active']): ?>
<span class="badge bg-success-subtle text-success px-3">Active</span>
@ -102,7 +118,7 @@ include 'includes/header.php';
<?php endforeach; ?>
<?php if (empty($ads)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">
<td colspan="6" class="text-center py-5 text-muted">
<i class="bi bi-images fs-1 d-block mb-3"></i>
No advertisement images found. Click "Add Image" to get started.
</td>

View File

@ -54,7 +54,7 @@ function getGroupToggleClass($pages) {
<link rel="icon" href="../<?= htmlspecialchars($faviconUrl) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">

View File

@ -28,6 +28,15 @@ $tables = $tables_pagination['data'];
$areas = $pdo->query("SELECT id, name FROM areas ORDER BY name ASC")->fetchAll();
include 'includes/header.php';
// Determine base URL for QR codes
$isHttps = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
|| ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https'
|| $_SERVER['SERVER_PORT'] == 443;
$protocol = $isHttps ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
$dir = dirname($_SERVER['PHP_SELF'], 2);
$baseUrl = $protocol . $host . ($dir === '/' ? '' : $dir) . '/qorder.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
@ -55,13 +64,20 @@ include 'includes/header.php';
</tr>
</thead>
<tbody>
<?php foreach ($tables as $table): ?>
<?php foreach ($tables as $table):
$qrUrl = $baseUrl . '?table_id=' . $table['id'];
?>
<tr>
<td class="ps-4 fw-medium">#<?= $table['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($table['name']) ?></td>
<td><span class="badge bg-secondary"><?= htmlspecialchars($table['area_name'] ?? 'N/A') ?></span></td>
<td><?= htmlspecialchars($table['capacity']) ?> pax</td>
<td>
<button class="btn btn-sm btn-dark me-1"
onclick="showQR('<?= htmlspecialchars($table['name'], ENT_QUOTES) ?>', '<?= $qrUrl ?>')"
title="View QR Code">
<i class="bi bi-qr-code me-1"></i> QR
</button>
<a href="table_edit.php?id=<?= $table['id'] ?>" class="btn btn-sm btn-outline-primary me-1" title="Edit"><i class="bi bi-pencil"></i></a>
<a href="?delete=<?= $table['id'] ?>" class="btn btn-sm btn-outline-danger" onclick="return confirm('Delete this table?')" title="Delete"><i class="bi bi-trash"></i></a>
</td>
@ -120,4 +136,51 @@ include 'includes/header.php';
</div>
</div>
<!-- QR Code Modal -->
<div class="modal fade" id="qrModal" tabindex="-1">
<div class="modal-dialog modal-sm modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="qrModalTitle">Table QR Code</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<div id="qr-container" class="mb-3">
<img id="qr-image" src="" alt="QR Code" class="img-fluid border p-2 bg-white" onerror="this.src='https://placehold.co/300x300?text=QR+Error'">
</div>
<div id="qr-url" class="small text-muted text-break mb-3"></div>
<button class="btn btn-sm btn-outline-primary" onclick="printQR()">
<i class="bi bi-printer"></i> Print QR
</button>
</div>
</div>
</div>
</div>
<script>
function showQR(tableName, url) {
document.getElementById('qrModalTitle').textContent = 'QR Code - ' + tableName;
document.getElementById('qr-image').src = 'https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=' + encodeURIComponent(url);
document.getElementById('qr-url').textContent = url;
const modal = new bootstrap.Modal(document.getElementById('qrModal'));
modal.show();
}
function printQR() {
const img = document.getElementById('qr-image').src;
const title = document.getElementById('qrModalTitle').textContent;
const win = window.open('', '_blank');
const html = '<html><head><title>Print QR</title><style>' +
'body { text-align: center; font-family: sans-serif; padding: 40px; }' +
'img { width: 300px; height: 300px; border: 1px solid #ccc; padding: 10px; }' +
'h1 { margin-bottom: 20px; }</style></head>' +
'<body onload="window.print(); window.close();">' +
'<h1>' + title + '</h1>' +
'<img src="' + img + '">' +
'<p>Scan to order</p></body></html>';
win.document.write(html);
win.document.close();
}
</script>
<?php include 'includes/footer.php'; ?>

74
ads.php
View File

@ -90,6 +90,10 @@ $companyName = $company['company_name'] ?? 'Foody';
object-fit: contain;
background-color: #000;
}
/* Layout filtering */
body.split-view .carousel-item.layout-fullscreen { display: none !important; }
body.fullscreen-promo .carousel-item.layout-split { display: none !important; }
/* Fullscreen Promo View */
.fullscreen-promo .serving-board {
@ -154,10 +158,23 @@ $companyName = $company['company_name'] ?? 'Foody';
<div class="promo-slider-container">
<?php if (!empty($ads)): ?>
<div id="promoCarousel" class="carousel slide" data-bs-ride="carousel" data-bs-interval="8000">
<div id="promoCarousel" class="carousel slide" data-bs-interval="8000">
<div class="carousel-inner">
<?php foreach ($ads as $index => $ad): ?>
<div class="carousel-item <?= $index === 0 ? 'active' : '' ?>">
<?php
$foundFirst = false;
foreach ($ads as $index => $ad):
$layout = $ad['display_layout'] ?? 'both';
$layoutClass = 'layout-' . $layout;
// Initial visibility in split-view
$isVisibleNow = ($layout === 'both' || $layout === 'split');
$activeClass = '';
if (!$foundFirst && $isVisibleNow) {
$activeClass = 'active';
$foundFirst = true;
}
?>
<div class="carousel-item <?= $activeClass ?> <?= $layoutClass ?>" data-layout="<?= $layout ?>">
<img src="<?= htmlspecialchars($ad['image_path']) ?>"
class="promo-image-element"
alt="Promotion">
@ -183,28 +200,61 @@ $companyName = $company['company_name'] ?? 'Foody';
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Carousel
let carouselInstance = null;
const el = document.querySelector('#promoCarousel');
if (el) {
new bootstrap.Carousel(el, {
function initCarousel() {
if (!el) return;
// Find first visible item and make it active
const isFullscreen = document.body.classList.contains('fullscreen-promo');
const items = el.querySelectorAll('.carousel-item');
let foundActive = false;
items.forEach(item => {
item.classList.remove('active');
const layout = item.getAttribute('data-layout');
const isVisible = (layout === 'both') ||
(isFullscreen && layout === 'fullscreen') ||
(!isFullscreen && layout === 'split');
if (isVisible && !foundActive) {
item.classList.add('active');
foundActive = true;
}
});
if (carouselInstance) {
carouselInstance.dispose();
carouselInstance = null;
}
carouselInstance = new bootstrap.Carousel(el, {
interval: 8000,
ride: 'carousel',
pause: false
});
carouselInstance.cycle();
}
initCarousel();
// Toggle View Functionality
const toggleBtn = document.getElementById('toggle-view');
const mainView = document.getElementById('main-view');
const toggleIcon = toggleBtn.querySelector('i');
toggleBtn.addEventListener('click', () => {
document.body.classList.toggle('fullscreen-promo');
if (document.body.classList.contains('fullscreen-promo')) {
const isFullscreenNow = document.body.classList.toggle('fullscreen-promo');
document.body.classList.toggle('split-view');
if (isFullscreenNow) {
toggleIcon.className = 'bi bi-fullscreen-exit';
} else {
toggleIcon.className = 'bi bi-arrows-fullscreen';
}
// Re-init carousel to handle hidden items
setTimeout(initCarousel, 100);
});
// Fetch Orders Logic
@ -217,6 +267,8 @@ document.addEventListener('DOMContentLoaded', function() {
const rContainer = document.getElementById('ready-orders');
const pContainer = document.getElementById('preparing-orders');
if (!rContainer || !pContainer) return;
const ready = orders.filter(o => o.status === 'ready');
const prep = orders.filter(o => o.status === 'preparing');
@ -246,4 +298,4 @@ document.addEventListener('DOMContentLoaded', function() {
});
</script>
</body>
</html>
</html>

369
qorder.php Normal file
View File

@ -0,0 +1,369 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/db/config.php';
require_once __DIR__ . '/includes/functions.php';
$pdo = db();
$settings = get_company_settings();
$table_id = isset($_GET['table_id']) ? (int)$_GET['table_id'] : 0;
if ($table_id <= 0) {
die("Invalid table ID. Please scan the QR code on your table.");
}
// Fetch table and outlet info
$stmt = $pdo->prepare("
SELECT t.id, t.name as table_name, a.outlet_id, o.name as outlet_name
FROM tables t
JOIN areas a ON t.area_id = a.id
JOIN outlets o ON a.outlet_id = o.id
WHERE t.id = ?
");
$stmt->execute([$table_id]);
$table_info = $stmt->fetch();
if (!$table_info) {
die("Table not found. Please contact staff.");
}
$outlet_id = (int)$table_info['outlet_id'];
$categories = $pdo->query("SELECT * FROM categories ORDER BY sort_order")->fetchAll();
$all_products = $pdo->query("SELECT p.*, c.name as category_name FROM products p JOIN categories c ON p.category_id = c.id")->fetchAll();
// Fetch variants
$variants_raw = $pdo->query("SELECT * FROM product_variants ORDER BY price_adjustment ASC")->fetchAll();
$variants_by_product = [];
foreach ($variants_raw as $v) {
$variants_by_product[$v['product_id']][] = $v;
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" />
<title><?= htmlspecialchars($settings['company_name']) ?> - Order Online</title>
<?php if (!empty($settings['favicon_url'])): ?>
<link rel="icon" href="<?= htmlspecialchars($settings['favicon_url']) ?>">
<?php endif; ?>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background-color: #f8f9fa; padding-bottom: 80px; }
.category-nav { overflow-x: auto; white-space: nowrap; background: #fff; padding: 10px; position: sticky; top: 0; z-index: 1020; border-bottom: 1px solid #eee; }
.category-item { display: inline-block; padding: 8px 16px; border-radius: 20px; background: #f1f3f5; margin-right: 8px; font-weight: 500; font-size: 0.9rem; cursor: pointer; border: 1px solid transparent; }
.category-item.active { background: #0d6efd; color: #fff; }
.product-card { border: none; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.05); transition: transform 0.2s; }
.product-card:active { transform: scale(0.98); }
.product-img { height: 140px; object-fit: cover; }
.cart-footer { position: fixed; bottom: 0; left: 0; right: 0; background: #fff; padding: 15px; border-top: 1px solid #eee; z-index: 1030; display: none; }
.badge-price { position: absolute; bottom: 10px; right: 10px; background: rgba(255,255,255,0.9); padding: 2px 8px; border-radius: 12px; font-weight: bold; font-size: 0.85rem; }
.quantity-controls { display: flex; align-items: center; gap: 10px; }
.quantity-btn { width: 32px; height: 32px; border-radius: 50%; border: 1px solid #dee2e6; background: #fff; display: flex; align-items: center; justify-content: center; }
</style>
</head>
<body>
<!-- Header -->
<header class="bg-white p-3 border-bottom d-flex align-items-center justify-content-between">
<div class="d-flex align-items-center gap-2">
<?php if (!empty($settings['logo_url'])): ?>
<img src="<?= htmlspecialchars($settings['logo_url']) ?>" alt="Logo" style="height: 32px;">
<?php endif; ?>
<span class="fw-bold"><?= htmlspecialchars($settings['company_name']) ?></span>
</div>
<div class="badge bg-light text-dark border">Table <?= htmlspecialchars($table_info['table_name']) ?></div>
</header>
<!-- Category Nav -->
<div class="category-nav shadow-sm">
<div class="category-item active" onclick="filterCategory('all', this)">All</div>
<?php foreach ($categories as $cat): ?>
<div class="category-item" onclick="filterCategory(<?= $cat['id'] ?>, this)"><?= htmlspecialchars($cat['name']) ?></div>
<?php endforeach; ?>
</div>
<div class="container py-3">
<div class="row g-3" id="products-container">
<?php foreach ($all_products as $product):
$has_variants = !empty($variants_by_product[$product['id']]);
?>
<div class="col-6 col-md-4 product-item" data-category-id="<?= $product['category_id'] ?>">
<div class="card h-100 product-card"
onclick="handleProductClick(<?= htmlspecialchars(json_encode([
'id' => $product['id'],
'name' => $product['name'],
'price' => (float)$product['price'],
'has_variants' => $has_variants
])) ?>)">
<div class="position-relative">
<img src="https://picsum.photos/seed/<?= $product['id'] ?>/400/300" class="card-img-top product-img" alt="<?= htmlspecialchars($product['name']) ?>">
<div class="badge-price text-primary"><?= number_format((float)$product['price'], 3) ?> OMR</div>
</div>
<div class="card-body p-2">
<h6 class="card-title mb-1 small fw-bold text-truncate"><?= htmlspecialchars($product['name']) ?></h6>
<p class="card-text small text-muted mb-0" style="font-size: 0.75rem;"><?= htmlspecialchars($product['category_name']) ?></p>
</div>
</div>
</div>
<?php endforeach; ?>
</div>
</div>
<!-- Cart Footer -->
<div class="cart-footer shadow-lg" id="cart-footer">
<div class="d-flex align-items-center justify-content-between">
<div>
<div class="small text-muted" id="cart-items-count">0 Items</div>
<div class="fw-bold fs-5 text-primary" id="cart-total-display">0.000 OMR</div>
</div>
<button class="btn btn-primary px-4 fw-bold" onclick="showCart()">
View Cart <i class="bi bi-cart-fill ms-1"></i>
</button>
</div>
</div>
<!-- Cart Modal -->
<div class="modal fade" id="cartModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-fullscreen-sm-down">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">Your Order</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body p-0">
<div id="cart-list" class="list-group list-group-flush">
<!-- Cart items here -->
</div>
<div class="p-3 bg-light border-top">
<div class="d-flex justify-content-between mb-2">
<span>Subtotal</span>
<span id="modal-subtotal">0.000 OMR</span>
</div>
<div class="d-flex justify-content-between fw-bold fs-5">
<span>Total</span>
<span id="modal-total">0.000 OMR</span>
</div>
</div>
<div class="p-3">
<div class="mb-3">
<label class="form-label small text-muted">Your Name (Optional)</label>
<input type="text" id="cust-name" class="form-control" placeholder="To identify your order">
</div>
<button class="btn btn-primary btn-lg w-100 fw-bold" id="btn-place-order" onclick="placeOrder()">
Place Order <i class="bi bi-send-fill ms-1"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Variant Selection Modal -->
<div class="modal fade" id="variantModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="variantTitle">Select Option</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="variant-list" class="list-group">
<!-- Variants injected by JS -->
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
const TABLE_ID = <?= $table_id ?>;
const OUTLET_ID = <?= $outlet_id ?>;
const PRODUCT_VARIANTS = <?= json_encode($variants_by_product) ?>;
let cart = [];
const cartModal = new bootstrap.Modal(document.getElementById('cartModal'));
const variantModal = new bootstrap.Modal(document.getElementById('variantModal'));
function filterCategory(catId, el) {
document.querySelectorAll('.category-item').forEach(i => i.classList.remove('active'));
el.classList.add('active');
document.querySelectorAll('.product-item').forEach(item => {
if (catId === 'all' || item.dataset.categoryId == catId) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
}
function handleProductClick(product) {
if (product.has_variants) {
showVariants(product);
} else {
addToCart(product.id, product.name, product.price);
}
}
function showVariants(product) {
const variants = PRODUCT_VARIANTS[product.id] || [];
const container = document.getElementById('variant-list');
document.getElementById('variantTitle').textContent = product.name;
container.innerHTML = '';
variants.forEach(v => {
const btn = document.createElement('button');
btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center py-3';
const adj = parseFloat(v.price_adjustment);
const finalPrice = product.price + adj;
btn.innerHTML = `
<div>
<div class="fw-bold">${v.name}</div>
<div class="small text-muted">${adj > 0 ? '+' : ''}${adj.toFixed(3)} OMR</div>
</div>
<div class="fw-bold text-primary">${finalPrice.toFixed(3)} OMR</div>
`;
btn.onclick = () => {
addToCart(product.id, `${product.name} (${v.name})`, finalPrice, v.id);
variantModal.hide();
};
container.appendChild(btn);
});
variantModal.show();
}
function addToCart(pid, name, price, vid = null) {
const existing = cart.find(i => i.product_id === pid && i.variant_id === vid);
if (existing) {
existing.quantity++;
} else {
cart.push({ product_id: pid, name, unit_price: price, variant_id: vid, quantity: 1 });
}
updateCartUI();
showToast(name + ' added to cart');
}
function updateCartUI() {
const total = cart.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
const count = cart.reduce((sum, item) => sum + item.quantity, 0);
document.getElementById('cart-items-count').textContent = count + ' Items';
document.getElementById('cart-total-display').textContent = total.toFixed(3) + ' OMR';
const footer = document.getElementById('cart-footer');
if (count > 0) {
footer.style.display = 'block';
} else {
footer.style.display = 'none';
}
}
function showCart() {
const list = document.getElementById('cart-list');
list.innerHTML = '';
cart.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'list-group-item p-3';
div.innerHTML = `
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="fw-bold text-truncate me-2">${item.name}</div>
<div class="fw-bold">${(item.unit_price * item.quantity).toFixed(3)}</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">${item.unit_price.toFixed(3)} / unit</div>
<div class="quantity-controls">
<button class="quantity-btn" onclick="updateQty(${index}, -1)"><i class="bi bi-dash"></i></button>
<span class="fw-bold">${item.quantity}</span>
<button class="quantity-btn" onclick="updateQty(${index}, 1)"><i class="bi bi-plus"></i></button>
</div>
</div>
`;
list.appendChild(div);
});
const total = cart.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
document.getElementById('modal-subtotal').textContent = total.toFixed(3) + ' OMR';
document.getElementById('modal-total').textContent = total.toFixed(3) + ' OMR';
cartModal.show();
}
function updateQty(index, delta) {
cart[index].quantity += delta;
if (cart[index].quantity <= 0) {
cart.splice(index, 1);
}
if (cart.length === 0) {
cartModal.hide();
} else {
showCart();
}
updateCartUI();
}
function placeOrder() {
if (cart.length === 0) return;
const btn = document.getElementById('btn-place-order');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Placing Order...';
const total = cart.reduce((sum, item) => sum + (item.unit_price * item.quantity), 0);
const customerName = document.getElementById('cust-name').value;
const payload = {
outlet_id: OUTLET_ID,
table_number: TABLE_ID, // api/order.php expects table ID in table_number
order_type: 'dine-in',
customer_name: customerName,
items: cart,
total_amount: total,
payment_type_id: null // Unpaid
};
fetch('api/order.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
})
.then(res => res.json())
.then(data => {
if (data.success) {
Swal.fire({
title: 'Order Placed!',
text: 'Your order has been sent to the kitchen. Thank you!',
icon: 'success',
confirmButtonText: 'Great!'
}).then(() => {
cart = [];
updateCartUI();
cartModal.hide();
document.getElementById('cust-name').value = '';
});
} else {
throw new Error(data.error || 'Failed to place order');
}
})
.catch(err => {
Swal.fire('Error', err.message, 'error');
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = 'Place Order <i class="bi bi-send-fill ms-1"></i>';
});
}
function showToast(msg) {
// Simple alert for now, or use a toast
console.log(msg);
}
</script>
</body>
</html>

34
test_tables_logic.php Normal file
View File

@ -0,0 +1,34 @@
<?php
require_once 'db/config.php';
require_once 'includes/functions.php';
$pdo = db();
// Simulate variables
$_SERVER['HTTPS'] = 'off';
$_SERVER['SERVER_PORT'] = 80;
$_SERVER['HTTP_HOST'] = 'localhost';
$_SERVER['PHP_SELF'] = '/admin/tables.php';
// Fetch tables with area names
$query = "SELECT tables.*, areas.name as area_name
FROM tables
LEFT JOIN areas ON tables.area_id = areas.id
ORDER BY tables.id DESC";
$tables_pagination = paginate_query($pdo, $query);
$tables = $tables_pagination['data'];
// Determine base URL for QR codes
$protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$host = $_SERVER['HTTP_HOST'];
$dir = dirname($_SERVER['PHP_SELF'], 2);
$baseUrl = $protocol . $host . ($dir === '/' ? '' : $dir) . '/qorder.php';
echo "Base URL: " . $baseUrl . "\n";
echo "Tables found: " . count($tables) . "\n";
foreach ($tables as $table) {
$qrUrl = $baseUrl . '?table_id=' . $table['id'];
echo "Table: " . $table['name'] . " - QR URL: " . $qrUrl . "\n";
}

11
test_url.php Normal file
View File

@ -0,0 +1,11 @@
<?php
$protocol = "http://";
$host = "localhost";
$_SERVER['PHP_SELF'] = "/admin/tables.php";
try {
$baseUrl = $protocol . $host . rtrim(dirname($_SERVER['PHP_SELF'], 2), '/\') . '/qorder.php';
echo "Base URL: " . $baseUrl . "\n";
} catch (Throwable $e) {
echo "Error: " . $e->getMessage() . "\n";
}