File manager - Edit - /home/webapp69.cm.in.th/u69319090029/Project SME/menu.php
Back
<?php header('Content-Type: text/html; charset=utf-8'); /** * CMTC Tech Solution - Customer Menu & Ordering (Self-Service Enterprise UI) * Theme: Private CMTC Tech Solution Theme */ require_once 'db.php'; if (session_status() === PHP_SESSION_NONE) { session_start(); } // 1. Extract Store & Table Parameters $raw_store_id = $_REQUEST['store_id'] ?? ($_REQUEST['shop_id'] ?? ''); $is_demo_mode = ( $raw_store_id === 'DEMO_STORE' || (!empty($_REQUEST['is_demo']) && $_REQUEST['is_demo'] == 1) || (!empty($_GET['demo']) && $_GET['demo'] == 1) ); // If scanning a Demo QR code, isolate session exclusively to DEMO_STORE sandbox if ($is_demo_mode) { $_SESSION['scanned_store_id'] = 'DEMO_STORE'; $_SESSION['is_demo_mode'] = true; } elseif (!empty($raw_store_id) && is_numeric($raw_store_id)) { // If scanning a real shop QR code, bind session strictly to the scanned store_id and clear demo flag $_SESSION['scanned_store_id'] = (int)$raw_store_id; unset($_SESSION['is_demo_mode']); } // Resolve active store context from session lock $session_store = $_SESSION['scanned_store_id'] ?? ''; if (!empty($_SESSION['is_demo_mode']) || $session_store === 'DEMO_STORE') { $is_demo_mode = true; $store_id = 0; } elseif (!empty($session_store) && is_numeric($session_store)) { $store_id = (int)$session_store; } else { $store_id = 0; } if (!empty($_GET['table'])) { $_SESSION['scanned_table'] = trim($_GET['table']); } elseif (!empty($_POST['table_number'])) { $_SESSION['scanned_table'] = trim($_POST['table_number']); } if (!empty($_GET['token'])) { $_SESSION['scanned_token'] = trim($_GET['token']); } if (!empty($_GET['shop_name'])) { $_SESSION['scanned_shop_name'] = trim($_GET['shop_name']); } $table_param = $_GET['table'] ?? ($_POST['table_number'] ?? ($_SESSION['scanned_table'] ?? '')); $token_param = $_GET['token'] ?? ($_SESSION['scanned_token'] ?? ''); $curr_shop_name = $_GET['shop_name'] ?? ($_SESSION['scanned_shop_name'] ?? ''); // 3. Customer Navigation Scope (Strictly bound to scanned store & table context) if ($is_demo_mode) { $user_role_home = "menu.php?store_id=DEMO_STORE&is_demo=1&table=" . urlencode($table_param ?: '1'); } elseif ($store_id > 0) { $user_role_home = "menu.php?store_id=" . $store_id . (!empty($curr_shop_name) ? "&shop_name=" . urlencode($curr_shop_name) : "") . (!empty($token_param) ? "&token=" . urlencode($token_param) : "") . (!empty($table_param) ? "&table=" . urlencode($table_param) : ""); } elseif (!empty($_SESSION['super_admin_logged_in'])) { $user_role_home = 'platform-admin.php'; } elseif (!empty($_SESSION['store_id']) || !empty($_SESSION['shop_admin_id'])) { $user_role_home = 'store-admin.php'; } else { $user_role_home = 'javascript:void(0)'; } $success_msg = ''; $error_msg = ''; // 4. JSON API for order status check if (isset($_GET['check_order_status'])) { if (ob_get_length()) ob_clean(); header('Content-Type: application/json; charset=utf-8'); $order_id = $_GET['check_order_status']; if (strpos($order_id, 'DEMO-') === 0) { echo json_encode([ 'success' => true, 'status' => 'preparing', 'table_number' => $table_param ?: '1' ]); exit; } try { $stmt = $pdo->prepare("SELECT id, table_number, status FROM orders WHERE id = :id"); $stmt->execute([':id' => (int)$order_id]); $order = $stmt->fetch(); if ($order) { echo json_encode([ 'success' => true, 'status' => $order['status'], 'table_number' => $order['table_number'] ]); } else { echo json_encode(['success' => false, 'message' => 'ไม่พบข้อมูลออเดอร์']); } } catch (Exception $e) { echo json_encode(['success' => false, 'message' => $e->getMessage()]); } exit; } $is_expired_session = false; if ($store_id > 0 && !empty($table_param)) { try { $num_only = preg_replace('/[^0-9]/', '', $table_param); if (empty($num_only)) $num_only = $table_param; $t1 = "Table " . $num_only; $t2 = $num_only; $t3 = $table_param; $stmt_t = $pdo->prepare("SELECT qr_token FROM tables_qr WHERE store_id = :store_id AND (table_number = :t1 OR table_number = :t2 OR table_number = :t3)"); $stmt_t->execute([':store_id' => $store_id, ':t1' => $t1, ':t2' => $t2, ':t3' => $t3]); $table_row = $stmt_t->fetch(); if ($table_row && !empty($table_row['qr_token'])) { if (!empty($token_param) && $token_param !== $table_row['qr_token']) { $is_expired_session = true; } } } catch (Exception $e) {} } // Fetch active order for the table (only status in 'pending', 'preparing', 'ready') $active_table_order_id = null; if ($store_id > 0 && !empty($table_param) && !$is_expired_session) { try { $num_only = preg_replace('/[^0-9]/', '', $table_param); if (empty($num_only)) $num_only = $table_param; $t1 = "Table " . $num_only; $t2 = $num_only; $t3 = $table_param; $stmt_active_ord = $pdo->prepare("SELECT id FROM orders WHERE store_id = :store_id AND (table_number = :t1 OR table_number = :t2 OR table_number = :t3) AND status IN ('pending', 'preparing', 'ready') ORDER BY id DESC LIMIT 1"); $stmt_active_ord->execute([':store_id' => $store_id, ':t1' => $t1, ':t2' => $t2, ':t3' => $t3]); $active_ord = $stmt_active_ord->fetch(); if ($active_ord) { $active_table_order_id = (int)$active_ord['id']; } } catch (Exception $e) {} } // 1.5 Handle Customer Payment Verification & Slip Upload (via AJAX POST) if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['verify_payment'])) { if (ob_get_length()) ob_clean(); header('Content-Type: application/json; charset=utf-8'); $target_order_id = intval($_POST['order_id'] ?? 0); $pay_store_id = intval($_POST['store_id'] ?? ($store_id ?: ($_SESSION['scanned_store_id'] ?? 0))); if ($target_order_id <= 0) { echo json_encode(['success' => false, 'message' => 'ไม่พบหมายเลขออเดอร์']); exit; } try { $stmt_chk = $pdo->prepare("SELECT id, store_id, status FROM orders WHERE id = :id AND store_id = :store_id"); $stmt_chk->execute([':id' => $target_order_id, ':store_id' => $pay_store_id]); $ord_data = $stmt_chk->fetch(PDO::FETCH_ASSOC); if (!$ord_data) { echo json_encode(['success' => false, 'message' => 'ไม่พบข้อมูลคำสั่งซื้อในระบบ']); exit; } // Handle Payment Slip File Upload if attached $slip_url = ''; if (isset($_FILES['slip_file']) && $_FILES['slip_file']['error'] === UPLOAD_ERR_OK) { $upload_dir = __DIR__ . '/uploads/slips/'; if (!is_dir($upload_dir)) @mkdir($upload_dir, 0777, true); $ext = strtolower(pathinfo($_FILES['slip_file']['name'], PATHINFO_EXTENSION)); if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'jfif', 'avif', 'svg', 'bmp', 'heic'])) { $slip_filename = 'slip_' . $target_order_id . '_' . time() . '.' . $ext; $target_dest = $upload_dir . $slip_filename; if (function_exists('processAndCompressImage') && processAndCompressImage($_FILES['slip_file']['tmp_name'], $target_dest, 1200, 80)) { $slip_url = 'uploads/slips/' . $slip_filename; } elseif (@move_uploaded_file($_FILES['slip_file']['tmp_name'], $target_dest)) { $slip_url = 'uploads/slips/' . $slip_filename; } } } // Transition order status to 'pending', payment_method = 'transfer', is_paid = 1 if (!empty($slip_url)) { $stmt_up = $pdo->prepare("UPDATE orders SET status = 'pending', payment_method = 'transfer', is_paid = 1, slip_url = :slip_url WHERE id = :id AND store_id = :store_id"); $stmt_up->execute([':slip_url' => $slip_url, ':id' => $target_order_id, ':store_id' => $pay_store_id]); } else { $stmt_up = $pdo->prepare("UPDATE orders SET status = 'pending', payment_method = 'transfer', is_paid = 1 WHERE id = :id AND store_id = :store_id"); $stmt_up->execute([':id' => $target_order_id, ':store_id' => $pay_store_id]); } echo json_encode([ 'success' => true, 'message' => 'ยืนยันการชำระเงินเรียบร้อยแล้ว! ออเดอร์ของคุณส่งเข้าห้องครัวแล้ว', 'order_id' => $target_order_id, 'status' => 'pending' ]); exit; } catch (Exception $e) { echo json_encode(['success' => false, 'message' => 'เกิดข้อผิดพลาดในการยืนยันชำระเงิน: ' . $e->getMessage()]); exit; } } // 1.5 Handle Cash Payment Confirmation if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['confirm_cash_payment'])) { if (ob_get_length()) ob_clean(); header('Content-Type: application/json; charset=utf-8'); $target_order_id = intval($_POST['order_id'] ?? 0); $pay_store_id = intval($_POST['store_id'] ?? 0); if ($target_order_id > 0) { $stmt_up = $pdo->prepare("UPDATE orders SET status = 'pending', payment_method = 'cash', is_paid = 0 WHERE id = :id AND store_id = :store_id"); $stmt_up->execute([':id' => $target_order_id, ':store_id' => $pay_store_id]); echo json_encode(['success' => true, 'message' => 'ยืนยันสั่งชำระเงินสด ออเดอร์ส่งเข้าครัวเรียบร้อยแล้ว', 'order_id' => $target_order_id, 'status' => 'pending']); exit; } } // 2. Handle Customer Order Submission (via AJAX POST) if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['place_order'])) { if (ob_get_length()) ob_clean(); header('Content-Type: application/json; charset=utf-8'); if ($store_id <= 0) { $store_id = !empty($_POST['store_id']) ? (int)$_POST['store_id'] : (!empty($_SESSION['scanned_store_id']) ? (int)$_SESSION['scanned_store_id'] : 0); } $table_number = trim($_POST['table_number'] ?? ''); $cart_json = $_POST['cart_data'] ?? '[]'; $cart_items = json_decode($cart_json, true); // Session-Based Rate Limiting & Cooldown Protection (Anti-Spam) $cooldown_seconds = 15; $last_order_ts = (int)($_SESSION['last_order_timestamp'] ?? 0); $time_diff = time() - $last_order_ts; if ($last_order_ts > 0 && $time_diff < $cooldown_seconds) { $remaining = $cooldown_seconds - $time_diff; http_response_code(429); echo json_encode([ 'success' => false, 'rate_limited' => true, 'remaining_seconds' => $remaining, 'message' => "กรุณารอสักครู่ ({$remaining} วินาที) ก่อนทำการสั่งอาหารเพิ่ม" ]); exit; } // Fallback store_id to Store 1 if not specified so orders are always recorded in database if ($store_id <= 0) { $store_id = 1; } if (empty($table_number)) { $table_number = !empty($_SESSION['scanned_table']) ? $_SESSION['scanned_table'] : '1'; } if ($is_expired_session) { echo json_encode(['success' => false, 'message' => 'โต๊ะนี้ทำการเช็กบิลเรียบร้อยแล้ว หรือรหัสสแกนไม่ถูกต้อง กรุณาสแกน QR Code ประจำโต๊ะอาหารอีกครั้งเพื่อสั่งซื้อ']); exit; } elseif (empty($cart_items)) { echo json_encode(['success' => false, 'message' => 'กรุณาเลือกอาหารอย่างน้อย 1 รายการก่อนสั่งซื้อ']); exit; } else { try { // Begin Transaction for Data Integrity $pdo->beginTransaction(); // 1. Insert order record (PromptPay QR food payment orders set payment_method='transfer' and is_paid=1) $payment_method = trim($_POST['payment_method'] ?? 'transfer'); $is_paid = ($payment_method === 'transfer') ? 1 : 0; $raw_voucher = trim($_POST['voucher'] ?? ''); $voucher = !empty($raw_voucher) ? substr($raw_voucher, 0, 50) : null; $stmt = $pdo->prepare("INSERT INTO orders (store_id, table_number, status, payment_method, is_paid, voucher) VALUES (:store_id, :table_number, 'unpaid', :payment_method, :is_paid, :voucher)"); $stmt->execute([ ':store_id' => $store_id, ':table_number' => $table_number, ':payment_method' => $payment_method, ':is_paid' => $is_paid, ':voucher' => $voucher ]); $order_id = $pdo->lastInsertId(); // 2. Prepare statements for items insert and menu validation $stmt_insert_item = $pdo->prepare("INSERT INTO order_items (order_id, menu_id, quantity, price, note, spice_level) VALUES (:order_id, :menu_id, :quantity, :price, :note, :spice_level)"); $stmt_check_prod = $pdo->prepare("SELECT price, is_available FROM menus WHERE menu_id = :id AND store_id = :store_id"); $grand_total = 0.0; foreach ($cart_items as $item) { $menu_id = (int)$item['id']; $qty = (int)$item['qty']; $note = trim($item['note'] ?? ''); $spice_level = trim($item['spice_level'] ?? ''); if ($qty <= 0) continue; $stmt_check_prod->execute([':id' => $menu_id, ':store_id' => $store_id]); $product = $stmt_check_prod->fetch(PDO::FETCH_ASSOC); $price = $product ? (float)$product['price'] : (float)($item['price'] ?? 0); $grand_total += ($price * $qty); $stmt_insert_item->execute([ ':order_id' => $order_id, ':menu_id' => $menu_id, ':quantity' => $qty, ':price' => $price, ':note' => $note, ':spice_level' => $spice_level ]); } $pdo->commit(); $_SESSION['last_order_timestamp'] = time(); echo json_encode([ 'success' => true, 'message' => "สร้างรายการสั่งซื้อเรียบร้อยแล้ว กรุณายืนยันชำระเงินเพื่อส่งเข้าห้องครัว", 'order_id' => $order_id, 'table_number' => $table_number, 'status' => 'unpaid', 'total_amount' => $grand_total, 'store_id' => $store_id, 'remaining_seconds' => $cooldown_seconds ]); exit; } catch (Exception $e) { $pdo->rollBack(); echo json_encode(['success' => false, 'message' => "เกิดข้อผิดพลาดในการบันทึกข้อมูล: " . $e->getMessage()]); exit; } } } $shop_info = null; $active_shops = []; if ($is_demo_mode || $raw_store_id === 'DEMO_STORE') { $shop_info = [ 'id' => 'DEMO_STORE', 'name' => 'ร้านค้า CMTC Smart Dining', 'address' => 'ศูนย์ทดลองระบบ CMTC Tech Solution', 'category' => 'ทดลองระบบสั่งอาหาร', 'promo_banner' => '', 'policy_text' => '⚠️ นี่คือโหมดทดลองใช้งาน ออเดอร์ของคุณจะไม่ถูกบันทึกเข้าร้านค้าจริง', 'custom_logo_url' => '' ]; try { $stmt = $pdo->query("SELECT menu_id as id, name, price, category, image_url, is_available, unit FROM menus WHERE is_available = 1 LIMIT 12"); $products = $stmt ? $stmt->fetchAll() : []; } catch (PDOException $e) { $products = []; } if (empty($products)) { $products = [ ['id' => 901, 'name' => 'ผัดกะเพราหมูกรอบ (Demo)', 'price' => 60, 'category' => 'อาหารจานเดียว', 'image_url' => '', 'is_available' => 1], ['id' => 902, 'name' => 'ข้าวผัดต้มยำกุ้ง (Demo)', 'price' => 70, 'category' => 'อาหารจานเดียว', 'image_url' => '', 'is_available' => 1], ['id' => 903, 'name' => 'ชาไทยเย็น (Demo)', 'price' => 35, 'category' => 'เครื่องดื่ม', 'image_url' => '', 'is_available' => 1] ]; } } elseif ($store_id > 0) { try { $stmt_shop = $pdo->prepare("SELECT store_id as id, store_name as name, address, category, phone, line_id, promo_banner, policy_text, custom_logo_url, store_banner_url FROM tenants WHERE store_id = :id AND status = 'active'"); $stmt_shop->execute([':id' => $store_id]); $shop_info = $stmt_shop->fetch(PDO::FETCH_ASSOC); if ($shop_info) { // Fetch categories in custom sort order $sorted_cat_order = []; try { $stmt_c = $pdo->prepare("SELECT name FROM categories WHERE store_id = :store_id AND is_active = 1 ORDER BY sort_order ASC, id ASC"); $stmt_c->execute([':store_id' => $store_id]); $sorted_cat_names = $stmt_c->fetchAll(PDO::FETCH_COLUMN); foreach ($sorted_cat_names as $idx => $cname) { $sorted_cat_order[$cname] = $idx; } } catch (Exception $e) {} $stmt = $pdo->prepare("SELECT menu_id as id, name, price, category, description, image_url, is_available, spice_options, sort_order, unit FROM menus WHERE store_id = :store_id AND is_available = 1 ORDER BY sort_order ASC, menu_id DESC"); $stmt->execute([':store_id' => $store_id]); $raw_products = $stmt->fetchAll(PDO::FETCH_ASSOC); $products = []; foreach ($raw_products as $rp) { $products[] = [ 'id' => (int)$rp['id'], 'name' => (string)$rp['name'], 'price' => (float)$rp['price'], 'category' => (string)($rp['category'] ?? 'ทั่วไป'), 'description' => (string)($rp['description'] ?? ''), 'image_url' => (string)($rp['image_url'] ?? ''), 'is_available' => (int)$rp['is_available'], 'spice_options' => (string)($rp['spice_options'] ?? 'เผ็ดปกติ,ไม่เผ็ด,เผ็ดน้อย,เผ็ดมาก'), 'sort_order' => (int)($rp['sort_order'] ?? 0), 'unit' => (string)($rp['unit'] ?? '') ]; } // Sort products by category sort order if custom categories exist if (!empty($sorted_cat_order)) { usort($products, function($a, $b) use ($sorted_cat_order) { $catOrderA = $sorted_cat_order[$a['category']] ?? 9999; $catOrderB = $sorted_cat_order[$b['category']] ?? 9999; if ($catOrderA !== $catOrderB) { return $catOrderA <=> $catOrderB; } if ($a['sort_order'] !== $b['sort_order']) { return $a['sort_order'] <=> $b['sort_order']; } return $b['id'] <=> $a['id']; }); } } else { $products = []; } } catch (PDOException $e) { $products = []; $error_msg = "ไม่สามารถเชื่อมต่อฐานข้อมูลได้: " . $e->getMessage(); } } else { // Fetch all active shops to display a shop directory try { $stmt_active = $pdo->query("SELECT store_id as id, store_name as name, category, address, custom_logo_url FROM tenants WHERE status = 'active' ORDER BY store_id DESC"); $active_shops = $stmt_active->fetchAll(); $products = []; } catch (PDOException $e) { $products = []; $error_msg = "ไม่สามารถดึงข้อมูลร้านค้าได้: " . $e->getMessage(); } } // ============================================================ // GUEST AUTHENTICATOR GATE (Static QR + Table Session Logic) // ============================================================ $guest_session_key = 'guest_tbl_' . $store_id . '_' . $table_param; $needs_auth = false; $auth_table_info = null; $auth_error = ''; if (!$is_demo_mode && $store_id > 0 && !empty($table_param) && $shop_info) { // --- POST: Guest submits their name --- if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['guest_auth_submit'])) { $guest_name = trim($_POST['guest_name'] ?? ''); $submitted_store = (int)($_POST['store_id'] ?? 0); $submitted_table = trim($_POST['table_number'] ?? ''); if (empty($guest_name) || mb_strlen($guest_name) < 1) { $auth_error = 'กรุณากรอกชื่อของคุณ'; $needs_auth = true; } elseif ($submitted_store !== $store_id || $submitted_table !== $table_param) { $auth_error = 'ข้อมูลโต๊ะไม่ถูกต้อง กรุณาสแกน QR ใหม่'; $needs_auth = true; } else { // Truncate name for safety $guest_name = mb_substr($guest_name, 0, 60); $new_sess_token = bin2hex(random_bytes(16)); try { $num_only = preg_replace('/[^0-9]/', '', $table_param); if (empty($num_only)) $num_only = $table_param; $stmt_occ = $pdo->prepare( "UPDATE tables_qr SET table_status = 'occupied', table_guest_name = :gname, session_token = :stoken " . "WHERE store_id = :sid AND (table_number = :t1 OR table_number = :t2 OR table_number = :t3)" ); $stmt_occ->execute([ ':gname' => $guest_name, ':stoken' => $new_sess_token, ':sid' => $store_id, ':t1' => 'Table ' . $num_only, ':t2' => $num_only, ':t3' => $table_param, ]); // Bind this device/browser to the table via session $_SESSION[$guest_session_key] = $new_sess_token; $_SESSION['guest_name_' . $store_id . '_' . $table_param] = $guest_name; // Redirect to clean GET URL to avoid form resubmission on refresh $redirect_url = 'menu.php?store_id=' . $store_id . '&table=' . urlencode($table_param); if (!empty($_SESSION['scanned_shop_name'])) { $redirect_url .= '&shop_name=' . urlencode($_SESSION['scanned_shop_name']); } header('Location: ' . $redirect_url); exit; } catch (Exception $e) { $auth_error = 'เกิดข้อผิดพลาด กรุณาลองใหม่'; $needs_auth = true; } } } if (!$needs_auth) { // --- GET: Check if this device already has a bound session --- $device_token = $_SESSION[$guest_session_key] ?? ''; // Fetch current table status from DB try { $num_only = preg_replace('/[^0-9]/', '', $table_param); if (empty($num_only)) $num_only = $table_param; $stmt_tbl_chk = $pdo->prepare( "SELECT table_status, session_token, table_guest_name FROM tables_qr " . "WHERE store_id = :sid AND (table_number = :t1 OR table_number = :t2 OR table_number = :t3) LIMIT 1" ); $stmt_tbl_chk->execute([ ':sid' => $store_id, ':t1' => 'Table ' . $num_only, ':t2' => $num_only, ':t3' => $table_param, ]); $auth_table_info = $stmt_tbl_chk->fetch(PDO::FETCH_ASSOC); } catch (Exception $e) { $auth_table_info = null; } $tbl_status = $auth_table_info['table_status'] ?? 'available'; $db_sess_token = $auth_table_info['session_token'] ?? ''; if ($tbl_status === 'available') { // Table is free — show authenticator $needs_auth = true; } elseif ($tbl_status === 'occupied') { if (!empty($device_token) && !empty($db_sess_token) && hash_equals($db_sess_token, $device_token)) { // Same device that checked in — allow through $needs_auth = false; } else { // Different device or session expired — show authenticator // (They will re-bind this device to the same table session) if (!empty($db_sess_token)) { // Table already has a guest; allow secondary devices to join by entering same name $needs_auth = true; } else { $needs_auth = true; } } } } } // ============================================================ // RENDER AUTHENTICATOR SCREEN (if gate is needed) // ============================================================ if ($needs_auth) { $auth_table_display = !empty($table_param) ? 'โต๊ะ ' . htmlspecialchars($table_param) : 'โต๊ะอาหาร'; $auth_store_name = htmlspecialchars($shop_info['name'] ?? 'ร้านอาหาร'); $auth_store_logo = htmlspecialchars($shop_info['custom_logo_url'] ?? 'logo.png'); $auth_bg_banner = !empty($shop_info['store_banner_url']) ? htmlspecialchars($shop_info['store_banner_url']) : 'https://images.unsplash.com/photo-1414235077428-338989a2e8c0?w=1200&auto=format&fit=crop&q=80'; $existing_guest = htmlspecialchars($auth_table_info['table_guest_name'] ?? ''); ?> <!DOCTYPE html> <html lang="th"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title><?php echo $auth_store_name; ?> — ยืนยันตัวตน | CMTC Smart Dining</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700;800&family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> <style> *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: 'Sarabun', 'Inter', sans-serif; min-height: 100vh; background: #0f172a; display: flex; align-items: center; justify-content: center; position: relative; overflow: hidden; } .auth-bg { position: fixed; inset: 0; background: linear-gradient(160deg, rgba(15,23,42,0.92) 0%, rgba(15,23,42,0.85) 100%), url('<?php echo $auth_bg_banner; ?>') center/cover no-repeat; z-index: 0; } .auth-bg::after { content: ''; position: absolute; inset: 0; background: radial-gradient(ellipse at 30% 60%, rgba(0,229,255,0.07) 0%, transparent 60%), radial-gradient(ellipse at 80% 20%, rgba(168,85,247,0.06) 0%, transparent 50%); } .auth-card { position: relative; z-index: 10; background: rgba(15,23,42,0.85); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border: 1px solid rgba(0,229,255,0.18); border-radius: 28px; padding: 44px 40px 40px; width: 100%; max-width: 440px; margin: 20px; box-shadow: 0 32px 80px rgba(0,0,0,0.5), 0 0 0 1px rgba(255,255,255,0.04) inset; animation: slideUp 0.45s cubic-bezier(.22,.68,0,1.2) both; } @keyframes slideUp { from { opacity:0; transform: translateY(32px) scale(0.97); } to { opacity:1; transform: translateY(0) scale(1); } } .auth-logo-wrap { display: flex; align-items: center; justify-content: center; margin-bottom: 24px; } .auth-logo { width: 80px; height: 80px; border-radius: 22px; background: rgba(255,255,255,0.96); padding: 8px; box-shadow: 0 10px 32px rgba(0,229,255,0.25), 0 0 0 2px rgba(0,229,255,0.35); object-fit: contain; } .auth-welcome { text-align: center; margin-bottom: 28px; } .auth-welcome .table-badge { display: inline-flex; align-items: center; gap: 8px; background: linear-gradient(135deg, rgba(0,229,255,0.15), rgba(168,85,247,0.12)); border: 1px solid rgba(0,229,255,0.3); color: #00E5FF; font-size: 13px; font-weight: 700; padding: 6px 18px; border-radius: 50px; letter-spacing: 0.5px; margin-bottom: 14px; } .auth-welcome h1 { font-size: 24px; font-weight: 800; color: #f1f5f9; line-height: 1.3; margin-bottom: 8px; } .auth-welcome h1 span { color: #00E5FF; } .auth-welcome p { font-size: 14px; color: #94a3b8; line-height: 1.6; } <?php if ($existing_guest): ?> .auth-occupied-notice { background: rgba(16,185,129,0.1); border: 1px solid rgba(16,185,129,0.3); border-radius: 12px; padding: 12px 16px; font-size: 13px; color: #6ee7b7; margin-bottom: 18px; text-align: center; } <?php endif; ?> .form-group { margin-bottom: 20px; } .form-label { display: block; font-size: 13px; font-weight: 600; color: #cbd5e1; margin-bottom: 8px; letter-spacing: 0.3px; } .form-input { width: 100%; padding: 14px 18px; background: rgba(30,41,59,0.8); border: 1.5px solid rgba(0,229,255,0.2); border-radius: 14px; color: #f1f5f9; font-size: 16px; font-family: inherit; transition: border-color 0.2s, box-shadow 0.2s; outline: none; } .form-input::placeholder { color: #475569; } .form-input:focus { border-color: #00E5FF; box-shadow: 0 0 0 3px rgba(0,229,255,0.12); } .btn-auth { width: 100%; padding: 15px; background: linear-gradient(135deg, #00E5FF, #0ea5e9); color: #0f172a; font-size: 16px; font-weight: 800; font-family: inherit; border: none; border-radius: 14px; cursor: pointer; transition: transform 0.15s, box-shadow 0.2s, opacity 0.2s; box-shadow: 0 8px 24px rgba(0,229,255,0.3); display: flex; align-items: center; justify-content: center; gap: 8px; } .btn-auth:hover { transform: translateY(-2px); box-shadow: 0 12px 32px rgba(0,229,255,0.4); } .btn-auth:active { transform: translateY(0); opacity: 0.9; } .auth-footer { text-align: center; margin-top: 20px; font-size: 12px; color: #475569; line-height: 1.6; } .auth-error { background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.3); border-radius: 10px; padding: 10px 14px; color: #fca5a5; font-size: 13px; margin-bottom: 16px; text-align: center; animation: shake 0.35s ease; } @keyframes shake { 0%,100% { transform: translateX(0); } 20%,60% { transform: translateX(-6px); } 40%,80% { transform: translateX(6px); } } .floating-orb { position: fixed; border-radius: 50%; filter: blur(80px); pointer-events: none; z-index: 1; animation: orbFloat 8s ease-in-out infinite alternate; } @keyframes orbFloat { from { transform: translate(0, 0); } to { transform: translate(20px, -30px); } } </style> </head> <body> <div class="auth-bg"></div> <!-- Ambient floating orbs --> <div class="floating-orb" style="width:350px;height:350px;background:rgba(0,229,255,0.05);top:-80px;right:-80px;"></div> <div class="floating-orb" style="width:280px;height:280px;background:rgba(168,85,247,0.05);bottom:-60px;left:-60px;animation-delay:-4s;"></div> <div class="auth-card"> <div class="auth-logo-wrap"> <img src="<?php echo $auth_store_logo; ?>" alt="Logo" class="auth-logo" onerror="this.src='logo.png'"> </div> <div class="auth-welcome"> <div class="table-badge">🪑 <?php echo $auth_table_display; ?></div> <h1>ยินดีต้อนรับสู่<br><span><?php echo $auth_store_name; ?></span></h1> <p>กรุณากรอกชื่อของคุณเพื่อเริ่มสั่งอาหาร</p> </div> <?php if (!empty($auth_error)): ?> <div class="auth-error">⚠️ <?php echo htmlspecialchars($auth_error); ?></div> <?php endif; ?> <?php if ($existing_guest): ?> <div class="auth-occupied-notice"> 🟢 โต๊ะนี้มีผู้ใช้งานอยู่แล้ว (<?php echo $existing_guest; ?>)<br> <small>กรอกชื่อของคุณเพื่อเชื่อมต่ออุปกรณ์นี้กับโต๊ะเดิม</small> </div> <?php endif; ?> <form method="POST" action="menu.php?store_id=<?php echo $store_id; ?>&table=<?php echo urlencode($table_param); ?>" id="guestAuthForm"> <input type="hidden" name="guest_auth_submit" value="1"> <input type="hidden" name="store_id" value="<?php echo $store_id; ?>"> <input type="hidden" name="table_number" value="<?php echo htmlspecialchars($table_param); ?>"> <div class="form-group"> <label class="form-label" for="guest_name">ชื่อของคุณ *</label> <input type="text" id="guest_name" name="guest_name" class="form-input" placeholder="เช่น สมชาย, นิดา, คุณแม่..." maxlength="60" required autocomplete="given-name" autofocus value="<?php echo htmlspecialchars($_POST['guest_name'] ?? ''); ?>" > </div> <button type="submit" class="btn-auth" id="authSubmitBtn"> <span>🍽️</span> เริ่มสั่งอาหาร </button> </form> <div class="auth-footer"> 🔒 ข้อมูลของคุณถูกเก็บไว้เพื่อประสบการณ์การสั่งอาหารเท่านั้น<br> CMTC Tech Solution — Smart Dining System </div> </div> <script> // Auto-focus on name input document.getElementById('guest_name')?.focus(); // Unlock Web Audio API context on first user interaction on authenticator screen document.addEventListener('click', function unlockAudioOnGuestAuth() { try { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (AudioCtx) { const tempCtx = new AudioCtx(); if (tempCtx.state === 'suspended') { tempCtx.resume(); } } } catch(e) {} document.removeEventListener('click', unlockAudioOnGuestAuth); }, { once: true }); // Loading state on submit document.getElementById('guestAuthForm')?.addEventListener('submit', function() { const btn = document.getElementById('authSubmitBtn'); if (btn) { btn.disabled = true; btn.innerHTML = '<span style="animation:spin 0.8s linear infinite;display:inline-block">⟳</span> กำลังเข้าสู่ระบบ...'; } }); </script> <style>@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}</style> </body> </html> <?php exit; } // Store guest name in a convenient variable for use in menu UI $current_guest_name = $_SESSION['guest_name_' . $store_id . '_' . $table_param] ?? ''; ?> <!DOCTYPE html> <html lang="th"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>CMTC Tech Solution - เมนูสั่งอาหาร</title> <link rel="stylesheet" href="style.css"> <!-- SweetAlert2 Library for Rich Commercial Popups --> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <!-- Navigation Sound & SweetAlert Engine --> <script src="js/nav_sound_swal.js"></script> </head> <body> <!-- Global Fixed Navigation Bar --> <nav class="global-navbar"> <a href="<?php echo htmlspecialchars($user_role_home); ?>" class="navbar-brand"> <img src="logo.png" class="navbar-logo" alt="Logo" style="width: 35px; height: 35px; object-fit: contain; border-radius: 50%;"> <span class="navbar-title">ระบบสั่งอาหาร • CMTC Tech Solution</span> </a> <div class="navbar-nav" style="display:flex; align-items:center; gap:8px;"> <button type="button" onclick="toggleNavSound()" class="nav-sound-toggle-btn" style="background: rgba(255,255,255,0.1); color: #94a3b8; border: 1px solid rgba(255,255,255,0.2); padding: 5px 10px; border-radius: 20px; font-weight: 800; font-size: 11px; cursor: pointer; white-space: nowrap;"> 🔊 เสียงนำทาง: ปิด </button> <a href="javascript:void(0)" onclick="handleGoBack(); return false;" class="btn btn-navbar-ghost">← ย้อนกลับ</a> <a href="<?php echo htmlspecialchars($user_role_home); ?>" class="btn btn-navbar-ghost">🏠 หน้าแรก</a> </div> </nav> <div class="wrapper" style="flex-direction: column;"> <!-- Institutional Header Hero with Cover Banner & Logo --> <?php $bg_banner = !empty($shop_info['store_banner_url']) ? $shop_info['store_banner_url'] : 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=1200&auto=format&fit=crop&q=80'; $store_logo = !empty($shop_info['custom_logo_url']) ? $shop_info['custom_logo_url'] : 'logo.png'; ?> <header class="gov-banner" style="background: linear-gradient(to bottom, rgba(15,23,42,0.65), rgba(15,23,42,0.95)), url('<?php echo htmlspecialchars($bg_banner); ?>') center/cover no-repeat; padding: 35px 25px; border-bottom: 2px solid var(--accent); position: relative; overflow: hidden;"> <div style="max-width: 1200px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap; width: 100%;"> <div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap;"> <div style="width: 80px; height: 80px; border-radius: 20px; background: rgba(255,255,255,0.95); padding: 6px; box-shadow: 0 10px 25px rgba(0,0,0,0.3); border: 2px solid var(--accent); flex-shrink: 0; display: flex; align-items: center; justify-content: center;"> <img src="<?php echo htmlspecialchars($store_logo); ?>" alt="Store Logo" style="width: 100%; height: 100%; object-fit: contain; border-radius: 14px;" onerror="this.src='logo.png'"> </div> <div class="gov-title-text"> <?php if ($shop_info): ?> <h1 style="font-size: 26px; font-weight: 800; color: #ffffff; text-shadow: 0 2px 10px rgba(0,0,0,0.7); margin: 0 0 6px 0;"> <?php echo htmlspecialchars($shop_info['name']); ?> <?php if (!empty($shop_info['category'])): ?> <span style="background: var(--accent); color: #0f172a; font-size: 12px; font-weight: 800; padding: 3px 10px; border-radius: 20px; vertical-align: middle; margin-left: 8px; font-family: sans-serif;"><?php echo htmlspecialchars($shop_info['category']); ?></span> <?php endif; ?> </h1> <div style="display: flex; align-items: center; gap: 15px; flex-wrap: wrap; font-size: 13.5px; color: #cbd5e1;"> <span>📍 <?php echo htmlspecialchars($shop_info['address'] ?? 'ร้านค้าสมาชิก CMTC Solution'); ?></span> <?php if (!empty($shop_info['phone'])): ?> <span>📞 <a href="tel:<?php echo htmlspecialchars($shop_info['phone']); ?>" style="color: var(--accent); text-decoration: none; font-weight: bold;"><?php echo htmlspecialchars($shop_info['phone']); ?></a></span> <?php endif; ?> <?php if (!empty($shop_info['line_id'])): ?> <span style="background: rgba(16,185,129,0.2); color: #10B981; padding: 2px 8px; border-radius: 6px; font-weight: bold;">💬 LINE: <?php echo htmlspecialchars($shop_info['line_id']); ?></span> <?php endif; ?> </div> <?php else: ?> <h1 style="font-size: 24px; font-weight: 800; color: #ffffff;">ศูนย์อาหาร CMTC Food Center</h1> <p style="margin: 0; color: #94a3b8; font-size: 13.5px;">ระบบบริการจัดการสั่งอาหารและบริการลูกค้าส่วนกลาง CMTC Enterprise Cloud System</p> <?php endif; ?> </div> </div> <div> <span class="system-badge" style="background: linear-gradient(135deg, #ff5722 0%, #e64a19 100%); color: #fff; border: none; padding: 8px 16px; border-radius: 20px; font-weight: 800; font-size: 12px; box-shadow: 0 4px 15px rgba(255,87,34,0.4);"> ⚡ ONLINE SELF-SERVICE MENU </span> </div> </div> <?php if ($shop_info && !empty($shop_info['promo_banner'])): ?> <div style="width: 100%; background: linear-gradient(90deg, rgba(255,87,34,0.9), rgba(230,74,25,0.9)); color: #fff; padding: 8px 20px; margin-top: 20px; border-radius: 8px; font-size: 13.5px; font-weight: 800; display: flex; align-items: center; gap: 10px; box-shadow: 0 4px 15px rgba(0,0,0,0.2);"> <span style="font-size: 16px;">📢</span> <marquee scrollamount="6" behavior="scroll"><?php echo htmlspecialchars($shop_info['promo_banner']); ?></marquee> </div> <?php endif; ?> </header> <div class="page-container"> <!-- Breadcrumbs Navigation --> <div class="breadcrumbs"> <a href="<?php echo htmlspecialchars($user_role_home); ?>">หน้าแรก</a> <span>></span> <a href="menu.php">เมนูอาหาร</a> <?php if ($shop_info): ?> <span>></span> <span><?php echo htmlspecialchars($shop_info['name']); ?></span> <?php endif; ?> </div> <?php if ($success_msg): ?> <div class="alert alert-success"><?php echo htmlspecialchars($success_msg); ?></div> <?php endif; ?> <?php if ($error_msg): ?> <div class="alert alert-danger"><?php echo htmlspecialchars($error_msg); ?></div> <?php endif; ?> <?php if (!$shop_info): ?> <!-- Strict QR Scan Lock Notice --> <div style="max-width: 650px; margin: 60px auto; text-align: center; background: #ffffff; border: 2px dashed #ff5722; padding: 45px 30px; border-radius: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.06);"> <div style="font-size: 64px; margin-bottom: 15px;">🔒</div> <h2 style="font-size: 24px; font-weight: 800; color: #0f172a; margin-bottom: 12px;">กรุณาสแกน QR Code ประจำโต๊ะอาหาร</h2> <p style="color: #64748b; font-size: 15px; line-height: 1.6; margin-bottom: 25px;"> เพื่อความปลอดภัยและป้องกันการสั่งอาหารสลับร้านค้า ระบบกำหนดให้สแกนป้าย QR Code บนโต๊ะอาหารของทางร้านเพื่อเข้าสู่เมนูสั่งซื้อประจำโต๊ะอาหารของท่านเท่านั้นครับ </p> <div style="background: #f8fafc; padding: 16px; border-radius: 12px; border: 1px solid #e2e8f0; font-size: 13.5px; color: #334155; font-weight: 600;"> 📱 โปรดใช้กล้องมือถือสแกนป้าย QR Code บนโต๊ะอาหารของท่านอีกครั้งเพื่อเริ่มสั่งอาหาร </div> </div> <?php else: ?> <?php if ($is_expired_session): ?> <div style="background: rgba(239, 68, 68, 0.1); border: 2px solid #ef4444; padding: 25px; border-radius: 12px; margin-bottom: 25px; text-align: center; color: #ef4444;"> <div style="font-size: 42px; margin-bottom: 8px;">🔒</div> <h3 style="margin: 0 0 6px; font-weight: 800; font-size: 20px;">รอบการสั่งซื้อของโต๊ะนี้เช็กบิลเรียบร้อยแล้ว</h3> <p style="margin: 0; font-size: 14.5px; opacity: 0.9;">ลูกค้าก่อนหน้าได้ทำการเช็กบิลเรียบร้อยแล้ว หากท่านเป็นลูกค้าท่านใหม่ กรุณาสแกนป้าย QR Code ประจำโต๊ะอาหารอีกครั้งเพื่อเปิดรอบการสั่งซื้อใหม่ครับ</p> </div> <?php elseif (!empty($_GET['table'])): ?> <div class="table-info-banner" style="background: linear-gradient(135deg, #00C9FF 0%, #92FE9D 100%); padding: 14px 18px; border-radius: 12px; margin-bottom: 20px; color: #0f172a; font-weight: 800; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 4px 15px rgba(0,201,255,0.2); gap: 10px; flex-wrap: wrap;"> <div style="display: flex; align-items: center; gap: 10px; font-size: 16px;"> <span style="font-size: 24px;">📱</span> <div> <div>คุณกำลังสั่งอาหารสำหรับ <u>โต๊ะ <?php echo htmlspecialchars($_GET['table']); ?></u></div> <div style="font-size: 12px; font-weight: 400; opacity: 0.85;">สแกนผ่าน QR Code ประจำโต๊ะอาหารเรียบร้อยแล้ว</div> </div> </div> <span style="background: rgba(15, 23, 42, 0.15); padding: 4px 12px; border-radius: 20px; font-size: 13px;">โต๊ะ <?php echo htmlspecialchars($_GET['table']); ?></span> </div> <?php endif; ?> <!-- Live Order Tracker Container --> <div id="liveTrackerBox" class="client-tracking-container" style="display: none;"> <div class="tracking-header"> <div class="tracking-title">🔔 ติดตามสถานะคำสั่งซื้อของคุณ</div> <div id="trackingBadge" class="tracking-badge pending">กำลังส่งคำสั่งซื้อ...</div> </div> <div style="font-size: 13px; color: var(--lp-text-primary); margin-bottom: 5px;"> ออเดอร์ของคุณ: <strong id="trackingOrderIdText">#--</strong> (โต๊ะ <span id="trackingTableText">--</span>) </div> <div id="trackingStepsLine" class="tracking-steps step-pending"> <div class="step-dot active" id="dot-pending" title="ส่งออเดอร์แล้ว">1</div> <div class="step-dot" id="dot-preparing" title="กำลังเตรียมปรุง">2</div> <div class="step-dot ready-dot" id="dot-ready" title="พร้อมรับประทาน">🛎️</div> </div> <div style="display: flex; justify-content: space-between; font-size: 11px; color: var(--text-secondary); margin-top: 5px;"> <span>ส่งรายการแล้ว</span> <span>กำลังปรุงอาหาร</span> <span>พร้อมรับอาหาร</span> </div> </div> <div style="display: flex; gap: 20px; flex-wrap: wrap;"> <!-- Food items list --> <div style="flex: 2; min-width: 320px;"> <div class="page-header"> <h2>รายการอาหารและเครื่องดื่มประจำวัน</h2> <a href="status.php" class="btn btn-ghost btn-sm">ดูบอร์ดแสดงสถานะคิว (Live Queue)</a> </div> <div class="menu-grid"> <?php if (empty($products)): ?> <div style="grid-column: 1/-1; text-align: center; padding: 40px; border: 1px dashed var(--border-color); background: #fff;"> ไม่พบรายการอาหารพร้อมให้บริการในคลังระบบขณะนี้ </div> <?php else: ?> <?php foreach ($products as $prod): $img_src = !empty($prod['image_url']) ? $prod['image_url'] : 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=600&auto=format&fit=crop&q=80'; ?> <div class="menu-card" style="overflow: hidden; border-radius: 14px; box-shadow: 0 4px 15px rgba(0,0,0,0.06); transition: transform 0.2s; background: #fff; display: flex; flex-direction: column; justify-content: space-between;"> <div> <div class="menu-card-image" style="width: 100%; height: 160px; overflow: hidden; background: #f8fafc; position: relative; cursor: pointer;" onclick="openFoodDetailModal(<?php echo $prod['id']; ?>)"> <img src="<?php echo htmlspecialchars($img_src); ?>" alt="<?php echo htmlspecialchars($prod['name']); ?>" style="width: 100%; height: 100%; object-fit: cover;" onerror="this.src='https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=600&auto=format&fit=crop&q=80'" loading="lazy" decoding="async"> <span class="badge badge-available" style="position: absolute; top: 10px; right: 10px; box-shadow: 0 2px 8px rgba(0,0,0,0.15);">พร้อมเสิร์ฟ</span> </div> <div class="menu-card-header" style="padding: 12px 14px 4px 14px;"> <div style="font-weight: 800; font-size: 15px; color: var(--text-color); margin-bottom: 4px;"><?php echo htmlspecialchars($prod['name']); ?></div> <?php if (!empty($prod['description'])): ?> <div style="font-size: 12.5px; color: #64748b; margin-bottom: 8px; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;"> <?php echo htmlspecialchars($prod['description']); ?> </div> <?php endif; ?> </div> </div> <div class="menu-card-body" style="padding: 0 14px 14px 14px;"> <div class="menu-card-price" style="font-size: 19px; font-weight: 800; color: #ff5722; margin-bottom: 10px;">฿<?php echo number_format($prod['price'], 2); ?></div> <button type="button" class="btn btn-primary" style="width: 100%; border-radius: 8px; font-weight: bold; background: linear-gradient(135deg, #ff5722 0%, #e64a19 100%); color: #fff; border: none; padding: 10px 4px; cursor: pointer; font-size: 13.5px; box-shadow: 0 4px 12px rgba(255,87,34,0.3);" onclick="quickAddToCart(<?php echo $prod['id']; ?>)"> 🛒 เพิ่มลงตะกร้า </button> </div> </div> <?php endforeach; ?> <?php endif; ?> </div> </div> <!-- Cart Sidebar (Sleek Dark Navy Widget matching screenshot) --> <div style="flex: 1; min-width: 300px; position: sticky; top: 90px; align-self: start; z-index: 90;"> <div class="cart-card-modern" style="background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); color: #ffffff; border-radius: 18px; padding: 20px; box-shadow: 0 10px 30px rgba(0,0,0,0.25); border: 1.5px solid #ff5722;"> <!-- Cart Header Widget --> <div style="display: flex; align-items: center; justify-content: space-between; padding-bottom: 16px; border-bottom: 1px solid rgba(255,255,255,0.12); margin-bottom: 16px;"> <div style="display: flex; align-items: center; gap: 12px;"> <div style="position: relative; display: flex; align-items: center;"> <span style="font-size: 30px;">🛒</span> <span id="item-count" style="position: absolute; top: -6px; right: -8px; background: #ff5722; color: #fff; font-size: 11px; font-weight: 800; padding: 2px 7px; border-radius: 10px; border: 2px solid #0f172a;">0</span> </div> <div> <div style="font-size: 13.5px; color: #94a3b8; font-weight: 600;">ตะกร้าอาหารของคุณ</div> <div style="font-size: 22px; font-weight: 800; color: #ff5722; line-height: 1.1;">฿<span id="cart-total-value">0.00</span></div> </div> </div> <span id="cart-badge-counter" style="display: none;">0</span> </div> <form id="order-form" method="POST" action=""> <input type="hidden" name="cart_data" id="cart_data_input" value="[]"> <input type="hidden" name="store_id" value="<?php echo $store_id; ?>"> <input type="hidden" name="shop_id" value="<?php echo $store_id; ?>"> <input type="hidden" name="table_number" id="table_number" value="<?php echo htmlspecialchars($table_param); ?>"> <div class="cart-items" id="cart-items-container" style="max-height: 380px; overflow-y: auto; margin-bottom: 16px; padding-right: 2px;"> <div style="color: #94a3b8; text-align: center; padding: 35px 10px; font-size: 14px; background: rgba(0,0,0,0.2); border-radius: 12px; border: 1px dashed rgba(255,255,255,0.1);" id="empty-cart-msg"> 🛒 ไม่มีอาหารในตะกร้าของคุณ </div> </div> <button type="submit" name="place_order" id="btn-submit-order" class="btn" style="width: 100%; background: linear-gradient(135deg, #ff5722 0%, #e64a19 100%); color: #fff; border: none; padding: 14px; border-radius: 12px; font-weight: 800; font-size: 15.5px; cursor: pointer; box-shadow: 0 4px 15px rgba(255,87,34,0.4); display: flex; align-items: center; justify-content: center; gap: 8px;"> 📋 ยืนยันและส่งใบสั่งอาหาร </button> </form> </div> </div> </div> <?php endif; ?> </div> </div> <!-- Order Submission Loading Overlay --> <div class="order-loading-overlay" id="orderLoadingOverlay"> <div class="order-spinner"></div> <h3 style="color: #ffffff; font-weight: 800; margin: 0;">กำลังส่งออเดอร์ของคุณไปยังห้องครัว... 🍳</h3> <p style="color: #94a3b8; font-size: 13.5px; margin-top: 8px;">กรุณารอสักครู่ ระบบกำลังจัดเตรียมคิวรับออเดอร์</p> </div> <!-- Global Footer --> <footer class="global-footer"> <div class="footer-brand">CMTC Tech Solution • ระบบแอดมินร้านค้า</div> <div class="footer-contact"> ติดต่อทีมงานบำรุงรักษา: CMTC Support • โทรศัพท์: 02-123-4567 • อีเมล: support@CMTC Tech Solution.cloud </div> </footer> <script> let cart = []; const serverProducts = <?php echo json_encode($products, JSON_HEX_TAG|JSON_HEX_APOS|JSON_HEX_QUOT|JSON_HEX_AMP); ?>; function isDrinkItem(item) { if (!item) return false; const cat = (item.category || '').toLowerCase(); const name = (item.name || item.menu_name || '').toLowerCase(); const drinkKeywords = [ 'เครื่องดื่ม', 'น้ำ', 'drink', 'beverage', 'ชา', 'กาแฟ', 'นม', 'โซดา', 'ชานม', 'ชาไทย', 'เอสเพรสโซ', 'คาปูชิโน', 'ลาเต้', 'อเมริกาโน', 'โกโก้', 'สมูทตี้', 'น้ำส้ม', 'น้ำมะนาว', 'โค้ก', 'เป๊ปซี่', 'แฟนต้า', 'สไปรท์', 'มัทฉะ', 'ชาเขียว', 'ชามะนาว', 'อิตาเลียนโซดา', 'juice', 'tea', 'coffee', 'boba' ]; return drinkKeywords.some(kw => cat.includes(kw) || name.includes(kw)); } function getItemUnit(item) { if (item && item.unit && String(item.unit).trim()) { return String(item.unit).trim(); } return isDrinkItem(item) ? 'แก้ว' : 'จาน'; } function getItemIcon(item) { return ''; } function openFoodDetailModal(itemOrId) { let item = itemOrId; if (typeof itemOrId === 'number' || typeof itemOrId === 'string') { item = serverProducts.find(p => p.id == itemOrId); } if (!item) { console.error("Food item not found for ID:", itemOrId); return; } const imgUrl = item.image_url || 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=600&auto=format&fit=crop&q=80'; const descText = item.description || (isDrinkItem(item) ? 'เครื่องดื่มชงสดใหม่รสชาติเข้มข้น' : 'เมนูอาหารคุณภาพ ปรุงสดใหม่ทุกจาน'); const unitText = getItemUnit(item); const rawOptions = item.spice_options || 'เผ็ดปกติ,ไม่เผ็ด,เผ็ดน้อย,เผ็ดมาก'; const optionsList = rawOptions.split(',').map(o => o.trim()).filter(o => o.length > 0); let spiceSelectOptionsHtml = ''; optionsList.forEach(opt => { spiceSelectOptionsHtml += `<option value="${opt}">${opt}</option>`; }); Swal.fire({ title: `<div style="font-size:20px; font-weight:800; color:#ff5722; font-family:'Sarabun',sans-serif;">${item.name}</div>`, html: ` <div style="text-align:left; font-family:'Sarabun',sans-serif; color:#334155;"> <img src="${imgUrl}" style="width:100%; height:180px; object-fit:cover; border-radius:12px; margin-bottom:15px; box-shadow:0 4px 12px rgba(0,0,0,0.1);" alt="${item.name}"> <div style="font-size:13px; color:#64748b; margin-bottom:8px;">หมวดหมู่: <strong>${item.category || 'ทั่วไป'}</strong></div> <div style="font-size:13.5px; color:#334155; margin-bottom:15px; background:#f8fafc; padding:10px 12px; border-radius:8px; border:1px solid #e2e8f0; line-height:1.5;">${descText}</div> <div style="font-size:24px; font-weight:800; color:#ff5722; margin-bottom:15px; text-align:right;">฿${parseFloat(item.price).toFixed(2)} / ${unitText}</div> <div style="margin-bottom:15px;"> <label style="font-size:13px; font-weight:700; display:block; margin-bottom:6px; color:#1e293b;">ตัวเลือกรสชาติเพิ่มเติม:</label> <select id="swal_spice_level" style="width:100%; padding:10px; border-radius:8px; border:1px solid #cbd5e1; font-size:13.5px; font-family:inherit;"> ${spiceSelectOptionsHtml} </select> </div> <div style="margin-bottom:15px;"> <label style="font-size:13px; font-weight:700; display:block; margin-bottom:6px; color:#1e293b;">รายละเอียดเพิ่มเติม / คำขอพิเศษ:</label> <input type="text" id="swal_note" placeholder="เช่น หวานน้อย 50% / แยกน้ำแข็ง / ขอแก้วใหญ่" style="width:100%; padding:10px; border-radius:8px; border:1px solid #cbd5e1; font-size:13.5px; font-family:inherit; box-sizing:border-box;"> </div> <div style="display:flex; justify-content:space-between; align-items:center; background:#f1f5f9; padding:12px 15px; border-radius:10px;"> <label style="font-size:14px; font-weight:700; color:#0f172a;">จำนวน (${unitText}):</label> <div style="display:flex; align-items:center; gap:12px;"> <button type="button" onclick="changeSwalQty(-1)" style="width:34px; height:34px; border-radius:50%; border:1px solid #cbd5e1; background:#fff; font-weight:800; font-size:18px; cursor:pointer;">-</button> <span id="swal_qty" style="font-size:18px; font-weight:800; width:24px; text-align:center; color:#0f172a;">1</span> <button type="button" onclick="changeSwalQty(1)" style="width:34px; height:34px; border-radius:50%; border:none; background:#ff5722; color:#fff; font-weight:800; font-size:18px; cursor:pointer;">+</button> </div> </div> </div> `, showCancelButton: true, confirmButtonText: `🛒 เพิ่มลงตะกร้า`, cancelButtonText: 'ยกเลิก', confirmButtonColor: '#ff5722', cancelButtonColor: '#64748b', focusConfirm: false, preConfirm: () => { const spice = document.getElementById('swal_spice_level').value; const note = document.getElementById('swal_note').value; const qty = parseInt(document.getElementById('swal_qty').innerText) || 1; return { spice, note, qty }; } }).then((result) => { if (result.isConfirmed) { addToCartCustom(item, result.value.qty, result.value.spice, result.value.note); } }); } function changeSwalQty(delta) { const el = document.getElementById('swal_qty'); if (el) { let val = parseInt(el.innerText) || 1; val += delta; if (val < 1) val = 1; el.innerText = val; } } function quickAddToCart(id) { openFoodDetailModal(id); } function addToCartCustom(item, qty, spice, note) { if (!item) return; const itemId = parseInt(item.id); const itemQty = parseInt(qty) || 1; const itemSpice = spice || 'เผ็ดปกติ'; const itemNote = note || ''; const existing = cart.find(c => c.id == itemId && c.spice_level === itemSpice && c.note === itemNote); if (existing) { existing.qty += itemQty; } else { cart.push({ id: itemId, name: item.name, price: parseFloat(item.price), qty: itemQty, spice_level: itemSpice, note: itemNote, unit: item.unit || '' }); } updateCartUI(); // Auto toast notification if (typeof Swal !== 'undefined') { const Toast = Swal.mixin({ toast: true, position: 'top-end', showConfirmButton: false, timer: 2000, timerProgressBar: true }); const unitText = getItemUnit(item); const iconText = getItemIcon(item); Toast.fire({ icon: 'success', title: `🎉 เพิ่ม "${item.name}" (x${itemQty} ${unitText}) ลงตะกร้าแล้ว!` }); } } function addToCart(id, name, price) { const item = serverProducts.find(p => p.id == id); if (item) { addToCartCustom(item, 1, 'เผ็ดปกติ', ''); } else { addToCartCustom({ id: id, name: name, price: price }, 1, 'เผ็ดปกติ', ''); } } function removeFromCart(index) { cart.splice(index, 1); updateCartUI(); } function changeQtyByIndex(index, delta) { if (cart[index]) { cart[index].qty += delta; if (cart[index].qty <= 0) { cart.splice(index, 1); } updateCartUI(); } } function scrollToCartContainer() { const cartEl = document.getElementById('cart-items-container') || document.getElementById('order-form'); if (cartEl) { cartEl.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } function updateCartUI() { const container = document.getElementById('cart-items-container'); const itemCountBadge = document.getElementById('item-count'); const totalSpan = document.getElementById('cart-total-value'); const cartInput = document.getElementById('cart_data_input'); const topBadge = document.getElementById('cart-badge-counter'); const floatBadge = document.getElementById('floating-cart-count'); if (!container) return; if (cart.length === 0) { container.innerHTML = '<div style="color: #94a3b8; text-align: center; padding: 30px 10px; font-size: 13.5px; background: rgba(0,0,0,0.2); border-radius: 12px; border: 1px dashed rgba(255,255,255,0.1);" id="empty-cart-msg">🛒 ไม่มีรายการในตะกร้าของคุณ</div>'; if (itemCountBadge) itemCountBadge.textContent = '0'; if (totalSpan) totalSpan.textContent = '0.00'; if (cartInput) cartInput.value = '[]'; if (topBadge) topBadge.textContent = '0'; if (floatBadge) floatBadge.textContent = '0'; return; } container.innerHTML = ''; let total = 0; let itemCount = 0; cart.forEach((item, index) => { total += item.price * item.qty; itemCount += item.qty; const div = document.createElement('div'); div.className = 'cart-item'; div.style.background = 'rgba(255, 255, 255, 0.05)'; div.style.border = '1px solid rgba(255, 255, 255, 0.1)'; div.style.borderRadius = '12px'; div.style.padding = '12px'; div.style.marginBottom = '10px'; div.style.display = 'flex'; div.style.justifyContent = 'space-between'; div.style.alignItems = 'center'; const unitText = getItemUnit(item); const iconText = getItemIcon(item); let extraInfo = ''; if (item.spice_level) extraInfo += `<div style="margin-top:4px;"><span style="background:rgba(0,229,255,0.15); color:#00e5ff; border:1px solid rgba(0,229,255,0.3); padding:2px 7px; border-radius:4px; font-size:11px; font-weight:bold; display:inline-block;">${item.spice_level}</span></div>`; if (item.note) extraInfo += `<div style="margin-top:3px;"><span style="color:#ffd166; font-size:12px; font-weight:bold; display:inline-block;">คำขอพิเศษ: ${item.note}</span></div>`; div.innerHTML = ` <div style="flex-grow: 1; padding-right: 6px;"> <strong style="color:#ffffff; font-size:14.5px;">${item.name}</strong><br> ${extraInfo} <span style="color: #ff5722; font-weight:800; font-size:13.5px; margin-top:2px; display:inline-block;">฿${item.price.toFixed(2)} x ${item.qty} ${unitText}</span> </div> <div style="display: flex; align-items: center; gap: 6px;"> <button type="button" class="btn btn-secondary btn-sm" style="padding: 3px 8px; border-radius:6px; background:rgba(255,255,255,0.15); color:#fff; border:none; font-weight:bold; cursor:pointer;" onclick="changeQtyByIndex(${index}, -1)">-</button> <span style="font-weight:bold; font-size:14px; width:16px; text-align:center; color:#fff;">${item.qty}</span> <button type="button" class="btn btn-secondary btn-sm" style="padding: 3px 8px; border-radius:6px; background:#ff5722; color:#fff; border:none; font-weight:bold; cursor:pointer;" onclick="changeQtyByIndex(${index}, 1)">+</button> <button type="button" class="btn btn-danger btn-sm" style="padding: 3px 8px; background-color: #ef4444; color:#fff; border:none; border-radius:6px; font-size:11px; font-weight:bold; cursor:pointer;" onclick="removeFromCart(${index})">ลบ</button> </div> `; container.appendChild(div); }); const formattedTotal = total.toLocaleString('th-TH', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); if (itemCountBadge) itemCountBadge.textContent = `${itemCount}`; if (topBadge) topBadge.textContent = `${itemCount}`; if (floatBadge) floatBadge.textContent = `${itemCount}`; if (totalSpan) totalSpan.textContent = formattedTotal; if (cartInput) cartInput.value = JSON.stringify(cart); } // 15-Second Front-end Rate Limiting & Cooldown Protection (Anti-Spam) let orderCooldownInterval = null; function startOrderCooldownTimer(seconds) { const btn = document.getElementById('btn-submit-order'); if (!btn) return; const secs = parseInt(seconds) || 15; const unlockTime = Date.now() + (secs * 1000); localStorage.setItem('order_cooldown_until', unlockTime); if (orderCooldownInterval) clearInterval(orderCooldownInterval); btn.disabled = true; btn.style.opacity = '0.75'; btn.style.cursor = 'not-allowed'; const updateTimerText = () => { const remaining = Math.ceil((unlockTime - Date.now()) / 1000); if (remaining > 0) { btn.innerHTML = `⏳ กรุณารอสักครู่ (${remaining} วินาที)...`; } else { clearInterval(orderCooldownInterval); orderCooldownInterval = null; localStorage.removeItem('order_cooldown_until'); btn.disabled = false; btn.style.opacity = '1'; btn.style.cursor = 'pointer'; btn.innerHTML = `📋 ยืนยันและส่งใบสั่งอาหาร`; } }; updateTimerText(); orderCooldownInterval = setInterval(updateTimerText, 1000); } // Restore active cooldown timer on page load window.addEventListener('DOMContentLoaded', () => { const cooldownUntil = parseInt(localStorage.getItem('order_cooldown_until') || '0'); if (cooldownUntil > Date.now()) { const remainingSecs = Math.ceil((cooldownUntil - Date.now()) / 1000); startOrderCooldownTimer(remainingSecs); } }); function openPaymentVerificationModal(data) { const orderId = data.order_id; const tableNum = data.table_number; const totalAmt = parseFloat(data.total_amount || 0).toFixed(2); const storeId = data.store_id || "<?php echo $store_id; ?>"; const qrPromptPayUrl = `https://api.qrserver.com/v1/create-qr-code/?size=220x220&data=PromptPay_Store_${storeId}_Order_${orderId}_Amount_${totalAmt}`; Swal.fire({ title: `<div style="font-size:20px; font-weight:800; color:#0f172a; font-family:'Sarabun',sans-serif;">💳 ยืนยันการชำระเงิน (ออเดอร์ #${orderId})</div>`, html: ` <div style="text-align:left; font-family:'Sarabun',sans-serif; color:#334155; font-size:14px; line-height:1.5;"> <div style="background:#f8fafc; padding:12px; border-radius:10px; border:1px solid #e2e8f0; margin-bottom:12px; text-align:center;"> <div style="font-size:13px; color:#64748b; margin-bottom:2px;">ยอดชำระเงินทั้งสิ้น (โต๊ะ ${tableNum})</div> <div style="font-size:26px; font-weight:800; color:#ff5722;">฿${totalAmt}</div> </div> <div style="text-align:center; margin-bottom:15px; background:#fff; padding:10px; border-radius:10px; border:1px dashed #cbd5e1;"> <img src="${qrPromptPayUrl}" style="width:170px; height:170px; border-radius:8px;" alt="PromptPay QR Code"> <div style="font-size:12px; color:#64748b; margin-top:4px;">📱 สแกน QR Code พร้อมเพย์ หรืออัปโหลดสลิปโอนเงิน</div> </div> <div style="margin-bottom:12px;"> <label style="font-size:13px; font-weight:700; display:block; margin-bottom:4px; color:#1e293b;">แนบหลักฐานสลิปการโอนเงิน (ถ้ามี):</label> <input type="file" id="swal_slip_file" accept="image/*,.pdf" style="width:100%; padding:8px; border-radius:6px; border:1px solid #cbd5e1; font-size:12.5px;"> </div> </div> `, showCancelButton: true, confirmButtonText: '✅ ยืนยันชำระเงิน & ส่งเข้าครัว', cancelButtonText: '💵 ชำระเงินสดกับพนักงาน', confirmButtonColor: '#10b981', cancelButtonColor: '#ff5722', allowOutsideClick: false, preConfirm: () => { const fileInput = document.getElementById('swal_slip_file'); const file = (fileInput && fileInput.files && fileInput.files.length > 0) ? fileInput.files[0] : null; return { slipFile: file }; } }).then((result) => { const overlay = document.getElementById('orderLoadingOverlay'); if (result.isConfirmed) { const slipFile = (result.value && result.value.slipFile) ? result.value.slipFile : null; const verifyFormData = new FormData(); verifyFormData.append('verify_payment', '1'); verifyFormData.append('order_id', orderId); verifyFormData.append('store_id', storeId); if (slipFile) { verifyFormData.append('slip_file', slipFile); } if (overlay) overlay.classList.add('active'); fetch(window.location.href, { method: 'POST', body: verifyFormData }) .then(res => res.json()) .then(resData => { if (overlay) overlay.classList.remove('active'); if (resData.success) { try { const audio = new Audio('https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3'); audio.play().catch(e => {}); } catch(e) {} localStorage.setItem('active_order_id', orderId); localStorage.setItem('active_order_table', tableNum); cart = []; updateCartUI(); startOrderCooldownTimer(15); Swal.fire({ icon: 'success', title: '🎉 ยืนยันชำระเงินโอน/ส่งสลิปเรียบร้อยแล้ว!', html: ` <div style="text-align:left; font-size:14px; line-height:1.6; font-family:'Sarabun',sans-serif; color:#334155;"> <div style="background:#f8fafc; padding:15px; border-radius:12px; border:1px solid #e2e8f0; margin-bottom:12px;"> <p style="margin:0 0 6px 0;"><strong>หมายเลขออเดอร์:</strong> <span style="color:#ff5722; font-weight:800; font-size:16px;">#${orderId}</span></p> <p style="margin:0 0 6px 0;"><strong>โต๊ะอาหาร:</strong> <span style="font-weight:800; font-size:15px;">โต๊ะที่ ${tableNum}</span></p> <p style="margin:0; color:#10b981; font-weight:700;">🟢 แนบหลักฐานสลิปเรียบร้อย ส่งออเดอร์เข้าห้องครัวแล้ว!</p> </div> </div> `, confirmButtonText: '🔔 ติดตามสถานะอาหาร (Live Tracking)', showCancelButton: true, cancelButtonText: 'สั่งอาหารเพิ่ม', confirmButtonColor: '#ff5722' }).then((r) => { if (r.isConfirmed) { window.location.href = 'status.php?order_id=' + orderId + (storeId ? '&store_id=' + storeId : ''); } }); initOrderTracker(orderId, tableNum); window.scrollTo({ top: 0, behavior: 'smooth' }); } else { Swal.fire({ icon: 'error', title: 'ไม่สามารถยืนยันชำระเงินได้', text: resData.message || 'เกิดข้อผิดพลาดในการยืนยันชำระเงิน', confirmButtonColor: '#ff5722' }); } }) .catch(err => { if (overlay) overlay.classList.remove('active'); Swal.fire({ icon: 'error', title: 'เกิดข้อผิดพลาดทางเทคนิค', text: err.message, confirmButtonColor: '#ff5722' }); }); } else { // Customer selected Cash Payment const cashFormData = new FormData(); cashFormData.append('confirm_cash_payment', '1'); cashFormData.append('order_id', orderId); cashFormData.append('store_id', storeId); if (overlay) overlay.classList.add('active'); fetch(window.location.href, { method: 'POST', body: cashFormData }) .then(res => res.json()) .then(resData => { if (overlay) overlay.classList.remove('active'); localStorage.setItem('active_order_id', orderId); localStorage.setItem('active_order_table', tableNum); cart = []; updateCartUI(); startOrderCooldownTimer(15); Swal.fire({ icon: 'success', title: '🎉 ส่งออเดอร์เรียบร้อย (เลือกชำระเงินสด)', html: ` <div style="text-align:left; font-size:14px; line-height:1.6; font-family:'Sarabun',sans-serif; color:#334155;"> <div style="background:#f8fafc; padding:15px; border-radius:12px; border:1px solid #e2e8f0; margin-bottom:12px;"> <p style="margin:0 0 6px 0;"><strong>หมายเลขออเดอร์:</strong> <span style="color:#ff5722; font-weight:800; font-size:16px;">#${orderId}</span></p> <p style="margin:0 0 6px 0;"><strong>โต๊ะอาหาร:</strong> <span style="font-weight:800; font-size:15px;">โต๊ะที่ ${tableNum}</span></p> <p style="margin:0; color:#3b82f6; font-weight:700;">💵 เลือกชำระเงินสดกับพนักงานเมื่อรับประทานเสร็จสิ้น</p> </div> </div> `, confirmButtonText: '🔔 ติดตามสถานะอาหาร (Live Tracking)', showCancelButton: true, cancelButtonText: 'สั่งอาหารเพิ่ม', confirmButtonColor: '#ff5722' }).then((r) => { if (r.isConfirmed) { window.location.href = 'status.php?order_id=' + orderId + (storeId ? '&store_id=' + storeId : ''); } }); initOrderTracker(orderId, tableNum); window.scrollTo({ top: 0, behavior: 'smooth' }); }); } }); } // Bind form submission to AJAX handler with SweetAlert2 Celebration Popup! var orderForm = document.getElementById('order-form'); if (orderForm) { orderForm.addEventListener('submit', function(event) { event.preventDefault(); const tableInput = document.getElementById('table_number'); if (!tableInput || !tableInput.value.trim()) { Swal.fire({ icon: 'warning', title: 'กรุณาระบุหมายเลขโต๊ะ', text: 'โปรดกรอกหมายเลขโต๊ะอาหารก่อนยืนยันส่งใบสั่งซื้อ', confirmButtonColor: '#ff5722' }); return; } if (cart.length === 0) { Swal.fire({ icon: 'info', title: 'ตะกร้าสินค้าว่างเปล่า', text: 'กรุณาเลือกรายการอาหารอย่างน้อย 1 รายการก่อนส่งออเดอร์', confirmButtonColor: '#ff5722' }); return; } // Show loading overlay const overlay = document.getElementById('orderLoadingOverlay'); if (overlay) overlay.classList.add('active'); // Ensure cart_data is up to date before submitting const cartInput = document.getElementById('cart_data_input'); if (cartInput) cartInput.value = JSON.stringify(cart); const formData = new FormData(this); formData.append('place_order', '1'); const currentUrl = window.location.href; const storeId = "<?php echo $store_id; ?>"; fetch(currentUrl, { method: 'POST', body: formData }) .then(async res => { const text = await res.text(); try { return JSON.parse(text); } catch(e) { console.error("Server raw response:", text); throw new Error("Server Response Error: " + text.substring(0, 150)); } }) .then(data => { if (overlay) overlay.classList.remove('active'); if (data.success) { if (data.status === 'unpaid') { openPaymentVerificationModal(data); } else { // Play Audio Success Chime try { const audio = new Audio('https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3'); audio.play().catch(e => {}); } catch(e) {} localStorage.setItem('active_order_id', data.order_id); localStorage.setItem('active_order_table', data.table_number); cart = []; updateCartUI(); // Trigger 15-second front-end cooldown timer startOrderCooldownTimer(data.remaining_seconds || 15); // SweetAlert2 Stunning Celebration Modal Swal.fire({ icon: 'success', title: '🎉 ส่งออเดอร์เข้าห้องครัวเรียบร้อยแล้ว!', html: ` <div style="text-align:left; font-size:14px; line-height:1.6; font-family:'Sarabun',sans-serif; color:#334155;"> <div style="background:#f8fafc; padding:15px; border-radius:12px; border:1px solid #e2e8f0; margin-bottom:12px;"> <p style="margin:0 0 6px 0;"><strong>หมายเลขออเดอร์:</strong> <span style="color:#ff5722; font-weight:800; font-size:16px;">#${data.order_id}</span></p> <p style="margin:0 0 6px 0;"><strong>โต๊ะอาหาร:</strong> <span style="font-weight:800; font-size:15px;">โต๊ะที่ ${data.table_number}</span></p> <p style="margin:0; color:#10b981; font-weight:700;">🟢 รายการสั่งอาหารส่งตรงเข้าหน้าจอพ่อครัว (Kitchen Display) แล้ว!</p> </div> <p style="font-size:13px; color:#64748b; margin:0;">ท่านสามารถติดตามสถานะการปรุงอาหารแบบเรียลไทม์ได้ทันที</p> </div> `, confirmButtonText: '🔔 ติดตามสถานะอาหาร (Live Tracking)', showCancelButton: true, cancelButtonText: 'สั่งอาหารเพิ่ม', confirmButtonColor: '#ff5722', cancelButtonColor: '#64748b' }).then((result) => { if (result.isConfirmed) { window.location.href = 'status.php?order_id=' + data.order_id + (storeId ? '&store_id=' + storeId : ''); } }); initOrderTracker(data.order_id, data.table_number); window.scrollTo({ top: 0, behavior: 'smooth' }); } } else { // Trigger cooldown timer if rate limited if (data.rate_limited || data.remaining_seconds) { startOrderCooldownTimer(data.remaining_seconds || 15); } Swal.fire({ icon: 'warning', title: 'กรุณารอสักครู่', text: data.message || 'เกิดข้อผิดพลาดในการส่งรายการสั่งซื้อ', confirmButtonColor: '#ff5722' }); } }) .catch(err => { if (overlay) overlay.classList.remove('active'); Swal.fire({ icon: 'error', title: 'เกิดข้อผิดพลาดทางเทคนิค', text: err.message, confirmButtonColor: '#ff5722' }); }); }); } // end if (orderForm) let trackerInterval = null; function initOrderTracker(orderId, tableNumber) { const box = document.getElementById('liveTrackerBox'); const orderIdText = document.getElementById('trackingOrderIdText'); const tableText = document.getElementById('trackingTableText'); const badge = document.getElementById('trackingBadge'); const stepsLine = document.getElementById('trackingStepsLine'); const dotPending = document.getElementById('dot-pending'); const dotPreparing = document.getElementById('dot-preparing'); const dotReady = document.getElementById('dot-ready'); if (!box) return; box.style.display = 'block'; orderIdText.textContent = `#${orderId}`; tableText.textContent = tableNumber; // Audio Chime Generator for Customer Sound Notification when Food is Ready function playCustomerFoodReadySound() { try { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (!AudioCtx) return; const ctx = new AudioCtx(); if (ctx.state === 'suspended') { ctx.resume(); } // 2-tone melodic chime (C5 -> G5) const osc1 = ctx.createOscillator(); const gain1 = ctx.createGain(); osc1.type = 'sine'; osc1.frequency.setValueAtTime(523.25, ctx.currentTime); // C5 gain1.gain.setValueAtTime(0.3, ctx.currentTime); gain1.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.3); osc1.connect(gain1); gain1.connect(ctx.destination); osc1.start(); osc1.stop(ctx.currentTime + 0.3); setTimeout(() => { const osc2 = ctx.createOscillator(); const gain2 = ctx.createGain(); osc2.type = 'sine'; osc2.frequency.setValueAtTime(783.99, ctx.currentTime); // G5 gain2.gain.setValueAtTime(0.4, ctx.currentTime); gain2.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.5); osc2.connect(gain2); gain2.connect(ctx.destination); osc2.start(); osc2.stop(ctx.currentTime + 0.5); }, 180); } catch (e) { console.error("Audio play failed:", e); } } let lastKnownStatus = null; // Clear any existing polling if (trackerInterval) clearInterval(trackerInterval); // Function to check status function checkStatus() { if (document.hidden) return; fetch(`menu.php?check_order_status=${orderId}`) .then(res => res.json()) .then(data => { if (!data.success) return; const status = data.status; // pending, preparing, ready, completed, cancelled // Reset dots dotPending.classList.remove('active'); dotPreparing.classList.remove('active'); dotReady.classList.remove('active'); if (status === 'ready' && lastKnownStatus !== 'ready') { // Trigger Audio Chime and Toast Notification when status transitions to 'ready' playCustomerFoodReadySound(); if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'success', title: '🛎️ อาหารของคุณเสร็จเรียบร้อยแล้ว!', text: 'อาหารของคุณปรุงเสร็จเรียบร้อยแล้ว พร้อมเสิร์ฟมารับอาหารได้เลย!', confirmButtonText: 'รับทราบ', confirmButtonColor: '#10B981', timer: 10000 }); } } lastKnownStatus = status; if (status === 'pending') { badge.className = 'tracking-badge pending'; badge.textContent = 'ส่งออเดอร์แล้ว (รอดำเนินการ)'; stepsLine.className = 'tracking-steps step-pending'; dotPending.classList.add('active'); } else if (status === 'preparing') { badge.className = 'tracking-badge preparing'; badge.textContent = 'กำลังเตรียมปรุงอาหาร... (กำลังทำ)'; stepsLine.className = 'tracking-steps step-preparing'; dotPending.classList.add('active'); dotPreparing.classList.add('active'); } else if (status === 'ready') { badge.className = 'tracking-badge ready'; badge.textContent = 'อาหารปรุงเสร็จแล้ว! 🛎️ มารับอาหารได้เลย'; stepsLine.className = 'tracking-steps step-ready'; dotPending.classList.add('active'); dotPreparing.classList.add('active'); dotReady.classList.add('active'); } else if (status === 'served' || status === 'completed') { // Order completed or served -> clear storage and hide tracker box if (trackerInterval) clearInterval(trackerInterval); localStorage.removeItem('active_order_id'); localStorage.removeItem('active_order_table'); box.style.display = 'none'; } else if (status === 'cancelled') { badge.className = 'tracking-badge cancelled'; badge.textContent = 'ออเดอร์นี้ถูกยกเลิกแล้ว'; if (trackerInterval) clearInterval(trackerInterval); localStorage.removeItem('active_order_id'); localStorage.removeItem('active_order_table'); } }) .catch(err => console.error("Tracker status check failed:", err)); } // Check immediately and poll every 3 seconds checkStatus(); trackerInterval = setInterval(checkStatus, 3000); } // On page load, check active order for this table from server or localStorage window.addEventListener('DOMContentLoaded', () => { const serverActiveOrderId = <?php echo json_encode($active_table_order_id); ?>; const currentTable = <?php echo json_encode($table_param); ?>; if (serverActiveOrderId) { localStorage.setItem('active_order_id', serverActiveOrderId); localStorage.setItem('active_order_table', currentTable); initOrderTracker(serverActiveOrderId, currentTable); } else { localStorage.removeItem('active_order_id'); localStorage.removeItem('active_order_table'); const box = document.getElementById('liveTrackerBox'); if (box) box.style.display = 'none'; } }); // Toggle Bottom Sheet Menu function toggleBottomSheet() { const sheet = document.getElementById('lpBottomSheet'); if (sheet) { sheet.classList.toggle('open'); } } // Handle browser go back safely function handleGoBack() { if (window.history.length > 1 && document.referrer && document.referrer !== window.location.href) { window.history.back(); } else { window.location.href = '<?php echo htmlspecialchars($user_role_home); ?>'; } } </script> <!-- Quick Menu Bottom Sheet --> <div class="lp-bottom-sheet" id="lpBottomSheet"> <div class="lp-bottom-sheet-backdrop" onclick="toggleBottomSheet()"></div> <div class="lp-bottom-sheet-content"> <div class="lp-bottom-sheet-header"> <span class="lp-bottom-sheet-title">📂 ทางลัดด่วน CMTC Tech Solution</span> <button class="lp-bottom-sheet-close" onclick="toggleBottomSheet()">×</button> </div> <div class="lp-bottom-sheet-grid"> <a href="<?php echo htmlspecialchars($user_role_home); ?>" class="lp-bottom-sheet-item"> <span class="lp-bottom-sheet-icon">🏠</span> <span class="lp-bottom-sheet-label">หน้าแรก</span> </a> <a href="menu.php" class="lp-bottom-sheet-item"> <span class="lp-bottom-sheet-icon">🛒</span> <span class="lp-bottom-sheet-label">สั่งอาหาร</span> </a> <a href="status.php" class="lp-bottom-sheet-item"> <span class="lp-bottom-sheet-icon">📊</span> <span class="lp-bottom-sheet-label">สถานะคิว</span> </a> <a href="store-admin.php" class="lp-bottom-sheet-item"> <span class="lp-bottom-sheet-icon">👨🍳</span> <span class="lp-bottom-sheet-label">คุมห้องครัว</span> </a> </div> </div> </div> <!-- Floating Sticky Cart Button (Bottom Right Icon Only) --> <button type="button" onclick="scrollToCartContainer()" id="floatingCartBtn" aria-label="Cart" style="position: fixed; bottom: 25px; right: 20px; z-index: 9999; background: linear-gradient(135deg, #ff5722 0%, #e64a19 100%); color: #ffffff; border: none; width: 56px; height: 56px; border-radius: 50%; font-size: 24px; display: flex; align-items: center; justify-content: center; box-shadow: 0 8px 25px rgba(255,87,34,0.5); cursor: pointer; transition: transform 0.2s;"> <span>🛒</span> <span id="floating-cart-count" style="position: absolute; top: -3px; right: -3px; background: #0f172a; color: #ffffff; border: 2px solid #ffffff; font-size: 11px; font-weight: 800; min-width: 22px; height: 22px; border-radius: 11px; display: flex; align-items: center; justify-content: center; padding: 0 4px; box-shadow: 0 2px 6px rgba(0,0,0,0.3);">0</span> </button> </body> </html>
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.25 |
proxy
|
phpinfo
|
Settings