first draft
This commit is contained in:
parent
5d1e95ef4f
commit
a238062edb
@ -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">
|
||||||
@ -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
12
api/last_msg.txt
Normal 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.
|
||||||
@ -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']) {
|
||||||
|
|||||||
@ -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');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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
23
pos.php
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user