38682-vm/qorder.php
2026-02-26 17:57:35 +00:00

522 lines
23 KiB
PHP

<?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.table_number 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, c.name_ar as category_name_ar 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="<?= get_base_url() . htmlspecialchars($settings['favicon_url']) ?>?v=<?= time() ?>">
<?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&family=Noto+Sans+Arabic:wght@400;600;700&display=swap" rel="stylesheet">
<style>
:root {
--primary-font: 'Inter', sans-serif;
--arabic-font: 'Noto Sans Arabic', sans-serif;
}
body { font-family: var(--primary-font); background-color: #f8f9fa; padding-bottom: 80px; }
body.lang-ar { font-family: var(--arabic-font); direction: rtl; text-align: right; }
.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: 1.1rem; cursor: pointer; border: 1px solid transparent; }
.lang-ar .category-item { margin-right: 0; margin-left: 8px; }
.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; position: relative; }
.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; }
.lang-ar .badge-price { right: auto; left: 10px; }
.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; }
.lang-toggle { font-size: 0.8rem; font-weight: bold; cursor: pointer; padding: 4px 8px; border-radius: 4px; border: 1px solid #dee2e6; background: #f8f9fa; }
.name-en { display: block; }
.name-ar { display: none; }
.lang-ar .name-en { display: none; }
.lang-ar .name-ar { display: block; }
.both-names .name-en { display: block; }
.both-names .name-ar { display: block; font-size: 0.85em; opacity: 0.8; color: #0d6efd; margin-top: 2px; }
.modal-header .btn-close { margin: 0; }
.lang-ar .modal-header .btn-close { margin-right: auto; margin-left: 0; }
</style>
</head>
<body class="both-names">
<!-- 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="<?= get_base_url() . htmlspecialchars($settings['logo_url']) ?>?v=<?= time() ?>" alt="Logo" style="height: 32px;">
<?php endif; ?>
<span class="fw-bold"><?= htmlspecialchars($settings['company_name']) ?></span>
</div>
<div class="d-flex align-items-center gap-2">
<div class="lang-toggle" onclick="toggleLanguage()" id="lang-btn">AR</div>
<div class="badge bg-light text-dark border"><span data-t="table">Table</span> <?= htmlspecialchars($table_info['table_name']) ?></div>
</div>
</header>
<!-- Category Nav -->
<div class="category-nav shadow-sm">
<div class="category-item active" onclick="filterCategory('all', this)">
<span class="name-en">All</span>
<span class="name-ar">الكل</span>
</div>
<?php foreach ($categories as $cat): ?>
<div class="category-item" onclick="filterCategory(<?= $cat['id'] ?>, this)">
<span class="name-en"><?= htmlspecialchars($cat['name']) ?></span>
<?php if (!empty($cat['name_ar'])): ?>
<span class="name-ar"><?= htmlspecialchars($cat['name_ar']) ?></span>
<?php else: ?>
<span class="name-ar"><?= htmlspecialchars($cat['name']) ?></span>
<?php endif; ?>
</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']]);
$effective_price = get_product_price($product);
$is_promo = $effective_price < (float)$product['price'];
?>
<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'],
'name_ar' => $product['name_ar'] ?? $product['name'],
'price' => (float)$effective_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">
<?php if ($is_promo): ?>
<span class="text-danger fw-bold"><?= format_currency($effective_price) ?></span>
<small class="text-muted text-decoration-line-through ms-1" style="font-size: 0.7rem;"><?= format_currency((float)$product['price']) ?></small>
<?php else: ?>
<?= format_currency((float)$product['price']) ?>
<?php endif; ?>
</div>
<?php if ($is_promo): ?>
<div class="position-absolute top-0 start-0 m-2">
<span class="badge bg-warning text-dark fw-bold rounded-pill" style="font-size: 0.6rem;">SALE</span>
</div>
<?php endif; ?>
</div>
<div class="card-body p-2">
<h6 class="card-title mb-1 small fw-bold text-truncate">
<span class="name-en"><?= htmlspecialchars($product['name']) ?></span>
<span class="name-ar" dir="rtl"><?= htmlspecialchars($product['name_ar'] ?? $product['name']) ?></span>
</h6>
<p class="card-text small text-muted mb-0 text-truncate" style="font-size: 0.75rem;">
<span class="name-en"><?= htmlspecialchars($product['category_name']) ?></span>
<span class="name-ar" dir="rtl"><?= htmlspecialchars($product['category_name_ar'] ?? $product['category_name']) ?></span>
</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"><span id="cart-items-count">0</span> <span data-t="items">Items</span></div>
<div class="fw-bold fs-5 text-primary" id="cart-total-display"><?= format_currency(0) ?></div>
</div>
<button class="btn btn-primary px-4 fw-bold" onclick="showCart()">
<span data-t="view_cart">View Cart</span> <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" data-t="your_order">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 data-t="subtotal">Subtotal</span>
<span id="modal-subtotal"><?= format_currency(0) ?></span>
</div>
<div class="d-flex justify-content-between fw-bold fs-5">
<span data-t="total">Total</span>
<span id="modal-total"><?= format_currency(0) ?></span>
</div>
</div>
<div class="p-3">
<div class="mb-3">
<label class="form-label small text-muted" data-t="cust_name_label">Your Name (Optional)</label>
<input type="text" id="cust-name" class="form-control" data-t-placeholder="cust_name_placeholder" placeholder="To identify your order">
</div>
<button class="btn btn-primary btn-lg w-100 fw-bold" id="btn-place-order" onclick="placeOrder()">
<span data-t="place_order">Place Order</span> <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" data-t="select_option">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) ?>;
const COMPANY_SETTINGS = <?= json_encode($settings) ?>;
let cart = [];
let currentLang = localStorage.getItem('qorder_lang') || 'en';
const cartModal = new bootstrap.Modal(document.getElementById('cartModal'));
const variantModal = new bootstrap.Modal(document.getElementById('variantModal'));
const translations = {
en: {
table: 'Table',
items: 'Items',
view_cart: 'View Cart',
your_order: 'Your Order',
subtotal: 'Subtotal',
total: 'Total',
cust_name_label: 'Your Name (Optional)',
cust_name_placeholder: 'To identify your order',
place_order: 'Place Order',
select_option: 'Select Option',
added_to_cart: 'added to cart',
order_placed: 'Order Placed!',
order_success_msg: 'Your order has been sent to the kitchen. Thank you!',
placing_order: 'Placing Order...'
},
ar: {
table: 'طاولة',
items: 'أصناف',
view_cart: 'عرض السلة',
your_order: 'طلبك',
subtotal: 'المجموع الفرعي',
total: 'الإجمالي',
cust_name_label: 'اسمك (اختياري)',
cust_name_placeholder: 'لتحديد طلبك',
place_order: 'إتمام الطلب',
select_option: 'اختر الخيار',
added_to_cart: 'تم الإضافة إلى السلة',
order_placed: 'تم إرسال الطلب!',
order_success_msg: 'تم إرسال طلبك إلى المطبخ. شكراً لك!',
placing_order: 'جاري إرسال الطلب...'
}
};
function formatCurrency(amount) {
const symbol = COMPANY_SETTINGS.currency_symbol || '$';
const decimals = parseInt(COMPANY_SETTINGS.currency_decimals || 2);
const position = COMPANY_SETTINGS.currency_position || 'before';
const formatted = parseFloat(Math.abs(amount)).toFixed(decimals);
if (position === 'after') {
return formatted + ' ' + symbol;
} else {
return symbol + formatted;
}
}
function updateTranslations() {
document.querySelectorAll('[data-t]').forEach(el => {
const key = el.getAttribute('data-t');
if (translations[currentLang][key]) {
el.textContent = translations[currentLang][key];
}
});
document.querySelectorAll('[data-t-placeholder]').forEach(el => {
const key = el.getAttribute('data-t-placeholder');
if (translations[currentLang][key]) {
el.placeholder = translations[currentLang][key];
}
});
document.getElementById('lang-btn').textContent = currentLang === 'en' ? 'AR' : 'EN';
if (currentLang === 'ar') {
document.body.classList.add('lang-ar');
} else {
document.body.classList.remove('lang-ar');
}
// Update currency in displays
updateCartUI();
}
function toggleLanguage() {
currentLang = currentLang === 'en' ? 'ar' : 'en';
localStorage.setItem('qorder_lang', currentLang);
updateTranslations();
}
// Initialize translations
updateTranslations();
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, currentLang === 'ar' ? product.name_ar : product.name, product.price);
}
}
function showVariants(product) {
const variants = PRODUCT_VARIANTS[product.id] || [];
const container = document.getElementById('variant-list');
document.getElementById('variantTitle').textContent = currentLang === 'ar' ? product.name_ar : 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;
const vName = currentLang === 'ar' && v.name_ar ? v.name_ar : v.name;
const pName = currentLang === 'ar' ? product.name_ar : product.name;
btn.innerHTML = `
<div>
<div class="fw-bold">${vName}</div>
<div class="small text-muted">${adj > 0 ? '+' : ''}${formatCurrency(adj)}</div>
</div>
<div class="fw-bold text-primary">${formatCurrency(finalPrice)}</div>
`;
btn.onclick = () => {
addToCart(product.id, `${pName} (${vName})`, 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 + ' ' + translations[currentLang].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;
document.getElementById('cart-total-display').textContent = formatCurrency(total);
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">${formatCurrency(item.unit_price * item.quantity)}</div>
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="text-muted small">${formatCurrency(item.unit_price)} / 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 = formatCurrency(total);
document.getElementById('modal-total').textContent = formatCurrency(total);
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>${translations[currentLang].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_id: TABLE_ID, // Use table_id instead of table_number
order_type: 'dine-in',
customer_name: customerName,
items: cart,
total_amount: total,
payment_type_id: null
};
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: translations[currentLang].order_placed,
text: translations[currentLang].order_success_msg,
icon: 'success',
confirmButtonText: currentLang === 'ar' ? 'ممتاز' : '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 = `${translations[currentLang].place_order} <i class="bi bi-send-fill ms-1"></i>`;
});
}
function showToast(msg) {
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 2000,
timerProgressBar: true
});
Toast.fire({
icon: 'success',
title: msg
});
}
</script>
</body>
</html>