adding qr ordering
This commit is contained in:
parent
4451897e8d
commit
7886680cd0
@ -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' : '' ?>>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
74
ads.php
@ -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
369
qorder.php
Normal 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
34
test_tables_logic.php
Normal 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
11
test_url.php
Normal 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";
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user