Autosave: 20260222-182125

This commit is contained in:
Flatlogic Bot 2026-02-22 18:21:25 +00:00
parent 2996ec35e3
commit 5d1e95ef4f
14 changed files with 977 additions and 81 deletions

View File

@ -2,63 +2,186 @@
require_once __DIR__ . '/../db/config.php';
$pdo = db();
$query = "SELECT * FROM loyalty_customers ORDER BY points DESC";
$customers_pagination = paginate_query($pdo, $query);
$customers = $customers_pagination['data'];
// Handle Settings Update
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_settings'])) {
$points_per_order = intval($_POST['points_per_order']);
$points_for_free_meal = intval($_POST['points_for_free_meal']);
$stmt = $pdo->prepare("UPDATE loyalty_settings SET points_per_order = ?, points_for_free_meal = ? WHERE id = 1");
$stmt->execute([$points_per_order, $points_for_free_meal]);
$success_msg = "Loyalty settings updated successfully!";
}
// Fetch Settings
$stmt = $pdo->query("SELECT * FROM loyalty_settings WHERE id = 1");
$settings = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$settings) {
// Default fallback if migration failed or empty
$settings = ['points_per_order' => 10, 'points_for_free_meal' => 70];
}
// Fetch Customers with Points
$page = isset($_GET['page']) ? max(1, intval($_GET['page'])) : 1;
$limit = 20;
$offset = ($page - 1) * $limit;
$count_stmt = $pdo->query("SELECT COUNT(*) FROM customers WHERE points > 0");
$total_customers = $count_stmt->fetchColumn();
$total_pages = ceil($total_customers / $limit);
$query = "SELECT * FROM customers WHERE points > 0 ORDER BY points DESC LIMIT $limit OFFSET $offset";
$stmt = $pdo->query($query);
$customers = $stmt->fetchAll(PDO::FETCH_ASSOC);
include 'includes/header.php';
?>
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fw-bold mb-0">Loyalty Program</h2>
<button class="btn btn-outline-primary" disabled>
<i class="bi bi-gear"></i> Settings (Coming Soon)
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#settingsModal">
<i class="bi bi-gear-fill me-2"></i> Configure Settings
</button>
</div>
<?php if (isset($success_msg)): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?= htmlspecialchars($success_msg) ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php endif; ?>
<div class="row mb-4">
<div class="col-md-6 col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted text-uppercase small">Current Configuration</h6>
<div class="d-flex justify-content-between align-items-center mt-3">
<div>
<span class="d-block text-muted small">Points per Order</span>
<span class="fs-4 fw-bold text-primary"><?= $settings['points_per_order'] ?> pts</span>
</div>
<div class="border-start ps-3">
<span class="d-block text-muted small">Free Meal Threshold</span>
<span class="fs-4 fw-bold text-success"><?= $settings['points_for_free_meal'] ?> pts</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
<div class="p-3 border-bottom bg-light">
<?php render_pagination_controls($customers_pagination); ?>
<div class="card-header bg-white py-3">
<h5 class="mb-0">Loyalty Members</h5>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Name</th>
<th>Email</th>
<th class="ps-4">Customer</th>
<th>Contact</th>
<th>Points Balance</th>
<th>Status</th>
<th>Joined</th>
</tr>
</thead>
<tbody>
<?php foreach ($customers as $customer): ?>
<?php
$progress = min(100, ($customer['points'] / $settings['points_for_free_meal']) * 100);
$eligible = $customer['points'] >= $settings['points_for_free_meal'];
?>
<tr>
<td class="ps-4 fw-medium">#<?= $customer['id'] ?></td>
<td class="fw-bold"><?= htmlspecialchars($customer['name']) ?></td>
<td><a href="mailto:<?= htmlspecialchars($customer['email']) ?>" class="text-decoration-none"><?= htmlspecialchars($customer['email']) ?></a></td>
<td class="ps-4">
<div class="fw-bold"><?= htmlspecialchars($customer['name']) ?></div>
</td>
<td>
<span class="badge bg-warning text-dark border border-warning">
<i class="bi bi-star-fill small me-1"></i> <?= $customer['points'] ?> pts
<div class="small"><?= htmlspecialchars($customer['phone'] ?? '-') ?></div>
<div class="small text-muted"><?= htmlspecialchars($customer['email'] ?? '') ?></div>
</td>
<td>
<div class="d-flex align-items-center">
<span class="badge bg-warning text-dark border border-warning me-2" style="width: 60px;">
<?= $customer['points'] ?> pts
</span>
<div class="progress flex-grow-1" style="height: 6px; max-width: 100px;">
<div class="progress-bar bg-success" role="progressbar" style="width: <?= $progress ?>%"></div>
</div>
</div>
</td>
<td>
<?php if ($eligible): ?>
<span class="badge bg-success-subtle text-success border border-success-subtle">
<i class="bi bi-gift-fill me-1"></i> Eligible for Free Meal
</span>
<?php else: ?>
<span class="text-muted small">
<?= $settings['points_for_free_meal'] - $customer['points'] ?> pts to go
</span>
<?php endif; ?>
</td>
<td class="text-muted small"><?= date('M d, Y', strtotime($customer['created_at'])) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($customers)): ?>
<tr>
<td colspan="5" class="text-center py-5 text-muted">No loyalty members yet.</td>
<td colspan="5" class="text-center py-5 text-muted">No active loyalty members found.</td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>
<!-- Bottom Pagination -->
<div class="p-3 border-top bg-light">
<?php render_pagination_controls($customers_pagination); ?>
<!-- Pagination -->
<?php if ($total_pages > 1): ?>
<div class="card-footer bg-white py-3">
<nav>
<ul class="pagination justify-content-center mb-0">
<li class="page-item <?= $page <= 1 ? 'disabled' : '' ?>">
<a class="page-link" href="?page=<?= $page - 1 ?>">Previous</a>
</li>
<?php for ($i = 1; $i <= $total_pages; $i++): ?>
<li class="page-item <?= $page == $i ? 'active' : '' ?>">
<a class="page-link" href="?page=<?= $i ?>"><?= $i ?></a>
</li>
<?php endfor; ?>
<li class="page-item <?= $page >= $total_pages ? 'disabled' : '' ?>">
<a class="page-link" href="?page=<?= $page + 1 ?>">Next</a>
</li>
</ul>
</nav>
</div>
<?php endif; ?>
</div>
<!-- Settings Modal -->
<div class="modal fade" id="settingsModal" tabindex="-1">
<div class="modal-dialog">
<form method="POST" class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Configure Loyalty Program</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" name="update_settings" value="1">
<div class="mb-3">
<label class="form-label">Points per Order</label>
<input type="number" name="points_per_order" class="form-control" value="<?= $settings['points_per_order'] ?>" min="0" required>
<div class="form-text">Points awarded for every completed order.</div>
</div>
<div class="mb-3">
<label class="form-label">Points for Free Meal</label>
<input type="number" name="points_for_free_meal" class="form-control" value="<?= $settings['points_for_free_meal'] ?>" min="0" required>
<div class="form-text">Threshold points required to redeem a free meal.</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>

View File

@ -9,16 +9,55 @@ if (isset($_POST['action']) && $_POST['action'] === 'update_status') {
$new_status = $_POST['status'];
$stmt = $pdo->prepare("UPDATE orders SET status = ? WHERE id = ?");
$stmt->execute([$new_status, $order_id]);
header("Location: orders.php");
header("Location: orders.php?" . http_build_query($_GET)); // Keep filters
exit;
}
$query = "SELECT o.*,
// Fetch Outlets for Filter
$outlets = $pdo->query("SELECT id, name FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
// Build Query with Filters
$params = [];
$where = [];
// Filter: Outlet
if (!empty($_GET['outlet_id'])) {
$where[] = "o.outlet_id = :outlet_id";
$params[':outlet_id'] = $_GET['outlet_id'];
}
// Filter: Date Range
if (!empty($_GET['start_date'])) {
$where[] = "DATE(o.created_at) >= :start_date";
$params[':start_date'] = $_GET['start_date'];
}
if (!empty($_GET['end_date'])) {
$where[] = "DATE(o.created_at) <= :end_date";
$params[':end_date'] = $_GET['end_date'];
}
// Filter: Search (Order No)
if (!empty($_GET['search'])) {
// Exact match for ID usually, but LIKE might be more user friendly if they type partial?
// "search by order no" usually implies ID. Let's stick to ID or simple LIKE.
// If numeric, assume ID.
if (is_numeric($_GET['search'])) {
$where[] = "o.id = :search";
$params[':search'] = $_GET['search'];
}
}
$where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
// Changed alias 'out' to 'ot' to avoid reserved keyword conflict
$query = "SELECT o.*, ot.name as outlet_name,
(SELECT GROUP_CONCAT(CONCAT(p.name, ' x', oi.quantity) SEPARATOR ', ') FROM order_items oi JOIN products p ON oi.product_id = p.id WHERE oi.order_id = o.id) as items_summary
FROM orders o
LEFT JOIN outlets ot ON o.outlet_id = ot.id
$where_clause
ORDER BY o.created_at DESC";
$orders_pagination = paginate_query($pdo, $query);
$orders_pagination = paginate_query($pdo, $query, $params);
$orders = $orders_pagination['data'];
include 'includes/header.php';
@ -31,6 +70,47 @@ include 'includes/header.php';
</span>
</div>
<!-- Filters -->
<div class="card border-0 shadow-sm mb-4">
<div class="card-body bg-light">
<form method="GET" class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">Outlet</label>
<select name="outlet_id" class="form-select">
<option value="">All Outlets</option>
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= (isset($_GET['outlet_id']) && $_GET['outlet_id'] == $outlet['id']) ? 'selected' : '' ?>>
<?= htmlspecialchars($outlet['name']) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">Date Range</label>
<div class="input-group">
<input type="date" name="start_date" class="form-control" value="<?= $_GET['start_date'] ?? '' ?>" placeholder="Start">
<span class="input-group-text bg-white border-start-0 border-end-0">-</span>
<input type="date" name="end_date" class="form-control" value="<?= $_GET['end_date'] ?? '' ?>" placeholder="End">
</div>
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-muted">Search</label>
<input type="text" name="search" class="form-control" placeholder="Order No (ID)" value="<?= htmlspecialchars($_GET['search'] ?? '') ?>">
</div>
<div class="col-md-3">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-filter"></i> Filter
</button>
<a href="orders.php" class="btn btn-outline-secondary">
<i class="bi bi-x-lg"></i>
</a>
</div>
</div>
</form>
</div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<!-- Pagination Controls -->
@ -42,6 +122,8 @@ include 'includes/header.php';
<thead class="bg-light">
<tr>
<th class="ps-4">ID</th>
<th>Outlet</th>
<th>Customer</th>
<th>Type</th>
<th>Source</th>
<th>Items</th>
@ -55,6 +137,22 @@ include 'includes/header.php';
<?php foreach ($orders as $order): ?>
<tr>
<td class="ps-4 fw-medium">#<?= $order['id'] ?></td>
<td>
<span class="badge bg-white text-dark border">
<i class="bi bi-shop me-1"></i>
<?= htmlspecialchars($order['outlet_name'] ?? 'Unknown') ?>
</span>
</td>
<td>
<?php if (!empty($order['customer_name'])): ?>
<div class="fw-medium"><?= htmlspecialchars($order['customer_name']) ?></div>
<?php if (!empty($order['customer_phone'])): ?>
<small class="text-muted"><i class="bi bi-telephone me-1"></i><?= htmlspecialchars($order['customer_phone']) ?></small>
<?php endif; ?>
<?php else: ?>
<span class="text-muted small">Guest</span>
<?php endif; ?>
</td>
<td>
<?php
$badge = match($order['order_type']) {
@ -81,11 +179,18 @@ include 'includes/header.php';
<?= ucfirst($order['status']) ?>
</span>
</td>
<td class="text-muted small"><?= date('H:i', strtotime($order['created_at'])) ?></td>
<td class="text-muted small">
<div><?= date('M d', strtotime($order['created_at'])) ?></div>
<div><?= date('H:i', strtotime($order['created_at'])) ?></div>
</td>
<td>
<form method="POST" class="d-flex gap-2">
<input type="hidden" name="order_id" value="<?= $order['id'] ?>">
<input type="hidden" name="action" value="update_status">
<!-- Preserve filter params in form action? No, form POSTs to same URL.
We can add hidden inputs to redirect back with params, but header location above handles it if we pass GET params.
The POST form action will just be "orders.php".
Ideally we should append query string to action. -->
<?php if ($order['status'] === 'pending'): ?>
<button type="submit" name="status" value="preparing" class="btn btn-sm btn-primary">
@ -111,9 +216,9 @@ include 'includes/header.php';
<?php endforeach; ?>
<?php if (empty($orders)): ?>
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<td colspan="10" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
No active orders.
No active orders found matching your criteria.
</td>
</tr>
<?php endif; ?>

64
api/create_customer.php Normal file
View File

@ -0,0 +1,64 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['error' => 'Method not allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$name = trim($input['name'] ?? '');
$phone = trim($input['phone'] ?? '');
if (empty($name)) {
echo json_encode(['error' => 'Name is required']);
exit;
}
if (!preg_match('/^\d{8}$/', $phone)) {
echo json_encode(['error' => 'Phone number must be exactly 8 digits']);
exit;
}
try {
$pdo = db();
// Check if phone already exists
$stmt = $pdo->prepare("SELECT id FROM customers WHERE phone = ?");
$stmt->execute([$phone]);
if ($stmt->fetch()) {
echo json_encode(['error' => 'Customer with this phone number already exists']);
exit;
}
$stmt = $pdo->prepare("INSERT INTO customers (name, phone, points) VALUES (?, ?, 0)");
if ($stmt->execute([$name, $phone])) {
$id = $pdo->lastInsertId();
// Fetch settings for consistency (though new customer is 0 points)
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$threshold = $settings ? intval($settings['points_for_free_meal']) : 70;
echo json_encode([
'success' => true,
'customer' => [
'id' => $id,
'name' => $name,
'phone' => $phone,
'email' => '',
'points' => 0,
'eligible_for_free_meal' => false,
'points_needed' => $threshold
]
]);
} else {
echo json_encode(['error' => 'Failed to create customer']);
}
} catch (Exception $e) {
error_log("Create Customer Error: " . $e->getMessage());
echo json_encode(['error' => 'Database error']);
}

View File

@ -7,6 +7,8 @@ $pdo = db();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Fetch active kitchen orders
try {
$outlet_id = isset($_GET['outlet_id']) ? intval($_GET['outlet_id']) : 1;
// We want orders that are NOT completed or cancelled
// Status flow: pending -> preparing -> ready -> completed
// Kitchen sees: pending, preparing, ready
@ -19,9 +21,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') {
JOIN products p ON oi.product_id = p.id
LEFT JOIN product_variants v ON oi.variant_id = v.id
WHERE o.status IN ('pending', 'preparing', 'ready')
AND o.outlet_id = :outlet_id
ORDER BY o.created_at ASC
");
$stmt->execute();
$stmt->execute(['outlet_id' => $outlet_id]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$orders = [];

View File

@ -1,6 +1,7 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../includes/WablasService.php';
$input = file_get_contents('php://input');
$data = json_decode($input, true);
@ -30,12 +31,12 @@ try {
$tid = $data['table_number'] ?? null; // Front-end sends ID as table_number
if ($tid) {
// Validate table exists AND belongs to the correct outlet
$stmt = $pdo->prepare("
SELECT t.id, t.name
$stmt = $pdo->prepare(
"SELECT t.id, t.name
FROM tables t
JOIN areas a ON t.area_id = a.id
WHERE t.id = ? AND a.outlet_id = ?
");
WHERE t.id = ? AND a.outlet_id = ?"
);
$stmt->execute([$tid, $outlet_id]);
$table = $stmt->fetch(PDO::FETCH_ASSOC);
if ($table) {
@ -47,13 +48,13 @@ try {
// If not found or not provided, leave null (Walk-in/Counter) or try to find a default table for this outlet
if (!$table_id) {
// Optional: try to find the first available table for this outlet
$stmt = $pdo->prepare("
SELECT t.id, t.name
$stmt = $pdo->prepare(
"SELECT t.id, t.name
FROM tables t
JOIN areas a ON t.area_id = a.id
WHERE a.outlet_id = ?
LIMIT 1
");
LIMIT 1"
);
$stmt->execute([$outlet_id]);
$table = $stmt->fetch(PDO::FETCH_ASSOC);
if ($table) {
@ -68,23 +69,56 @@ try {
$customer_name = $data['customer_name'] ?? null;
$customer_phone = $data['customer_phone'] ?? null;
// Fetch Loyalty Settings
$settingsStmt = $pdo->query("SELECT points_per_order, points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$points_per_order = $settings ? intval($settings['points_per_order']) : 10;
$points_threshold = $settings ? intval($settings['points_for_free_meal']) : 70;
$current_points = 0;
$points_deducted = 0;
$points_awarded = 0;
if ($customer_id) {
$stmt = $pdo->prepare("SELECT name, phone FROM customers WHERE id = ?");
$stmt = $pdo->prepare("SELECT name, phone, points FROM customers WHERE id = ?");
$stmt->execute([$customer_id]);
$cust = $stmt->fetch(PDO::FETCH_ASSOC);
if ($cust) {
$customer_name = $cust['name'];
$customer_phone = $cust['phone'];
$current_points = intval($cust['points']);
} else {
$customer_id = null;
}
}
// Loyalty Redemption Logic
$redeem_loyalty = !empty($data['redeem_loyalty']);
if ($redeem_loyalty && $customer_id) {
if ($current_points < $points_threshold) {
throw new Exception("Insufficient loyalty points for redemption.");
}
// Deduct points
$deductStmt = $pdo->prepare("UPDATE customers SET points = points - ? WHERE id = ?");
$deductStmt->execute([$points_threshold, $customer_id]);
$points_deducted = $points_threshold;
}
// Award Points
if ($customer_id) {
$awardStmt = $pdo->prepare("UPDATE customers SET points = points + ? WHERE id = ?");
$awardStmt->execute([$points_per_order, $customer_id]);
$points_awarded = $points_per_order;
}
// Payment Type
$payment_type_id = isset($data['payment_type_id']) ? intval($data['payment_type_id']) : null;
$discount = isset($data['discount']) ? floatval($data['discount']) : 0.00;
$total_amount = isset($data['total_amount']) ? floatval($data['total_amount']) : 0.00;
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, total_amount, discount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $total_amount, $discount]);
$stmt = $pdo->prepare("INSERT INTO orders (outlet_id, table_id, table_number, order_type, customer_id, customer_name, customer_phone, payment_type_id, total_amount, discount, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending')");
$stmt->execute([$outlet_id, $table_id, $table_number, $order_type, $customer_id, $customer_name, $customer_phone, $payment_type_id, $total_amount, $discount]);
$order_id = $pdo->lastInsertId();
$item_stmt = $pdo->prepare("INSERT INTO order_items (order_id, product_id, variant_id, quantity, unit_price) VALUES (?, ?, ?, ?, ?)");
@ -102,6 +136,50 @@ try {
}
$pdo->commit();
// --- Post-Transaction Actions ---
// Send WhatsApp Notification if Customer exists
if ($customer_id && $customer_phone) {
try {
// Calculate final points
$final_points = $current_points - $points_deducted + $points_awarded;
// Determine message content
$wablas = new WablasService($pdo);
$msg = "Dear *$customer_name*,
";
$msg .= "Thank you for dining with us! 🍽️
";
$msg .= "You've earned *$points_awarded points* with this order.
";
if ($points_deducted > 0) {
$msg .= "You also redeemed *$points_deducted points* for a free meal! 🎁
";
}
$msg .= "
💰 *Current Balance: $final_points points*
";
if ($final_points >= $points_threshold) {
$msg .= "🎉 Congratulations! You have enough points for a *FREE MEAL* on your next visit!";
} else {
$needed = $points_threshold - $final_points;
$msg .= "You need *$needed more points* to unlock a free meal.";
}
// Send (fire and forget, don't block response on failure, just log)
$res = $wablas->sendMessage($customer_phone, $msg);
if (!$res['success']) {
error_log("Wablas Notification Failed: " . $res['message']);
}
} catch (Exception $w) {
error_log("Wablas Exception: " . $w->getMessage());
}
}
echo json_encode(['success' => true, 'order_id' => $order_id]);
} catch (Exception $e) {

View File

@ -11,11 +11,24 @@ if (strlen($q) < 2) {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT id, name, phone, email FROM customers WHERE name LIKE ? OR phone LIKE ? LIMIT 10");
// Fetch Loyalty Settings
$settingsStmt = $pdo->query("SELECT points_for_free_meal FROM loyalty_settings WHERE id = 1");
$settings = $settingsStmt->fetch(PDO::FETCH_ASSOC);
$threshold = $settings ? $settings['points_for_free_meal'] : 70;
$stmt = $pdo->prepare("SELECT id, name, phone, email, points FROM customers WHERE name LIKE ? OR phone LIKE ? LIMIT 10");
$searchTerm = "%$q%";
$stmt->execute([$searchTerm, $searchTerm]);
$customers = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($customers as &$customer) {
$customer['points'] = intval($customer['points']);
$customer['eligible_for_free_meal'] = $customer['points'] >= $threshold;
$customer['points_needed'] = max(0, $threshold - $customer['points']);
$customer['threshold'] = $threshold;
}
echo json_encode($customers);
} catch (Exception $e) {
error_log("Customer Search Error: " . $e->getMessage());

View File

@ -6,6 +6,13 @@ document.addEventListener('DOMContentLoaded', () => {
const cartDiscountInput = document.getElementById('cart-discount-input');
const checkoutBtn = document.getElementById('checkout-btn');
// Loyalty State
let isLoyaltyRedemption = false;
const loyaltySection = document.getElementById('loyalty-section');
const loyaltyPointsDisplay = document.getElementById('loyalty-points-display');
const loyaltyMessage = document.getElementById('loyalty-message');
const redeemLoyaltyBtn = document.getElementById('redeem-loyalty-btn');
// Table Management
let currentTableId = null;
let currentTableName = null;
@ -25,6 +32,12 @@ document.addEventListener('DOMContentLoaded', () => {
const clearCustomerBtn = document.getElementById('clear-customer');
const customerInfo = document.getElementById('customer-info');
const customerNameDisplay = document.getElementById('customer-name-display');
let currentCustomer = null;
// Payment Modal
const paymentModalEl = document.getElementById('paymentSelectionModal');
const paymentSelectionModal = new bootstrap.Modal(paymentModalEl);
const paymentMethodsContainer = document.getElementById('payment-methods-container');
// Product Search & Filter
const productSearchInput = document.getElementById('product-search-input');
@ -123,6 +136,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
function selectCustomer(cust) {
currentCustomer = cust;
selectedCustomerId.value = cust.id;
customerNameDisplay.textContent = cust.name;
customerSearchInput.value = cust.name; // Show name in input
@ -130,19 +144,73 @@ document.addEventListener('DOMContentLoaded', () => {
customerResults.style.display = 'none';
clearCustomerBtn.classList.remove('d-none');
// customerInfo.classList.remove('d-none');
// Loyalty Logic
if (loyaltySection) {
loyaltySection.classList.remove('d-none');
loyaltyPointsDisplay.textContent = cust.points + ' pts';
if (cust.eligible_for_free_meal) {
redeemLoyaltyBtn.disabled = false;
loyaltyMessage.innerHTML = '<span class="text-success fw-bold">Eligible for Free Meal!</span>';
} else {
redeemLoyaltyBtn.disabled = true;
const needed = cust.points_needed || 0;
loyaltyMessage.textContent = `${needed} pts away from a free meal.`;
}
}
// Reset redemption state when switching customer (though usually we clear first)
isLoyaltyRedemption = false;
}
if (clearCustomerBtn) {
clearCustomerBtn.addEventListener('click', () => {
currentCustomer = null;
selectedCustomerId.value = '';
customerSearchInput.value = '';
customerSearchInput.disabled = false;
clearCustomerBtn.classList.add('d-none');
customerInfo.classList.add('d-none');
// Hide Loyalty
if (loyaltySection) loyaltySection.classList.add('d-none');
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;
updateCart();
customerSearchInput.focus();
});
}
// Loyalty Redeem Click
if (redeemLoyaltyBtn) {
redeemLoyaltyBtn.addEventListener('click', () => {
if (cart.length === 0) {
showToast("Cart is empty!", "warning");
return;
}
if (!currentCustomer || !currentCustomer.eligible_for_free_meal) return;
if (confirm("Redeem 70 points for a free meal? This will apply a full discount to the current order.")) {
isLoyaltyRedemption = true;
// Calculate total and apply as discount
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
cartDiscountInput.value = subtotal.toFixed(2);
updateCart();
showToast("Loyalty Redemption Applied!", "success");
redeemLoyaltyBtn.disabled = true; // Prevent double click
loyaltyMessage.innerHTML = '<span class="text-success fw-bold"><i class="bi bi-check-circle"></i> Redeemed! Place order to finalize.</span>';
}
});
}
// --- Table & Order Type Logic ---
const orderTypeInputs = document.querySelectorAll('input[name="order_type"]');
@ -293,6 +361,18 @@ document.addEventListener('DOMContentLoaded', () => {
cart.push({...product});
}
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) {
@ -353,6 +433,13 @@ document.addEventListener('DOMContentLoaded', () => {
// Discount
let discount = parseFloat(cartDiscountInput.value) || 0;
// Auto-update discount if loyalty active
if (isLoyaltyRedemption) {
discount = subtotal;
cartDiscountInput.value = subtotal.toFixed(2);
}
let total = subtotal - discount;
if (total < 0) total = 0;
@ -361,7 +448,13 @@ document.addEventListener('DOMContentLoaded', () => {
}
if (cartDiscountInput) {
cartDiscountInput.addEventListener('input', updateCart);
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();
});
}
window.removeFromCart = function(index) {
@ -369,7 +462,41 @@ document.addEventListener('DOMContentLoaded', () => {
updateCart();
};
// --- Checkout ---
// --- Payment Selection Logic ---
function renderPaymentMethods() {
if (!paymentMethodsContainer) return;
paymentMethodsContainer.innerHTML = '';
if (typeof PAYMENT_TYPES !== 'undefined' && PAYMENT_TYPES.length > 0) {
PAYMENT_TYPES.forEach(pt => {
const col = document.createElement('div');
col.className = 'col-6';
col.innerHTML = `
<button class="btn btn-outline-primary w-100 payment-btn d-flex flex-column align-items-center justify-content-center"
onclick="processOrder(${pt.id}, '${pt.name}')">
<i class="bi ${getPaymentIcon(pt.name)} fs-3 mb-1"></i>
<span>${pt.name}</span>
</button>
`;
paymentMethodsContainer.appendChild(col);
});
} else {
paymentMethodsContainer.innerHTML = '<div class="alert alert-warning">No payment methods configured.</div>';
}
}
function getPaymentIcon(name) {
const n = name.toLowerCase();
if (n.includes('cash')) return 'bi-cash-coin';
if (n.includes('card') || n.includes('visa') || n.includes('master')) return 'bi-credit-card';
if (n.includes('qr') || n.includes('scan')) return 'bi-qr-code';
return 'bi-wallet2';
}
// Initialize Payment Methods
renderPaymentMethods();
// --- Checkout Flow ---
checkoutBtn.addEventListener('click', () => {
if (cart.length === 0) return;
@ -382,6 +509,13 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
// Open Payment Modal instead of direct submission
paymentSelectionModal.show();
});
window.processOrder = function(paymentTypeId, paymentTypeName) {
const orderTypeInput = document.querySelector('input[name="order_type"]:checked');
const orderType = orderTypeInput ? orderTypeInput.value : 'dine-in';
const subtotal = cart.reduce((acc, item) => acc + (item.price * item.quantity), 0);
const discount = parseFloat(cartDiscountInput.value) || 0;
const totalAmount = Math.max(0, subtotal - discount);
@ -392,8 +526,10 @@ document.addEventListener('DOMContentLoaded', () => {
order_type: orderType,
customer_id: custId || null,
outlet_id: new URLSearchParams(window.location.search).get('outlet_id') || 1,
payment_type_id: paymentTypeId,
total_amount: totalAmount,
discount: discount,
redeem_loyalty: isLoyaltyRedemption, // Send flag
items: cart.map(item => ({
product_id: item.id,
quantity: item.quantity,
@ -402,8 +538,9 @@ document.addEventListener('DOMContentLoaded', () => {
}))
};
checkoutBtn.disabled = true;
checkoutBtn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Processing...';
// Disable all payment buttons
const btns = paymentMethodsContainer.querySelectorAll('button');
btns.forEach(b => b.disabled = true);
fetch('api/order.php', {
method: 'POST',
@ -412,12 +549,27 @@ document.addEventListener('DOMContentLoaded', () => {
})
.then(res => res.json())
.then(data => {
checkoutBtn.disabled = false;
checkoutBtn.innerHTML = 'Place Order <i class="bi bi-arrow-right ms-2"></i>';
btns.forEach(b => b.disabled = false);
paymentSelectionModal.hide();
if (data.success) {
// Print Receipt
printReceipt({
orderId: data.order_id,
customer: currentCustomer,
items: [...cart],
total: totalAmount,
discount: discount,
orderType: orderType,
tableNumber: (orderType === 'dine-in') ? currentTableName : null,
date: new Date().toLocaleString(),
paymentMethod: paymentTypeName,
loyaltyRedeemed: isLoyaltyRedemption
});
cart = [];
cartDiscountInput.value = 0;
isLoyaltyRedemption = false; // Reset
updateCart();
if (clearCustomerBtn) clearCustomerBtn.click();
showToast(`Order #${data.order_id} placed!`, 'success');
@ -426,11 +578,11 @@ document.addEventListener('DOMContentLoaded', () => {
}
})
.catch(err => {
checkoutBtn.disabled = false;
checkoutBtn.innerHTML = 'Place Order';
btns.forEach(b => b.disabled = false);
paymentSelectionModal.hide();
showToast('Network Error', 'danger');
});
});
};
function showToast(msg, type = 'primary') {
const toastContainer = document.getElementById('toast-container');
@ -450,6 +602,174 @@ document.addEventListener('DOMContentLoaded', () => {
el.addEventListener('hidden.bs.toast', () => el.remove());
}
function printReceipt(data) {
const width = 300;
const height = 600;
const left = (screen.width - width) / 2;
const top = (screen.height - height) / 2;
const win = window.open('', 'Receipt', `width=${width},height=${height},top=${top},left=${left}`);
const itemsHtml = data.items.map(item => `
<tr>
<td style="padding: 2px 0;">
${item.name} <br>
${item.variant_name ? `<small>(${item.variant_name})</small>` : ''}
</td>
<td style="text-align: right; vertical-align: top;">${item.quantity} x ${formatCurrency(item.price)}</td>
</tr>
`).join('');
const customerHtml = data.customer ? `
<div style="margin-bottom: 10px; border-bottom: 1px dashed #000; padding-bottom: 5px;">
<strong>Customer:</strong><br>
${data.customer.name}<br>
${data.customer.phone || ''}
</div>
` : '';
const tableHtml = data.tableNumber ? `<div>Table: ${data.tableNumber}</div>` : '';
const paymentHtml = data.paymentMethod ? `<div>Payment: ${data.paymentMethod}</div>` : '';
const loyaltyHtml = data.loyaltyRedeemed ? `<div><strong>* Free Meal Redeemed *</strong></div>` : '';
const html = `
<html>
<head>
<title>Receipt #${data.orderId}</title>
<style>
body { font-family: 'Courier New', monospace; font-size: 12px; margin: 0; padding: 10px; }
.header { text-align: center; margin-bottom: 10px; }
.header h2 { margin: 0; font-size: 16px; }
table { width: 100%; border-collapse: collapse; }
.totals { margin-top: 10px; border-top: 1px dashed #000; padding-top: 5px; }
.footer { text-align: center; margin-top: 20px; font-size: 10px; }
@media print {
.no-print { display: none; }
}
</style>
</head>
<body>
<div class="header">
<h2>FLATLOGIC POS</h2>
<div>123 Main St, City</div>
<div>Tel: 123-456-7890</div>
</div>
<div style="margin-bottom: 10px;">
<div>Order #${data.orderId}</div>
<div>${data.date}</div>
<div>Type: ${data.orderType.toUpperCase()}</div>
${tableHtml}
${paymentHtml}
${loyaltyHtml}
</div>
${customerHtml}
<table>
${itemsHtml}
</table>
<div class="totals">
<table style="width: 100%">
<tr>
<td>Subtotal</td>
<td style="text-align: right">${formatCurrency(data.total + data.discount)}</td>
</tr>
${data.discount > 0 ? `
<tr>
<td>Discount</td>
<td style="text-align: right">-${formatCurrency(data.discount)}</td>
</tr>` : ''}
<tr style="font-weight: bold; font-size: 14px;">
<td>Total</td>
<td style="text-align: right">${formatCurrency(data.total)}</td>
</tr>
</table>
</div>
<div class="footer">
Thank you for your visit!<br>
Please come again.
</div>
<script>
window.onload = function() {
window.print();
setTimeout(function() { window.close(); }, 500);
}
</script>
</body>
</html>
`;
win.document.write(html);
win.document.close();
}
// Initialize logic
checkOrderType();
// --- Add Customer Logic ---
const addCustomerBtn = document.getElementById('add-customer-btn');
const addCustomerModalEl = document.getElementById('addCustomerModal');
if (addCustomerBtn && addCustomerModalEl) {
const addCustomerModal = new bootstrap.Modal(addCustomerModalEl);
const saveCustomerBtn = document.getElementById('save-new-customer');
const newCustomerName = document.getElementById('new-customer-name');
const newCustomerPhone = document.getElementById('new-customer-phone');
const phoneError = document.getElementById('phone-error');
addCustomerBtn.addEventListener('click', () => {
newCustomerName.value = '';
newCustomerPhone.value = '';
phoneError.classList.add('d-none');
addCustomerModal.show();
});
saveCustomerBtn.addEventListener('click', () => {
const name = newCustomerName.value.trim();
const phone = newCustomerPhone.value.trim();
if (name === '') {
showToast('Name is required', 'warning');
return;
}
// 8 digits validation
if (!/^\d{8}$/.test(phone)) {
phoneError.classList.remove('d-none');
return;
} else {
phoneError.classList.add('d-none');
}
saveCustomerBtn.disabled = true;
saveCustomerBtn.textContent = 'Saving...';
fetch('api/create_customer.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: name, phone: phone })
})
.then(res => res.json())
.then(data => {
saveCustomerBtn.disabled = false;
saveCustomerBtn.textContent = 'Save Customer';
if (data.success) {
addCustomerModal.hide();
selectCustomer(data.customer);
showToast('Customer created successfully', 'success');
} else {
showToast(data.error || 'Error creating customer', 'danger');
}
})
.catch(err => {
saveCustomerBtn.disabled = false;
saveCustomerBtn.textContent = 'Save Customer';
showToast('Network error', 'danger');
});
});
}
});

View File

@ -0,0 +1,44 @@
-- Create customers table if it doesn't exist
CREATE TABLE IF NOT EXISTS customers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
phone VARCHAR(20) UNIQUE,
email VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Add customer_id to orders table
SET @dbname = DATABASE();
SET @tablename = "orders";
SET @columnname = "customer_id";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE orders ADD COLUMN customer_id INT DEFAULT NULL AFTER order_type;"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
-- Add foreign key if it doesn't exist
SET @constraintname = "fk_orders_customer";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (constraint_name = @constraintname)
) > 0,
"SELECT 1",
"ALTER TABLE orders ADD CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE SET NULL;"
));
PREPARE addConstraint FROM @preparedStatement;
EXECUTE addConstraint;
DEALLOCATE PREPARE addConstraint;

View File

@ -0,0 +1,15 @@
-- Add payment_type_id to orders table
ALTER TABLE orders ADD COLUMN payment_type_id INT DEFAULT NULL;
-- Seed default payment types if table is empty
INSERT INTO payment_types (name, type, is_active)
SELECT * FROM (SELECT 'Cash', 'cash', 1) AS tmp
WHERE NOT EXISTS (
SELECT name FROM payment_types WHERE name = 'Cash'
) LIMIT 1;
INSERT INTO payment_types (name, type, is_active)
SELECT * FROM (SELECT 'Credit Card', 'card', 1) AS tmp
WHERE NOT EXISTS (
SELECT name FROM payment_types WHERE name = 'Credit Card'
) LIMIT 1;

View File

@ -0,0 +1,11 @@
INSERT INTO payment_types (name, type, is_active)
SELECT * FROM (SELECT 'Cash' as n, 'cash' as t, 1 as a) AS tmp
WHERE NOT EXISTS (
SELECT name FROM payment_types WHERE name = 'Cash'
) LIMIT 1;
INSERT INTO payment_types (name, type, is_active)
SELECT * FROM (SELECT 'Credit Card' as n, 'card' as t, 1 as a) AS tmp
WHERE NOT EXISTS (
SELECT name FROM payment_types WHERE name = 'Credit Card'
) LIMIT 1;

View File

@ -0,0 +1,28 @@
-- Add points column to customers table if it doesn't exist
SET @dbname = DATABASE();
SET @tablename = "customers";
SET @columnname = "points";
SET @preparedStatement = (SELECT IF(
(
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
WHERE
(table_name = @tablename)
AND (table_schema = @dbname)
AND (column_name = @columnname)
) > 0,
"SELECT 1",
"ALTER TABLE customers ADD COLUMN points INT DEFAULT 0;"
));
PREPARE alterIfNotExists FROM @preparedStatement;
EXECUTE alterIfNotExists;
DEALLOCATE PREPARE alterIfNotExists;
-- Create loyalty_settings table
CREATE TABLE IF NOT EXISTS loyalty_settings (
id INT PRIMARY KEY,
points_per_order INT DEFAULT 10,
points_for_free_meal INT DEFAULT 70
);
-- Seed default settings
INSERT IGNORE INTO loyalty_settings (id, points_per_order, points_for_free_meal) VALUES (1, 10, 70);

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS integration_settings (
id INT AUTO_INCREMENT PRIMARY KEY,
provider VARCHAR(50) NOT NULL,
setting_key VARCHAR(100) NOT NULL,
setting_value TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_provider_key (provider, setting_key)
);

View File

@ -1,3 +1,9 @@
<?php
require_once __DIR__ . '/db/config.php';
$pdo = db();
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll(PDO::FETCH_ASSOC);
$current_outlet_id = isset($_GET['outlet_id']) ? (int)$_GET['outlet_id'] : 1;
?>
<!DOCTYPE html>
<html lang="en">
<head>
@ -50,7 +56,14 @@
<div class="container-fluid">
<div class="d-flex justify-content-between align-items-center py-3 px-4 bg-white shadow-sm mb-4">
<h2 class="h4 mb-0">Kitchen Display</h2>
<div>
<div class="d-flex align-items-center">
<select id="outlet-selector" class="form-select form-select-sm me-3" style="width: auto;">
<?php foreach ($outlets as $outlet): ?>
<option value="<?= $outlet['id'] ?>" <?= $current_outlet_id == $outlet['id'] ? 'selected' : '' ?>>
<?= htmlspecialchars($outlet['name']) ?>
</option>
<?php endforeach; ?>
</select>
<span id="clock" class="text-muted me-3"></span>
<a href="index.php" class="btn btn-outline-secondary btn-sm">Back to Home</a>
</div>
@ -65,9 +78,11 @@
</div>
<script>
const OUTLET_ID = <?= $current_outlet_id ?>;
async function fetchOrders() {
try {
const response = await fetch('api/kitchen.php');
const response = await fetch('api/kitchen.php?outlet_id=' + OUTLET_ID);
if (!response.ok) throw new Error('Network response was not ok');
const orders = await response.json();
renderOrders(orders);
@ -159,6 +174,11 @@ async function updateStatus(orderId, newStatus) {
}
}
// Outlet Selector Logic
document.getElementById('outlet-selector').addEventListener('change', function() {
window.location.href = '?outlet_id=' + this.value;
});
// Clock
setInterval(() => {
document.getElementById('clock').textContent = new Date().toLocaleTimeString();

64
pos.php
View File

@ -6,6 +6,7 @@ $pdo = db();
$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();
$outlets = $pdo->query("SELECT * FROM outlets ORDER BY name")->fetchAll();
$payment_types = $pdo->query("SELECT * FROM payment_types WHERE is_active = 1 ORDER BY id")->fetchAll();
// Fetch variants
$variants_raw = $pdo->query("SELECT * FROM product_variants ORDER BY price_adjustment ASC")->fetchAll();
@ -46,6 +47,7 @@ $order_type = $_GET['order_type'] ?? 'dine-in';
.category-btn:hover { background-color: #e9ecef; }
.category-btn.active { background-color: #0d6efd; color: white; }
.search-dropdown { position: absolute; width: 100%; z-index: 1000; max-height: 200px; overflow-y: auto; display: none; }
.payment-btn { height: 80px; font-size: 1.2rem; font-weight: bold; }
</style>
</head>
<body>
@ -176,12 +178,30 @@ $order_type = $_GET['order_type'] ?? 'dine-in';
<span class="input-group-text bg-white border-end-0"><i class="bi bi-person"></i></span>
<input type="text" class="form-control border-start-0 ps-0" id="customer-search" placeholder="Search Customer..." autocomplete="off">
<button class="btn btn-outline-secondary d-none" type="button" id="clear-customer"><i class="bi bi-x"></i></button>
<button class="btn btn-outline-primary" type="button" id="add-customer-btn" title="Add New Customer">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<div class="list-group shadow-sm search-dropdown" id="customer-results"></div>
<input type="hidden" id="selected-customer-id">
<div id="customer-info" class="small text-success mt-1 d-none">
<i class="bi bi-check-circle-fill me-1"></i> <span id="customer-name-display"></span>
</div>
<!-- Loyalty Section -->
<div id="loyalty-section" class="d-none mt-2 p-2 bg-warning-subtle rounded border border-warning">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="d-block fw-bold small text-warning-emphasis">Loyalty Points</span>
<span id="loyalty-points-display" class="fw-bold fs-5">0</span>
</div>
<button id="redeem-loyalty-btn" class="btn btn-sm btn-success shadow-sm" disabled>
<i class="bi bi-gift-fill me-1"></i> Redeem Meal
</button>
</div>
<div id="loyalty-message" class="small text-muted mt-1 fst-italic" style="font-size: 0.75rem;"></div>
</div>
</div>
</div>
@ -258,9 +278,53 @@ $order_type = $_GET['order_type'] ?? 'dine-in';
</div>
</div>
<!-- Add Customer Modal -->
<div class="modal fade" id="addCustomerModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fs-6 fw-bold">Add New Customer</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="new-customer-name" class="form-label small text-muted">Customer Name</label>
<input type="text" class="form-control form-control-sm" id="new-customer-name" placeholder="Enter name">
</div>
<div class="mb-3">
<label for="new-customer-phone" class="form-label small text-muted">Phone Number (8 Digits)</label>
<input type="text" class="form-control form-control-sm" id="new-customer-phone" placeholder="12345678" maxlength="8">
<div class="form-text text-danger d-none" id="phone-error">Must be exactly 8 digits.</div>
</div>
<div class="d-grid">
<button type="button" class="btn btn-primary btn-sm" id="save-new-customer">Save Customer</button>
</div>
</div>
</div>
</div>
</div>
<!-- Payment Selection Modal -->
<div class="modal fade" id="paymentSelectionModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold">Select Payment Method</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row g-3" id="payment-methods-container">
<!-- Injected via JS -->
</div>
</div>
</div>
</div>
</div>
<script>
const COMPANY_SETTINGS = <?= json_encode($settings) ?>;
const PRODUCT_VARIANTS = <?= json_encode($variants_by_product) ?>;
const PAYMENT_TYPES = <?= json_encode($payment_types) ?>;
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="assets/js/main.js?v=<?= time() ?>"></script>