first draft

This commit is contained in:
Flatlogic Bot 2026-02-23 02:38:46 +00:00
parent 5d1e95ef4f
commit a238062edb
7 changed files with 311 additions and 155 deletions

View File

@ -37,14 +37,17 @@ function isActive($page) {
<link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>"> <link rel="stylesheet" href="../assets/css/custom.css?v=<?= time() ?>">
<style> <style>
body { font-family: 'Inter', sans-serif; background-color: #f8f9fa; } 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: 1rem; z-index: 1000; } .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 .nav-link { color: #555; padding: 0.75rem 1.5rem; font-weight: 500; } .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 .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; }
.main-content { margin-left: 250px; padding: 2rem; } .main-content { margin-left: 250px; padding: 2rem; }
@media (max-width: 768px) { @media (max-width: 768px) {
.sidebar { transform: translateX(-100%); transition: transform 0.3s ease; } .sidebar { transform: translateX(-100%); transition: transform 0.3s ease; top: 56px; height: calc(100vh - 56px); }
.sidebar.show { transform: translateX(0); } .sidebar.show { transform: translateX(0); }
.main-content { margin-left: 0; } .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; box-shadow: 0 2px 12px rgba(0,0,0,0.04); transition: transform 0.2s; }
.stat-card:hover { transform: translateY(-3px); } .stat-card:hover { transform: translateY(-3px); }
@ -71,7 +74,7 @@ function isActive($page) {
<!-- Sidebar --> <!-- Sidebar -->
<div class="offcanvas-md offcanvas-start sidebar" tabindex="-1" id="sidebarMenu"> <div class="offcanvas-md offcanvas-start sidebar" tabindex="-1" id="sidebarMenu">
<div class="d-flex align-items-center justify-content-center px-4 pb-3 border-bottom d-none d-md-flex" style="min-height: 80px;"> <div class="d-flex align-items-center justify-content-center sidebar-header d-none d-md-flex">
<a href="index.php" class="text-decoration-none"> <a href="index.php" class="text-decoration-none">
<?php if ($logoUrl): ?> <?php if ($logoUrl): ?>
<img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="max-height: 50px; max-width: 100%;"> <img src="../<?= htmlspecialchars($logoUrl) ?>" alt="Logo" style="max-height: 50px; max-width: 100%;">
@ -84,83 +87,110 @@ function isActive($page) {
<h5 class="fw-bold">Menu</h5> <h5 class="fw-bold">Menu</h5>
</div> </div>
<ul class="nav flex-column mt-2"> <div class="sidebar-content">
<li class="nav-item"> <ul class="nav flex-column">
<a class="nav-link <?= isActive('index.php') ?>" href="index.php"> <li class="nav-item">
<i class="bi bi-grid me-2"></i> Dashboard <a class="nav-link <?= isActive('index.php') ?>" href="index.php">
</a> <i class="bi bi-grid me-2"></i> Dashboard
</li> </a>
<li class="nav-item"> </li>
<a class="nav-link" href="../pos.php" target="_blank"> </ul>
<i class="bi bi-display me-2"></i> POS Terminal
</a> <h6 class="sidebar-heading">POS & Operations</h6>
</li> <ul class="nav flex-column">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= isActive('orders.php') ?>" href="orders.php"> <a class="nav-link" href="../pos.php" target="_blank">
<i class="bi bi-receipt me-2"></i> Orders (POS) <i class="bi bi-display me-2"></i> POS Terminal
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= isActive('products.php') ?>" href="products.php"> <a class="nav-link <?= isActive('orders.php') ?>" href="orders.php">
<i class="bi bi-box-seam me-2"></i> Products <i class="bi bi-receipt me-2"></i> Orders (POS)
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link <?= isActive('categories.php') ?>" href="categories.php"> <a class="nav-link" href="../kitchen.php" target="_blank">
<i class="bi bi-tags me-2"></i> Categories <i class="bi bi-fire me-2"></i> Kitchen View
</a> </a>
</li> </li>
<li class="nav-item"> </ul>
<a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
<i class="bi bi-shop me-2"></i> Outlets <h6 class="sidebar-heading">Menu Management</h6>
</a> <ul class="nav flex-column">
</li> <li class="nav-item">
<li class="nav-item"> <a class="nav-link <?= isActive('products.php') ?>" href="products.php">
<a class="nav-link <?= isActive('areas.php') ?>" href="areas.php"> <i class="bi bi-box-seam me-2"></i> Products
<i class="bi bi-geo-alt me-2"></i> Areas </a>
</a> </li>
</li> <li class="nav-item">
<li class="nav-item"> <a class="nav-link <?= isActive('categories.php') ?>" href="categories.php">
<a class="nav-link <?= isActive('tables.php') ?>" href="tables.php"> <i class="bi bi-tags me-2"></i> Categories
<i class="bi bi-ui-checks-grid me-2"></i> Tables </a>
</a> </li>
</li> </ul>
<li class="nav-item">
<a class="nav-link <?= isActive('customers.php') ?>" href="customers.php"> <h6 class="sidebar-heading">Restaurant Setup</h6>
<i class="bi bi-people-fill me-2"></i> Customers <ul class="nav flex-column">
</a> <li class="nav-item">
</li> <a class="nav-link <?= isActive('outlets.php') ?>" href="outlets.php">
<li class="nav-item"> <i class="bi bi-shop me-2"></i> Outlets
<a class="nav-link <?= isActive('suppliers.php') ?>" href="suppliers.php"> </a>
<i class="bi bi-truck me-2"></i> Suppliers </li>
</a> <li class="nav-item">
</li> <a class="nav-link <?= isActive('areas.php') ?>" href="areas.php">
<li class="nav-item"> <i class="bi bi-geo-alt me-2"></i> Areas
<a class="nav-link <?= isActive('loyalty.php') ?>" href="loyalty.php"> </a>
<i class="bi bi-award me-2"></i> Loyalty </li>
</a> <li class="nav-item">
</li> <a class="nav-link <?= isActive('tables.php') ?>" href="tables.php">
<li class="nav-item"> <i class="bi bi-ui-checks-grid me-2"></i> Tables
<a class="nav-link <?= isActive('payment_types.php') ?>" href="payment_types.php"> </a>
<i class="bi bi-credit-card me-2"></i> Payment Types </li>
</a> </ul>
</li>
<li class="nav-item"> <h6 class="sidebar-heading">People & Partners</h6>
<a class="nav-link <?= isActive('integrations.php') ?>" href="integrations.php"> <ul class="nav flex-column">
<i class="bi bi-plugin me-2"></i> Integrations <li class="nav-item">
</a> <a class="nav-link <?= isActive('customers.php') ?>" href="customers.php">
</li> <i class="bi bi-people-fill me-2"></i> Customers
<li class="nav-item"> </a>
<a class="nav-link <?= isActive('company.php') ?>" href="company.php"> </li>
<i class="bi bi-building me-2"></i> Company <li class="nav-item">
</a> <a class="nav-link <?= isActive('suppliers.php') ?>" href="suppliers.php">
</li> <i class="bi bi-truck me-2"></i> Suppliers
<li class="nav-item mt-4 border-top pt-2"> </a>
<a class="nav-link text-muted" href="../index.php" target="_blank"> </li>
<i class="bi bi-box-arrow-up-right me-2"></i> View Site <li class="nav-item">
</a> <a class="nav-link <?= isActive('loyalty.php') ?>" href="loyalty.php">
</li> <i class="bi bi-award me-2"></i> Loyalty
</ul> </a>
</li>
</ul>
<h6 class="sidebar-heading">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">
<i class="bi bi-credit-card me-2"></i> Payment Types
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('integrations.php') ?>" href="integrations.php">
<i class="bi bi-plugin me-2"></i> Integrations
</a>
</li>
<li class="nav-item">
<a class="nav-link <?= isActive('company.php') ?>" href="company.php">
<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
</a>
</li>
</ul>
</div>
</div> </div>
<div class="main-content pt-5 pt-md-4"> <div class="main-content pt-5 pt-md-4">

View File

@ -28,7 +28,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Wablas // Wablas
if ($provider === 'wablas') { if ($provider === 'wablas') {
$keys = ['domain', 'token', 'secret_key']; $keys = ['domain', 'token', 'secret_key', 'order_template'];
foreach ($keys as $k) { foreach ($keys as $k) {
$val = $_POST[$k] ?? ''; $val = $_POST[$k] ?? '';
$stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)"); $stmt = $pdo->prepare("INSERT INTO integration_settings (provider, setting_key, setting_value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
@ -72,6 +72,22 @@ $thawaniSec = getSetting($allSettings, 'thawani', 'secret_key');
$wablasDom = getSetting($allSettings, 'wablas', 'domain'); $wablasDom = getSetting($allSettings, 'wablas', 'domain');
$wablasTok = getSetting($allSettings, 'wablas', 'token'); $wablasTok = getSetting($allSettings, 'wablas', 'token');
$wablasSecKey = getSetting($allSettings, 'wablas', 'secret_key'); $wablasSecKey = getSetting($allSettings, 'wablas', 'secret_key');
$wablasTemplate = getSetting($allSettings, 'wablas', 'order_template');
// Default template if empty
if (empty($wablasTemplate)) {
$wablasTemplate = "Dear *{customer_name}*,
Thank you for dining with *{company_name}*! 🍽️
*Order Details:*
{order_details}
Total: *{total_amount}* OMR
You've earned *{points_earned} points* with this order.
💰 *Current Balance: {new_balance} points*";
}
require_once __DIR__ . '/includes/header.php'; require_once __DIR__ . '/includes/header.php';
?> ?>
@ -148,6 +164,17 @@ require_once __DIR__ . '/includes/header.php';
<input type="password" class="form-control" name="secret_key" value="<?= htmlspecialchars($wablasSecKey) ?>"> <input type="password" class="form-control" name="secret_key" value="<?= htmlspecialchars($wablasSecKey) ?>">
</div> </div>
<div class="mb-3">
<label class="form-label">Order Notification Template</label>
<textarea class="form-control font-monospace" name="order_template" rows="8"><?= htmlspecialchars($wablasTemplate) ?></textarea>
<div class="form-text mt-2">
<strong>Available Variables:</strong><br>
<code>{customer_name}</code>, <code>{company_name}</code>, <code>{order_id}</code>,
<code>{order_details}</code> (list of items), <code>{total_amount}</code>,
<code>{points_earned}</code>, <code>{points_redeemed}</code>, <code>{new_balance}</code>.
</div>
</div>
<div class="mb-3 border-top pt-3"> <div class="mb-3 border-top pt-3">
<label class="form-label text-muted small">Test Configuration</label> <label class="form-label text-muted small">Test Configuration</label>
<div class="input-group"> <div class="input-group">

12
api/last_msg.txt Normal file
View File

@ -0,0 +1,12 @@
Dear *Moosa Ali Al-Abri*,
Thank you for dining with us! 🍽️
*Order Details:*
2 x Signature Burger
1 x Veggie Delight
Total: *37.480* OMR
You've earned *10 points* with this order.
💰 *Current Balance: 20 points*
You need *50 more points* to unlock a free meal.

View File

@ -122,6 +122,13 @@ try {
$order_id = $pdo->lastInsertId(); $order_id = $pdo->lastInsertId();
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)"); $item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");
// Pre-prepare statements for name fetching
$prodNameStmt = $pdo->prepare("SELECT name FROM products WHERE id = ?");
$varNameStmt = $pdo->prepare("SELECT name FROM product_variants WHERE id = ?");
$order_items_list = []; // To store item details for notification
if (!empty($data['items']) && is_array($data['items'])) { if (!empty($data['items']) && is_array($data['items'])) {
foreach ($data['items'] as $item) { foreach ($data['items'] as $item) {
$pid = $item['product_id'] ?? ($item['id'] ?? null); $pid = $item['product_id'] ?? ($item['id'] ?? null);
@ -131,6 +138,20 @@ try {
if ($pid) { if ($pid) {
$item_stmt->execute([$order_id, $pid, $vid, $qty, $price]); $item_stmt->execute([$order_id, $pid, $vid, $qty, $price]);
// Fetch names for notification
$prodNameStmt->execute([$pid]);
$pRow = $prodNameStmt->fetch(PDO::FETCH_ASSOC);
$pName = $pRow ? $pRow['name'] : 'Item';
if ($vid) {
$varNameStmt->execute([$vid]);
$vRow = $varNameStmt->fetch(PDO::FETCH_ASSOC);
if ($vRow) {
$pName .= " (" . $vRow['name'] . ")";
}
}
$order_items_list[] = "$qty x $pName";
} }
} }
} }
@ -147,28 +168,58 @@ try {
// Determine message content // Determine message content
$wablas = new WablasService($pdo); $wablas = new WablasService($pdo);
$msg = "Dear *$customer_name*,
"; // Fetch Company Name
$msg .= "Thank you for dining with us! 🍽️ require_once __DIR__ . '/../includes/functions.php';
"; $companySettings = get_company_settings();
$msg .= "You've earned *$points_awarded points* with this order. $company_name = $companySettings['company_name'] ?? 'Flatlogic POS';
";
if ($points_deducted > 0) { // Fetch Template
$msg .= "You also redeemed *$points_deducted points* for a free meal! 🎁 $stmt = $pdo->prepare("SELECT setting_value FROM integration_settings WHERE provider = 'wablas' AND setting_key = 'order_template'");
"; $stmt->execute();
$template = $stmt->fetchColumn();
// Default Template if not set
if (!$template) {
$template = "Dear *{customer_name}*,
Thank you for dining with *{company_name}*! 🍽️
*Order Details:*
{order_details}
Total: *{total_amount}* OMR
You've earned *{points_earned} points* with this order.
💰 *Current Balance: {new_balance} points*
{loyalty_status}";
} }
$msg .= "
💰 *Current Balance: $final_points points*
";
// Calculate Loyalty Status String
$loyalty_status = "";
if ($final_points >= $points_threshold) { if ($final_points >= $points_threshold) {
$msg .= "🎉 Congratulations! You have enough points for a *FREE MEAL* on your next visit!"; $loyalty_status = "🎉 Congratulations! You have enough points for a *FREE MEAL* on your next visit!";
} else { } else {
$needed = $points_threshold - $final_points; $needed = $points_threshold - $final_points;
$msg .= "You need *$needed more points* to unlock a free meal."; $loyalty_status = "You need *$needed more points* to unlock a free meal.";
} }
// Prepare replacements
$replacements = [
'{customer_name}' => $customer_name,
'{company_name}' => $company_name,
'{order_id}' => $order_id,
'{order_details}' => !empty($order_items_list) ? implode("\n", $order_items_list) : 'N/A',
'{total_amount}' => number_format($total_amount, 3),
'{points_earned}' => $points_awarded,
'{points_redeemed}' => $points_deducted,
'{new_balance}' => $final_points,
'{loyalty_status}' => $loyalty_status
];
$msg = str_replace(array_keys($replacements), array_values($replacements), $template);
// Send (fire and forget, don't block response on failure, just log) // Send (fire and forget, don't block response on failure, just log)
$res = $wablas->sendMessage($customer_phone, $msg); $res = $wablas->sendMessage($customer_phone, $msg);
if (!$res['success']) { if (!$res['success']) {

View File

@ -4,7 +4,10 @@ document.addEventListener('DOMContentLoaded', () => {
const cartTotalPrice = document.getElementById('cart-total-price'); const cartTotalPrice = document.getElementById('cart-total-price');
const cartSubtotal = document.getElementById('cart-subtotal'); const cartSubtotal = document.getElementById('cart-subtotal');
const cartDiscountInput = document.getElementById('cart-discount-input'); const cartDiscountInput = document.getElementById('cart-discount-input');
const checkoutBtn = document.getElementById('checkout-btn');
// Updated Button References
const quickOrderBtn = document.getElementById('quick-order-btn');
const placeOrderBtn = document.getElementById('place-order-btn');
// Loyalty State // Loyalty State
let isLoyaltyRedemption = false; let isLoyaltyRedemption = false;
@ -176,10 +179,6 @@ document.addEventListener('DOMContentLoaded', () => {
// Hide Loyalty // Hide Loyalty
if (loyaltySection) loyaltySection.classList.add('d-none'); if (loyaltySection) loyaltySection.classList.add('d-none');
isLoyaltyRedemption = false; isLoyaltyRedemption = false;
// If we had a discount applied via loyalty, remove it?
// Better to just let the user manually adjust if they want, but technically if it was 100% off due to loyalty, we should revert.
// But tracking "discount source" is complex. For now, let's just leave discount as is or reset it if it equals total?
// Safer to reset discount to 0.
cartDiscountInput.value = 0; cartDiscountInput.value = 0;
updateCart(); updateCart();
@ -322,15 +321,11 @@ document.addEventListener('DOMContentLoaded', () => {
list.innerHTML = ''; list.innerHTML = '';
// Add "Base" option if needed, but usually variants are mandatory if they exist.
// Assuming mandatory for now or include a base option.
variants.forEach(v => { variants.forEach(v => {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center'; btn.className = 'list-group-item list-group-item-action d-flex justify-content-between align-items-center';
const adj = parseFloat(v.price_adjustment); const adj = parseFloat(v.price_adjustment);
const sign = adj > 0 ? '+' : ''; const sign = adj > 0 ? '+' : '';
const priceText = adj !== 0 ? `${sign}${formatCurrency(adj)}` : '';
const finalPrice = product.base_price + adj; const finalPrice = product.base_price + adj;
btn.innerHTML = ` btn.innerHTML = `
@ -351,28 +346,13 @@ document.addEventListener('DOMContentLoaded', () => {
} }
function addToCart(product) { function addToCart(product) {
// Unique ID for cart item is ProductID + VariantID
const existing = cart.find(item => item.id === product.id && item.variant_id === product.variant_id); const existing = cart.find(item => item.id === product.id && item.variant_id === product.variant_id);
if (existing) { if (existing) {
existing.quantity++; existing.quantity++;
} else { } else {
// Clone to avoid reference issues
cart.push({...product}); cart.push({...product});
} }
updateCart(); updateCart();
// If loyalty was applied, maybe re-apply or warn?
// Simple: If loyalty applied, update discount to match new total?
if (isLoyaltyRedemption) {
// Re-calculate full discount
// Wait, updateCart is called below.
// We need to re-calc discount after update.
// But updateCart uses discount input value.
// Let's just reset loyalty on cart change? Or re-apply 100% discount.
// Better: Reset loyalty on cart modification to avoid confusion?
// Or keep it simple: Just update discount.
}
} }
window.changeQuantity = function(index, delta) { window.changeQuantity = function(index, delta) {
@ -395,7 +375,8 @@ document.addEventListener('DOMContentLoaded', () => {
</div>`; </div>`;
cartSubtotal.innerText = formatCurrency(0); cartSubtotal.innerText = formatCurrency(0);
cartTotalPrice.innerText = formatCurrency(0); cartTotalPrice.innerText = formatCurrency(0);
checkoutBtn.disabled = true; if (quickOrderBtn) quickOrderBtn.disabled = true;
if (placeOrderBtn) placeOrderBtn.disabled = true;
return; return;
} }
@ -431,10 +412,7 @@ document.addEventListener('DOMContentLoaded', () => {
cartSubtotal.innerText = formatCurrency(subtotal); cartSubtotal.innerText = formatCurrency(subtotal);
// Discount
let discount = parseFloat(cartDiscountInput.value) || 0; let discount = parseFloat(cartDiscountInput.value) || 0;
// Auto-update discount if loyalty active
if (isLoyaltyRedemption) { if (isLoyaltyRedemption) {
discount = subtotal; discount = subtotal;
cartDiscountInput.value = subtotal.toFixed(2); cartDiscountInput.value = subtotal.toFixed(2);
@ -444,15 +422,12 @@ document.addEventListener('DOMContentLoaded', () => {
if (total < 0) total = 0; if (total < 0) total = 0;
cartTotalPrice.innerText = formatCurrency(total); cartTotalPrice.innerText = formatCurrency(total);
checkoutBtn.disabled = false; if (quickOrderBtn) quickOrderBtn.disabled = false;
if (placeOrderBtn) placeOrderBtn.disabled = false;
} }
if (cartDiscountInput) { if (cartDiscountInput) {
cartDiscountInput.addEventListener('input', () => { cartDiscountInput.addEventListener('input', () => {
// If user manually changes discount, maybe disable loyalty flag?
// But if they just tweak it, it's fine.
// Ideally, if they lower it, loyalty flag might still be true but that's weird.
// Let's assume manual input overrides auto-loyalty.
updateCart(); updateCart();
}); });
} }
@ -493,29 +468,45 @@ document.addEventListener('DOMContentLoaded', () => {
return 'bi-wallet2'; return 'bi-wallet2';
} }
// Initialize Payment Methods
renderPaymentMethods(); renderPaymentMethods();
// --- Checkout Flow --- // --- Checkout Flow (Quick Order) ---
checkoutBtn.addEventListener('click', () => { function validateOrder() {
if (cart.length === 0) return; if (cart.length === 0) return false;
const orderTypeInput = document.querySelector('input[name="order_type"]:checked'); const orderTypeInput = document.querySelector('input[name="order_type"]:checked');
const orderType = orderTypeInput ? orderTypeInput.value : 'dine-in'; const orderType = orderTypeInput ? orderTypeInput.value : 'takeaway';
if (orderType === 'dine-in' && !currentTableId) { if (orderType === 'dine-in' && !currentTableId) {
showToast('Please select a table first', 'warning'); showToast('Please select a table first', 'warning');
openTableSelectionModal(); openTableSelectionModal();
return; return false;
} }
return true;
}
// Open Payment Modal instead of direct submission if (quickOrderBtn) {
paymentSelectionModal.show(); quickOrderBtn.addEventListener('click', () => {
}); if (validateOrder()) {
paymentSelectionModal.show();
}
});
}
// --- Place Order (Pay Later) Flow ---
if (placeOrderBtn) {
placeOrderBtn.addEventListener('click', () => {
if (validateOrder()) {
if (confirm("Place order without immediate payment?")) {
processOrder(null, 'Pay Later');
}
}
});
}
window.processOrder = function(paymentTypeId, paymentTypeName) { window.processOrder = function(paymentTypeId, paymentTypeName) {
const orderTypeInput = document.querySelector('input[name="order_type"]:checked'); const orderTypeInput = document.querySelector('input[name="order_type"]:checked');
const orderType = orderTypeInput ? orderTypeInput.value : 'dine-in'; const orderType = orderTypeInput ? orderTypeInput.value : 'takeaway';
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0); const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
const discount = parseFloat(cartDiscountInput.value) || 0; const discount = parseFloat(cartDiscountInput.value) || 0;
const totalAmount = Math.max(0, subtotal - discount); const totalAmount = Math.max(0, subtotal - discount);
@ -529,7 +520,7 @@ document.addEventListener('DOMContentLoaded', () => {
payment_type_id: paymentTypeId, payment_type_id: paymentTypeId,
total_amount: totalAmount, total_amount: totalAmount,
discount: discount, discount: discount,
redeem_loyalty: isLoyaltyRedemption, // Send flag redeem_loyalty: isLoyaltyRedemption,
items: cart.map(item => ({ items: cart.map(item => ({
product_id: item.id, product_id: item.id,
quantity: item.quantity, quantity: item.quantity,
@ -538,9 +529,13 @@ document.addEventListener('DOMContentLoaded', () => {
})) }))
}; };
// Disable all payment buttons // Disable buttons
const btns = paymentMethodsContainer.querySelectorAll('button'); if (paymentMethodsContainer) {
btns.forEach(b => b.disabled = true); const btns = paymentMethodsContainer.querySelectorAll('button');
btns.forEach(b => b.disabled = true);
}
if (quickOrderBtn) quickOrderBtn.disabled = true;
if (placeOrderBtn) placeOrderBtn.disabled = true;
fetch('api/order.php', { fetch('api/order.php', {
method: 'POST', method: 'POST',
@ -549,7 +544,13 @@ document.addEventListener('DOMContentLoaded', () => {
}) })
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
btns.forEach(b => b.disabled = false); if (paymentMethodsContainer) {
const btns = paymentMethodsContainer.querySelectorAll('button');
btns.forEach(b => b.disabled = false);
}
if (quickOrderBtn) quickOrderBtn.disabled = false;
if (placeOrderBtn) placeOrderBtn.disabled = false;
paymentSelectionModal.hide(); paymentSelectionModal.hide();
if (data.success) { if (data.success) {
@ -578,7 +579,13 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}) })
.catch(err => { .catch(err => {
btns.forEach(b => b.disabled = false); if (paymentMethodsContainer) {
const btns = paymentMethodsContainer.querySelectorAll('button');
btns.forEach(b => b.disabled = false);
}
if (quickOrderBtn) quickOrderBtn.disabled = false;
if (placeOrderBtn) placeOrderBtn.disabled = false;
paymentSelectionModal.hide(); paymentSelectionModal.hide();
showToast('Network Error', 'danger'); showToast('Network Error', 'danger');
}); });

View File

@ -47,15 +47,27 @@ class WablasService {
return ['success' => false, 'message' => 'Connection failed: ' . $curlError]; return ['success' => false, 'message' => 'Connection failed: ' . $curlError];
} }
$data = json_decode($response, true);
if ($httpCode >= 200 && $httpCode < 300) { if ($httpCode >= 200 && $httpCode < 300) {
$data = json_decode($response, true);
// Some APIs return 200 even on logical error, check for status field if exists // Some APIs return 200 even on logical error, check for status field if exists
if (isset($data['status']) && $data['status'] === false) { if (isset($data['status']) && $data['status'] === false) {
return ['success' => false, 'message' => 'API Error: ' . ($data['message'] ?? 'Unknown error')]; return ['success' => false, 'message' => 'API Error: ' . ($data['message'] ?? 'Unknown error')];
} }
// Check for explicit disconnected status if available in data
if (isset($data['data']['status']) && $data['data']['status'] !== 'connected') {
return ['success' => false, 'message' => 'Device Status: ' . $data['data']['status'] . ' (May need re-scan)'];
}
return ['success' => true, 'message' => 'Connection successful! Device info retrieved.']; return ['success' => true, 'message' => 'Connection successful! Device info retrieved.'];
} }
// Handle error responses with JSON message
if ($data && isset($data['message'])) {
return ['success' => false, 'message' => 'Provider Error (' . $httpCode . '): ' . $data['message']];
}
return ['success' => false, 'message' => 'HTTP Error: ' . $httpCode]; return ['success' => false, 'message' => 'HTTP Error: ' . $httpCode];
} }
@ -64,6 +76,14 @@ class WablasService {
return ['success' => false, 'message' => 'Wablas configuration missing']; return ['success' => false, 'message' => 'Wablas configuration missing'];
} }
// Format phone number: ensure 968 country code if only 8 digits provided
$cleanPhone = preg_replace('/[^0-9]/', '', $phone);
if (strlen($cleanPhone) === 8) {
$phone = '968' . $cleanPhone;
} else {
$phone = $cleanPhone;
}
$payload = [ $payload = [
'phone' => $phone, 'phone' => $phone,
'message' => $message, 'message' => $message,

23
pos.php
View File

@ -18,7 +18,7 @@ foreach ($variants_raw as $v) {
$table_id = $_GET['table'] ?? '1'; // Default table $table_id = $_GET['table'] ?? '1'; // Default table
$outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1; $outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
$settings = get_company_settings(); $settings = get_company_settings();
$order_type = $_GET['order_type'] ?? 'dine-in'; $order_type = $_GET['order_type'] ?? 'takeaway';
?> ?>
<!doctype html> <!doctype html>
<html lang="en"> <html lang="en">
@ -62,6 +62,10 @@ $order_type = $_GET['order_type'] ?? 'dine-in';
</a> </a>
<div class="d-flex align-items-center gap-3"> <div class="d-flex align-items-center gap-3">
<a href="kitchen.php" class="btn btn-sm btn-outline-secondary">Kitchen View</a>
<a href="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>
<div id="current-table-display" class="badge bg-light text-dark border px-3 py-2" style="display: none; font-size: 0.9rem;"> <div id="current-table-display" class="badge bg-light text-dark border px-3 py-2" style="display: none; font-size: 0.9rem;">
Table <?= htmlspecialchars($table_id) ?> Table <?= htmlspecialchars($table_id) ?>
</div> </div>
@ -162,12 +166,12 @@ $order_type = $_GET['order_type'] ?? 'dine-in';
<div class="p-3 border-bottom bg-white"> <div class="p-3 border-bottom bg-white">
<!-- Order Type --> <!-- Order Type -->
<div class="btn-group w-100 mb-3" role="group"> <div class="btn-group w-100 mb-3" role="group">
<input type="radio" class="btn-check" name="order_type" id="ot-dine-in" value="dine-in" <?= $order_type === 'dine-in' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary btn-sm" for="ot-dine-in">Dine-In</label>
<input type="radio" class="btn-check" name="order_type" id="ot-takeaway" value="takeaway" <?= $order_type === 'takeaway' ? 'checked' : '' ?>> <input type="radio" class="btn-check" name="order_type" id="ot-takeaway" value="takeaway" <?= $order_type === 'takeaway' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary btn-sm" for="ot-takeaway">Takeaway</label> <label class="btn btn-outline-primary btn-sm" for="ot-takeaway">Takeaway</label>
<input type="radio" class="btn-check" name="order_type" id="ot-dine-in" value="dine-in" <?= $order_type === 'dine-in' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary btn-sm" for="ot-dine-in">Dine-In</label>
<input type="radio" class="btn-check" name="order_type" id="ot-delivery" value="delivery" <?= $order_type === 'delivery' ? 'checked' : '' ?>> <input type="radio" class="btn-check" name="order_type" id="ot-delivery" value="delivery" <?= $order_type === 'delivery' ? 'checked' : '' ?>>
<label class="btn btn-outline-primary btn-sm" for="ot-delivery">Delivery</label> <label class="btn btn-outline-primary btn-sm" for="ot-delivery">Delivery</label>
</div> </div>
@ -233,9 +237,14 @@ $order_type = $_GET['order_type'] ?? 'dine-in';
<span class="fs-5 fw-bold">Total</span> <span class="fs-5 fw-bold">Total</span>
<span class="fs-4 fw-bold text-primary" id="cart-total-price"><?= format_currency(0) ?></span> <span class="fs-4 fw-bold text-primary" id="cart-total-price"><?= format_currency(0) ?></span>
</div> </div>
<button class="btn btn-primary w-100 btn-lg shadow-sm" id="checkout-btn" disabled> <div class="d-flex gap-2 w-100">
Place Order <i class="bi bi-arrow-right ms-2"></i> <button class="btn btn-primary w-50 btn-lg shadow-sm" id="quick-order-btn" disabled>
</button> Quick Order <i class="bi bi-lightning-fill ms-2"></i>
</button>
<button class="btn btn-warning w-50 btn-lg shadow-sm text-white" id="place-order-btn" disabled>
Place Order <i class="bi bi-clock ms-2"></i>
</button>
</div>
</div> </div>
</div> </div>