File manager - Edit - /home/webapp69.cm.in.th/u69319090029/Project SME/store-admin.php
Back
<?php header('Content-Type: text/html; charset=utf-8'); error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING); require_once 'db.php'; if (session_status() === PHP_SESSION_NONE) { session_start(); } // Global Initialization of authentication & store context flags $is_super_admin = !empty($_SESSION['super_admin_logged_in']); $is_impersonating = !empty($_SESSION['is_impersonating']); $is_shop_admin = !empty($_SESSION['shop_admin_id']); $is_logged_in = $is_shop_admin || $is_super_admin || $is_impersonating || !empty($_SESSION['is_logged_in']); // Support direct URL impersonation parameter ?shop_id=XX or ?store_id=XX for Super Admin if ($is_super_admin && (!empty($_GET['shop_id']) || !empty($_GET['impersonate']) || !empty($_GET['store_id']))) { $target_id = intval($_GET['shop_id'] ?? $_GET['impersonate'] ?? $_GET['store_id'] ?? 0); if ($target_id > 0) { $_SESSION['is_impersonating'] = true; $_SESSION['impersonated_store_id'] = $target_id; $_SESSION['shop_id'] = $target_id; $_SESSION['store_id'] = $target_id; $_SESSION['is_logged_in'] = true; $is_impersonating = true; $is_logged_in = true; } } $active_tab = $_REQUEST['tab'] ?? $_GET['tab'] ?? $_POST['tab'] ?? 'store-dash'; $success_msg = ''; $error_msg = ''; if (!function_exists('getItemUnitPHP')) { function getItemUnitPHP($name, $category = '', $unit = '') { if (!empty($unit)) { return trim($unit); } $str = mb_strtolower($name . ' ' . $category, 'UTF-8'); $drinkKeywords = [ 'เครื่องดื่ม', 'น้ำ', 'drink', 'beverage', 'ชา', 'กาแฟ', 'นม', 'โซดา', 'ชานม', 'ชาไทย', 'เอสเพรสโซ', 'คาปูชิโน', 'ลาเต้', 'อเมริกาโน', 'โกโก้', 'สมูทตี้', 'น้ำส้ม', 'น้ำมะนาว', 'โค้ก', 'เป๊ปซี่', 'แฟนต้า', 'สไปรท์', 'มัทฉะ', 'ชาเขียว', 'ชามะนาว', 'อิตาเลียนโซดา', 'juice', 'tea', 'coffee', 'boba' ]; foreach ($drinkKeywords as $kw) { if (mb_strpos($str, $kw, 0, 'UTF-8') !== false) { return 'แก้ว'; } } return 'จาน'; } } // Handle Login Action if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && isset($_POST['login_admin'])) { $username = trim($_POST['username'] ?? ''); $password = $_POST['password'] ?? ''; if (empty($username) || empty($password)) { $error_msg = 'กรุณากรอกชื่อผู้ใช้และรหัสผ่าน'; } else { try { $stmt = $pdo->prepare("SELECT u.*, t.store_name FROM users u JOIN tenants t ON u.store_id = t.store_id WHERE u.username = :username AND u.role = 'store_admin' AND t.status = 'active' LIMIT 1"); $stmt->execute([':username' => $username]); $admin = $stmt->fetch(); if ($admin && (password_verify($password, $admin['password']) || $admin['password'] === $password)) { // Re-hash password if stored as plain text if ($admin['password'] === $password) { $rehash = password_hash($password, PASSWORD_DEFAULT); $upStmt = $pdo->prepare("UPDATE users SET password = :password WHERE id = :id"); $upStmt->execute([':password' => $rehash, ':id' => $admin['id']]); } session_regenerate_id(true); $_SESSION['shop_admin_id'] = $admin['id']; $_SESSION['shop_id'] = $admin['store_id']; $_SESSION['shop_admin_name'] = $admin['username']; $_SESSION['shop_name'] = $admin['store_name']; $success_msg = "เข้าสู่ระบบสำเร็จ ยินดีต้อนรับคุณ {$admin['username']}"; } else { $error_msg = 'ชื่อผู้ใช้หรือรหัสผ่านไม่ถูกต้อง หรือบัญชีของท่านอาจถูกระงับ'; } } catch (PDOException $e) { $error_msg = 'เกิดข้อผิดพลาดในการตรวจสอบข้อมูล: ' . $e->getMessage(); } } } // Handle Exit Impersonation Action for Super Admin if (isset($_GET['action']) && $_GET['action'] === 'exit_impersonation') { unset($_SESSION['is_impersonating']); unset($_SESSION['impersonated_store_id']); unset($_SESSION['shop_admin_id']); unset($_SESSION['shop_id']); unset($_SESSION['store_id']); unset($_SESSION['shop_admin_name']); unset($_SESSION['shop_name']); header("Location: platform-admin.php?tab=tenants&msg=impersonation_exited"); exit; } // Handle Logout Action if (isset($_GET['action']) && $_GET['action'] === 'logout') { unset($_SESSION['shop_admin_id']); unset($_SESSION['shop_id']); unset($_SESSION['store_id']); unset($_SESSION['shop_admin_name']); unset($_SESSION['shop_name']); unset($_SESSION['is_impersonating']); unset($_SESSION['impersonated_store_id']); session_destroy(); header("Location: login.php"); exit; } // STRICT MULTI-TENANT SECURITY CHECK: $is_super_admin = !empty($_SESSION['super_admin_logged_in']); $is_impersonating = !empty($_SESSION['is_impersonating']); $is_shop_admin = !empty($_SESSION['shop_admin_id']); if (!$is_shop_admin && !$is_super_admin) { header("Location: login.php"); exit; } // Reject Direct Super Admin Access without Impersonation Token if ($is_super_admin && !$is_impersonating) { header("Location: platform-admin.php?tab=tenants&error=direct_store_access_denied"); exit; } $store_id = 0; if ($is_super_admin && $is_impersonating) { $store_id = intval($_SESSION['impersonated_store_id'] ?? $_SESSION['shop_id'] ?? 0); } else { $store_id = intval($_SESSION['shop_id'] ?? $_SESSION['store_id'] ?? 0); } if ($store_id <= 0) { if ($is_super_admin) { header("Location: platform-admin.php?tab=tenants&error=invalid_store"); exit; } else { header("Location: login.php"); exit; } } // Synchronize Session Key Pair $_SESSION['shop_id'] = $store_id; $_SESSION['store_id'] = $store_id; $store_info = null; $plan_info = null; // Fetch store details & plan try { $stmt_store = $pdo->prepare("SELECT t.*, p.name as plan_name, p.limit_tables FROM tenants t LEFT JOIN plans p ON t.plan_id = p.id WHERE t.store_id = :store_id"); $stmt_store->execute([':store_id' => $store_id]); $store_info = $stmt_store->fetch(); if (!$store_info) { // Fallback: If specific tenant not found, fetch first available tenant $stmt_store_fb = $pdo->query("SELECT t.*, p.name as plan_name, p.limit_tables FROM tenants t LEFT JOIN plans p ON t.plan_id = p.id ORDER BY t.store_id ASC LIMIT 1"); $store_info = $stmt_store_fb->fetch(); if ($store_info) { $store_id = intval($store_info['store_id']); $_SESSION['shop_id'] = $store_id; $_SESSION['store_id'] = $store_id; } } } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการดึงข้อมูลร้านค้า: " . $e->getMessage(); } // Calculate 1-Month Free Trial Expiration Status $is_trial_expired = false; $store_st = strtolower($store_info['status'] ?? 'trial'); $trial_ends_ts = !empty($store_info['trial_ends_at']) ? strtotime($store_info['trial_ends_at']) : 0; $trial_days_left = ($trial_ends_ts > time()) ? ceil(($trial_ends_ts - time()) / 86400) : 0; if (!$is_super_admin && $store_st !== 'active' && $store_st !== 'paid') { if ($trial_ends_ts === 0 || $trial_ends_ts < time()) { $is_trial_expired = true; } } // Handle Subscription Renewal Payment Submission if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_subscription_payment'])) { $plan_id = intval($_POST['plan_id'] ?? 2); $slip_url = NULL; 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'])) { if ($_FILES['slip_file']['size'] <= 5 * 1024 * 1024) { // 5MB limit $filename = 'sub_slip_' . $store_id . '_' . time() . '.' . $ext; if (processAndCompressImage($_FILES['slip_file']['tmp_name'], $upload_dir . $filename, 1200, 80)) { $slip_url = 'uploads/slips/' . $filename; } } else { $error_msg = "⚠️ ขนาดไฟล์สลิปการโอนเงินใหญ่เกินไป (ต้องไม่เกิน 5MB)"; } } else { $error_msg = "⚠️ ไฟล์สลิปการโอนเงินต้องเป็นไฟล์รูปภาพ (JPG, PNG, WEBP) เท่านั้น"; } } elseif (!empty($_POST['slip_url'])) { $slip_url = trim($_POST['slip_url']); } if (empty($error_msg)) { if (empty($slip_url)) { $error_msg = "⚠️ กรุณาอัปโหลดไฟล์สลิปการโอนเงินจริงก่อนกดยืนยันชำระเงิน (ห้ามส่งข้อมูลหากยังไม่ได้โอนเงิน)"; } else { try { $stmt_p = $pdo->prepare("SELECT price FROM plans WHERE id = :id"); $stmt_p->execute([':id' => $plan_id]); $p_price = floatval($stmt_p->fetchColumn() ?: 990.00); $stmt_pay = $pdo->prepare("INSERT INTO payments (store_id, plan_id, amount, slip_url, status, note) VALUES (:store_id, :plan_id, :amount, :slip_url, 'pending', 'แจ้งชำระเงินโดยร้านค้า (รอการตรวจสอบสลิปโดย Super Admin)')"); $stmt_pay->execute([ ':store_id' => $store_id, ':plan_id' => $plan_id, ':amount' => $p_price, ':slip_url' => $slip_url ]); $success_msg = "✅ ส่งข้อมูลแจ้งชำระเงินสมัครแพ็กเกจเรียบร้อยแล้ว! สลิปถูกส่งเข้าสู่ระบบ และรอ Super Admin ตรวจสอบอนุมัติ"; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการบันทึกข้อมูลการชำระเงิน: " . $e->getMessage(); } } } } // Block operational POST requests if trial is expired if ($is_trial_expired && $_SERVER['REQUEST_METHOD'] === 'POST' && !isset($_POST['submit_subscription_payment'])) { $error_msg = "⚠️ ระยะเวลาทดลองใช้งานฟรี (Free Trial) ของท่านหมดอายุแล้ว กรุณาสมัครแพ็กเกจเพื่อเปิดใช้งานระบบต่อ"; } // JSON API endpoint for auto polling live orders in Admin Panel if (isset($_GET['fetch_live_orders'])) { if (ob_get_length()) ob_clean(); header('Content-Type: application/json; charset=utf-8'); try { $stmt_orders = $pdo->prepare("SELECT o.*, DATE_FORMAT(o.created_at, '%H:%i:%s') as order_time FROM orders o WHERE o.store_id = :store_id ORDER BY o.id DESC LIMIT 50"); $stmt_orders->execute([':store_id' => $store_id]); $orders_list = $stmt_orders->fetchAll(PDO::FETCH_ASSOC); foreach ($orders_list as &$ord) { $stmt_items = $pdo->prepare("SELECT oi.*, m.name as menu_name FROM order_items oi LEFT JOIN menus m ON oi.menu_id = m.menu_id WHERE oi.order_id = :order_id"); $stmt_items->execute([':order_id' => $ord['id']]); $ord['items'] = $stmt_items->fetchAll(PDO::FETCH_ASSOC); } echo json_encode(['success' => true, 'orders' => $orders_list]); } catch (Exception $e) { echo json_encode(['success' => false, 'error' => $e->getMessage()]); } exit; } // Export Sales Data to CSV endpoint for Microsoft Excel in Thai if (isset($_GET['export_sales_csv'])) { if (ob_get_length()) ob_clean(); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="sales_report_store_' . $store_id . '_' . date('Y-m-d') . '.csv"'); $output = fopen('php://output', 'w'); // Write UTF-8 BOM for Excel Thai language compatibility fprintf($output, chr(0xEF).chr(0xBB).chr(0xBF)); fputcsv($output, ['เลขที่ออเดอร์', 'หมายเลขโต๊ะ', 'ยอดรวม (บาท)', 'สถานะ', 'วันที่เวลา']); try { $stmt_exp = $pdo->prepare("SELECT o.id, o.table_number, COALESCE(SUM(oi.price * oi.quantity), 0) as total, o.status, DATE_FORMAT(o.created_at, '%d/%m/%Y %H:%i:%s') as created_at FROM orders o LEFT JOIN order_items oi ON o.id = oi.order_id WHERE o.store_id = :store_id GROUP BY o.id ORDER BY o.id DESC"); $stmt_exp->execute([':store_id' => $store_id]); while ($row = $stmt_exp->fetch(PDO::FETCH_ASSOC)) { $status_str = 'กำลังดำเนินการ'; if ($row['status'] === 'completed') $status_str = 'เช็กบิลแล้ว'; elseif ($row['status'] === 'cancelled') $status_str = 'ยกเลิก'; elseif ($row['status'] === 'preparing') $status_str = 'กำลังปรุง'; elseif ($row['status'] === 'ready' || $row['status'] === 'served') $status_str = 'เสิร์ฟแล้ว'; fputcsv($output, [ '#' . $row['id'], $row['table_number'], number_format($row['total'], 2), $status_str, $row['created_at'] ]); } } catch (Exception $e) {} fclose($output); exit; } // 1. Update Profile & Promotions / Policies / Store Images & Contacts if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_store_profile'])) { $store_name = trim($_POST['store_name'] ?? ''); $category = trim($_POST['category'] ?? ''); $address = trim($_POST['address'] ?? ''); $phone = trim($_POST['phone'] ?? ''); $line_id = trim($_POST['line_id'] ?? ''); $promo_banner = trim($_POST['promo_banner'] ?? ''); $policy_text = trim($_POST['policy_text'] ?? ''); $custom_logo_url = trim($_POST['custom_logo_url'] ?? ''); $store_banner_url = trim($_POST['store_banner_url'] ?? ''); // Handle Store Logo File Upload if (isset($_FILES['logo_file']) && $_FILES['logo_file']['error'] === UPLOAD_ERR_OK) { $upload_dir = __DIR__ . '/uploads/store/'; if (!is_dir($upload_dir)) @mkdir($upload_dir, 0777, true); $ext = strtolower(pathinfo($_FILES['logo_file']['name'], PATHINFO_EXTENSION)); if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'jfif', 'avif', 'svg', 'bmp', 'heic', 'heif'])) { $filename = 'logo_' . $store_id . '_' . time() . '.' . $ext; if (processAndCompressImage($_FILES['logo_file']['tmp_name'], $upload_dir . $filename, 1200, 80)) { $custom_logo_url = 'uploads/store/' . $filename; } } } // Handle Store Cover Banner File Upload if (isset($_FILES['banner_file']) && $_FILES['banner_file']['error'] === UPLOAD_ERR_OK) { $upload_dir = __DIR__ . '/uploads/store/'; if (!is_dir($upload_dir)) @mkdir($upload_dir, 0777, true); $ext = strtolower(pathinfo($_FILES['banner_file']['name'], PATHINFO_EXTENSION)); if (in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'gif', 'jfif', 'avif', 'svg', 'bmp', 'heic', 'heif'])) { $filename = 'banner_' . $store_id . '_' . time() . '.' . $ext; if (processAndCompressImage($_FILES['banner_file']['tmp_name'], $upload_dir . $filename, 1200, 80)) { $store_banner_url = 'uploads/store/' . $filename; } } } if (empty($store_name)) { $error_msg = 'กรุณาระบุชื่อร้านอาหาร'; } else { try { $stmt = $pdo->prepare("UPDATE tenants SET store_name = :store_name, category = :category, address = :address, phone = :phone, line_id = :line_id, promo_banner = :promo_banner, policy_text = :policy_text, custom_logo_url = :custom_logo_url, store_banner_url = :store_banner_url WHERE store_id = :store_id"); $stmt->execute([ ':store_name' => $store_name, ':category' => $category, ':address' => $address, ':phone' => $phone, ':line_id' => $line_id, ':promo_banner' => $promo_banner, ':policy_text' => $policy_text, ':custom_logo_url' => $custom_logo_url, ':store_banner_url' => $store_banner_url, ':store_id' => $store_id ]); $success_msg = "อัปเดตข้อมูล โลโก้ ภาพปกแบนเนอร์ และนโยบายร้านค้าเรียบร้อยแล้ว!"; $_SESSION['shop_name'] = $store_name; // Refresh local store_info $store_info['store_name'] = $store_name; $store_info['category'] = $category; $store_info['address'] = $address; $store_info['phone'] = $phone; $store_info['line_id'] = $line_id; $store_info['promo_banner'] = $promo_banner; $store_info['policy_text'] = $policy_text; $store_info['custom_logo_url'] = $custom_logo_url; $store_info['store_banner_url'] = $store_banner_url; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการอัปเดตข้อมูล: " . $e->getMessage(); } } } // 2. Menu Item Management (CRUD) // Add Menu Item if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_menu_item'])) { $name = trim($_POST['menu_name'] ?? ''); $price = floatval($_POST['menu_price'] ?? 0); $category = trim($_POST['menu_category'] ?? ''); $unit = trim($_POST['menu_unit'] ?? ''); if (empty($unit)) { $unit = getItemUnitPHP($name, $category); } $description = trim($_POST['menu_description'] ?? ''); $spice_options = trim($_POST['menu_spice_options'] ?? ''); if (empty($spice_options)) $spice_options = 'เผ็ดปกติ,ไม่เผ็ด,เผ็ดน้อย,เผ็ดมาก'; $image_url = trim($_POST['menu_image_url'] ?? ''); $is_available = isset($_POST['menu_is_available']) ? 1 : 0; // Handle Image File Upload if uploaded from computer if (isset($_FILES['menu_image_file']) && $_FILES['menu_image_file']['error'] === UPLOAD_ERR_OK) { $upload_dir = __DIR__ . '/uploads/menus/'; if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0777, true); } $file_ext = strtolower(pathinfo($_FILES['menu_image_file']['name'], PATHINFO_EXTENSION)); $allowed_exts = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'jfif', 'avif', 'svg', 'bmp', 'heic', 'heif']; if (in_array($file_ext, $allowed_exts)) { $new_filename = 'menu_' . $store_id . '_' . time() . '_' . rand(100, 999) . '.' . $file_ext; if (processAndCompressImage($_FILES['menu_image_file']['tmp_name'], $upload_dir . $new_filename, 1200, 80)) { $image_url = 'uploads/menus/' . $new_filename; } } } if (empty($name) || $price <= 0 || empty($category)) { $error_msg = "กรุณากรอกข้อมูลอาหารหลักให้ครบถ้วน"; } else { try { $stmt_max_m = $pdo->prepare("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM menus WHERE store_id = :store_id"); $stmt_max_m->execute([':store_id' => $store_id]); $next_menu_sort = intval($stmt_max_m->fetchColumn()); $stmt = $pdo->prepare("INSERT INTO menus (store_id, name, price, category, unit, description, image_url, is_available, spice_options, sort_order) VALUES (:store_id, :name, :price, :category, :unit, :description, :image_url, :is_available, :spice_options, :sort_order)"); $stmt->execute([ ':store_id' => $store_id, ':name' => $name, ':price' => $price, ':category' => $category, ':unit' => $unit, ':description' => $description, ':image_url' => $image_url, ':is_available' => $is_available, ':spice_options' => $spice_options, ':sort_order' => $next_menu_sort ]); $success_msg = "เพิ่มรายการอาหาร '{$name}' พร้อมตัวเลือกและรูปภาพเรียบร้อยแล้ว!"; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการเพิ่มเมนู: " . $e->getMessage(); } } } // Edit Menu Item if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_menu_item'])) { $menu_id = intval($_POST['menu_id'] ?? 0); $name = trim($_POST['menu_name'] ?? ''); $price = floatval($_POST['menu_price'] ?? 0); $category = trim($_POST['menu_category'] ?? ''); $unit = trim($_POST['menu_unit'] ?? ''); if (empty($unit)) { $unit = getItemUnitPHP($name, $category); } $description = trim($_POST['menu_description'] ?? ''); $spice_options = trim($_POST['menu_spice_options'] ?? ''); if (empty($spice_options)) $spice_options = 'เผ็ดปกติ,ไม่เผ็ด,เผ็ดน้อย,เผ็ดมาก'; $image_url = trim($_POST['menu_image_url'] ?? ''); $is_available = isset($_POST['menu_is_available']) ? 1 : 0; // Fetch existing image_url if no new URL passed if (empty($image_url) && $menu_id > 0) { try { $stmt_curr = $pdo->prepare("SELECT image_url FROM menus WHERE menu_id = :menu_id AND store_id = :store_id"); $stmt_curr->execute([':menu_id' => $menu_id, ':store_id' => $store_id]); $curr_img = $stmt_curr->fetchColumn(); if ($curr_img) $image_url = $curr_img; } catch (Exception $e) {} } // Handle Image File Upload if uploaded from computer if (isset($_FILES['menu_image_file']) && $_FILES['menu_image_file']['error'] === UPLOAD_ERR_OK) { $upload_dir = __DIR__ . '/uploads/menus/'; if (!is_dir($upload_dir)) { @mkdir($upload_dir, 0777, true); } $file_ext = strtolower(pathinfo($_FILES['menu_image_file']['name'], PATHINFO_EXTENSION)); $allowed_exts = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'jfif', 'avif', 'svg', 'bmp', 'heic', 'heif']; if (in_array($file_ext, $allowed_exts)) { $new_filename = 'menu_' . $store_id . '_' . time() . '_' . rand(100, 999) . '.' . $file_ext; if (processAndCompressImage($_FILES['menu_image_file']['tmp_name'], $upload_dir . $new_filename, 1200, 80)) { $image_url = 'uploads/menus/' . $new_filename; } } } if (empty($name) || $price <= 0 || empty($category) || $menu_id === 0) { $error_msg = "กรุณากรอกข้อมูลแก้ไขให้ครบถ้วน"; } else { try { $stmt = $pdo->prepare("UPDATE menus SET name = :name, price = :price, category = :category, unit = :unit, image_url = :image_url, is_available = :is_available, spice_options = :spice_options WHERE menu_id = :menu_id AND store_id = :store_id"); $stmt->execute([ ':name' => $name, ':price' => $price, ':category' => $category, ':unit' => $unit, ':image_url' => $image_url, ':is_available' => $is_available, ':spice_options' => $spice_options, ':menu_id' => $menu_id, ':store_id' => $store_id ]); $success_msg = "แก้ไขรายการอาหาร '{$name}' พร้อมตัวเลือกเรียบร้อยแล้ว!"; $active_tab = 'menu-mgr'; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการแก้ไขเมนู: " . $e->getMessage(); } } } // Delete Menu Item if ($is_logged_in && isset($_GET['action']) && $_GET['action'] === 'delete_menu' && isset($_GET['menu_id'])) { $menu_id = intval($_GET['menu_id']); try { $stmt = $pdo->prepare("DELETE FROM menus WHERE menu_id = :menu_id AND store_id = :store_id"); $stmt->execute([ ':menu_id' => $menu_id, ':store_id' => $store_id ]); $success_msg = "ลบรายการอาหารเรียบร้อย"; $active_tab = 'menu-mgr'; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการลบรายการอาหาร: " . $e->getMessage(); } } // Add Category if ($is_logged_in && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_category'])) { $cat_name = trim($_POST['category_name'] ?? ''); if (empty($cat_name)) { $error_msg = "กรุณากรอกชื่อหมวดหมู่"; } else { try { $stmt_max = $pdo->prepare("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM categories WHERE store_id = :store_id"); $stmt_max->execute([':store_id' => $store_id]); $next_sort = intval($stmt_max->fetchColumn()); $stmt_ins = $pdo->prepare("INSERT INTO categories (store_id, name, sort_order, is_active) VALUES (:store_id, :name, :sort_order, 1)"); $stmt_ins->execute([ ':store_id' => $store_id, ':name' => $cat_name, ':sort_order' => $next_sort ]); $success_msg = "เพิ่มหมวดหมู่ '{$cat_name}' เรียบร้อยแล้ว!"; $active_tab = 'menu-mgr'; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการเพิ่มหมวดหมู่: " . $e->getMessage(); } } } // Edit Category if ($is_logged_in && $_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_category'])) { $cat_id = intval($_POST['category_id'] ?? 0); $cat_name = trim($_POST['category_name'] ?? ''); if (empty($cat_name) || $cat_id <= 0) { $error_msg = "กรุณากรอกชื่อหมวดหมู่ให้ถูกต้อง"; } else { try { $stmt_old = $pdo->prepare("SELECT name FROM categories WHERE id = :id AND store_id = :store_id"); $stmt_old->execute([':id' => $cat_id, ':store_id' => $store_id]); $old_name = $stmt_old->fetchColumn(); $stmt_upd = $pdo->prepare("UPDATE categories SET name = :name WHERE id = :id AND store_id = :store_id"); $stmt_upd->execute([':name' => $cat_name, ':id' => $cat_id, ':store_id' => $store_id]); if ($old_name && $old_name !== $cat_name) { $stmt_menus_upd = $pdo->prepare("UPDATE menus SET category = :new_name WHERE store_id = :store_id AND category = :old_name"); $stmt_menus_upd->execute([':new_name' => $cat_name, ':store_id' => $store_id, ':old_name' => $old_name]); } $success_msg = "แก้ไขหมวดหมู่เรียบร้อยแล้ว!"; $active_tab = 'menu-mgr'; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการแก้ไขหมวดหมู่: " . $e->getMessage(); } } } // Delete Category if ($is_logged_in && isset($_GET['action']) && $_GET['action'] === 'delete_category' && isset($_GET['category_id'])) { $cat_id = intval($_GET['category_id']); try { $stmt = $pdo->prepare("DELETE FROM categories WHERE id = :id AND store_id = :store_id"); $stmt->execute([':id' => $cat_id, ':store_id' => $store_id]); $success_msg = "ลบหมวดหมู่เรียบร้อยแล้ว!"; $active_tab = 'menu-mgr'; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการลบหมวดหมู่: " . $e->getMessage(); } } // Toggle Category Status if ($is_logged_in && isset($_GET['action']) && $_GET['action'] === 'toggle_category' && isset($_GET['category_id'])) { $cat_id = intval($_GET['category_id']); try { $stmt = $pdo->prepare("UPDATE categories SET is_active = IF(is_active = 1, 0, 1) WHERE id = :id AND store_id = :store_id"); $stmt->execute([':id' => $cat_id, ':store_id' => $store_id]); $success_msg = "อัปเดตสถานะหมวดหมู่เรียบร้อย!"; $active_tab = 'menu-mgr'; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการเปลี่ยนสถานะหมวดหมู่: " . $e->getMessage(); } } // Helper to refresh table QR token (manual reset only — does NOT change qr_code_url) // Static QR Architecture: The URL embedded in the physical sticker is PERMANENT. // Only the optional qr_token field is regenerated here (for legacy token checks if needed). if (!function_exists('refreshTableQRToken')) { function refreshTableQRToken($pdo, $store_id, $table_number) { try { $num_only = preg_replace('/[^0-9]/', '', $table_number); if (empty($num_only)) $num_only = $table_number; $t_name_1 = "Table " . $num_only; $t_name_2 = $num_only; $t_name_3 = $table_number; // Only update qr_token — qr_code_url stays STATIC forever $new_token = substr(md5(uniqid(mt_rand(), true)), 0, 10); $stmt = $pdo->prepare("UPDATE tables_qr SET qr_token = :token WHERE store_id = :store_id AND (table_number = :t1 OR table_number = :t2 OR table_number = :t3)"); $stmt->execute([ ':token' => $new_token, ':store_id' => $store_id, ':t1' => $t_name_1, ':t2' => $t_name_2, ':t3' => $t_name_3 ]); return $new_token; } catch (Exception $e) { return null; } } } // Helper to reset table occupancy state on checkout (makes table ready for next guest) if (!function_exists('releaseTableSession')) { function releaseTableSession($pdo, $store_id, $table_number) { try { $num_only = preg_replace('/[^0-9]/', '', $table_number); if (empty($num_only)) $num_only = $table_number; $stmt = $pdo->prepare( "UPDATE tables_qr SET table_status = 'available', table_guest_name = NULL, session_token = NULL " . "WHERE store_id = :store_id AND (table_number = :t1 OR table_number = :t2 OR table_number = :t3)" ); $stmt->execute([ ':store_id' => $store_id, ':t1' => "Table " . $num_only, ':t2' => $num_only, ':t3' => $table_number ]); return true; } catch (Exception $e) { return false; } } } // Manual QR Token Reset Action if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['reset_table_qr'])) { $table_id = intval($_POST['table_id'] ?? 0); $table_num = trim($_POST['table_number'] ?? ''); if (!empty($table_num)) { $newToken = refreshTableQRToken($pdo, $store_id, $table_num); if ($newToken) { $success_msg = "สร้างและรีเซ็ต QR Code ชุดใหม่สำหรับ โต๊ะ " . htmlspecialchars($table_num) . " เรียบร้อยแล้ว! 🔄"; } } } // 2.8 Update Store Profile / Store Details Handler if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_store_profile'])) { $store_name = trim($_POST['store_name'] ?? ''); $category = trim($_POST['category'] ?? ''); $custom_logo_url = trim($_POST['custom_logo_url'] ?? ''); $address = trim($_POST['address'] ?? ''); $promo_banner = trim($_POST['promo_banner'] ?? ''); $policy_text = trim($_POST['policy_text'] ?? ''); if (empty($store_name)) { $error_msg = "กรุณาระบุชื่อร้านอาหาร"; } else { try { $stmt_update_store = $pdo->prepare(" UPDATE tenants SET store_name = :store_name, category = :category, custom_logo_url = :custom_logo_url, address = :address, promo_banner = :promo_banner, policy_text = :policy_text WHERE store_id = :store_id "); $stmt_update_store->execute([ ':store_name' => $store_name, ':category' => $category, ':custom_logo_url' => $custom_logo_url, ':address' => $address, ':promo_banner' => $promo_banner, ':policy_text' => $policy_text, ':store_id' => $store_id ]); // Refresh $store_info array immediately after saving $stmt_info = $pdo->prepare(" SELECT t.*, p.name as plan_name, p.limit_tables FROM tenants t LEFT JOIN plans p ON t.plan_id = p.id WHERE t.store_id = :store_id "); $stmt_info->execute([':store_id' => $store_id]); $store_info = $stmt_info->fetch(); // Update session store name if set if (isset($_SESSION['shop_name'])) { $_SESSION['shop_name'] = $store_info['store_name']; } $success_msg = "✅ บันทึกและอัปเดตรายละเอียดข้อมูลร้านอาหารเรียบร้อยแล้ว!"; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการบันทึกข้อมูลร้านค้า: " . $e->getMessage(); } } } // 3. Generate Table QRs if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['generate_tables'])) { $table_count = intval($_POST['table_count'] ?? 0); $limit = intval($store_info['limit_tables']); if ($table_count <= 0 || $table_count > $limit) { $error_msg = "จำนวนโต๊ะอาหารต้องมากกว่า 0 และไม่เกินจำนวนโควต้าของแพ็กเกจท่าน ({$limit} โต๊ะ)"; } else { try { $pdo->beginTransaction(); // Clear existing tables $stmtClear = $pdo->prepare("DELETE FROM tables_qr WHERE store_id = :store_id"); $stmtClear->execute([':store_id' => $store_id]); // Insert new table set $s_name = $store_info['store_name'] ?? ''; $encoded_shop_name = urlencode($s_name); $stmtInsert = $pdo->prepare("INSERT INTO tables_qr (store_id, table_number, qr_code_url, table_status) VALUES (:store_id, :table_number, :qr_code_url, 'available')"); for ($i = 1; $i <= $table_count; $i++) { $table_number = "Table " . $i; // Static URL — no token embedded; physical QR sticker never needs reprinting $qr_code_url = "menu.php?store_id={$store_id}&table={$i}"; $stmtInsert->execute([ ':store_id' => $store_id, ':table_number' => $table_number, ':qr_code_url' => $qr_code_url ]); } $pdo->commit(); $success_msg = "สร้างป้าย QR Code ประจำโต๊ะอาหารทั้งหมด {$table_count} โต๊ะ เรียบร้อยแล้ว!"; } catch (PDOException $e) { $pdo->rollBack(); $error_msg = "เกิดข้อผิดพลาดในการสร้างโต๊ะอาหาร: " . $e->getMessage(); } } } // 4. Update Order Status Action if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_order_status'])) { $order_id = intval($_POST['order_id'] ?? 0); $new_status = trim($_POST['new_status'] ?? ''); $allowed_statuses = ['pending', 'preparing', 'served', 'ready', 'completed', 'cancelled']; if ($order_id > 0 && in_array($new_status, $allowed_statuses)) { try { $stmt = $pdo->prepare("UPDATE orders SET status = :status WHERE id = :id AND store_id = :store_id"); $stmt->execute([':status' => $new_status, ':id' => $order_id, ':store_id' => $store_id]); $success_msg = "อัปเดตสถานะออเดอร์ #{$order_id} เรียบร้อยแล้ว"; // On checkout/served/completed: release table session so next customer sees Authenticator if ($new_status === 'completed' || $new_status === 'served') { $stmt_ord = $pdo->prepare("SELECT table_number FROM orders WHERE id = :id AND store_id = :store_id"); $stmt_ord->execute([':id' => $order_id, ':store_id' => $store_id]); $ord_info = $stmt_ord->fetch(); if ($ord_info && !empty($ord_info['table_number'])) { releaseTableSession($pdo, $store_id, $ord_info['table_number']); $success_msg = "อัปเดตออเดอร์ #{$order_id} ({$new_status}) และคืนสถานะโต๊ะ " . htmlspecialchars($ord_info['table_number']) . " ให้พร้อมรับลูกค้าใหม่เรียบร้อย 🔄"; } } } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการอัปเดตสถานะออเดอร์: " . $e->getMessage(); } } } // 5. Whole Table Checkout & Clear Session Action if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['checkout_table'])) { $table_num = trim($_POST['table_number'] ?? ''); if (!empty($table_num)) { try { $num_only = preg_replace('/[^0-9]/', '', $table_num); if (empty($num_only)) $num_only = $table_num; $t1 = "Table " . $num_only; $t2 = $num_only; $t3 = $table_num; // Mark all active orders for this table as completed $stmt = $pdo->prepare("UPDATE orders SET status = 'completed' WHERE store_id = :store_id AND (table_number = :t1 OR table_number = :t2 OR table_number = :t3) AND status IN ('unpaid', 'pending', 'preparing', 'ready', 'served')"); $stmt->execute([':store_id' => $store_id, ':t1' => $t1, ':t2' => $t2, ':t3' => $t3]); // Release table session — sets table_status='available' so next scanner sees Authenticator releaseTableSession($pdo, $store_id, $table_num); $success_msg = "เช็กบิลและเคลียร์โต๊ะ " . htmlspecialchars($table_num) . " เรียบร้อยแล้ว! โต๊ะพร้อมรับลูกค้าใหม่ 🔄"; } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการเช็กบิลโต๊ะ: " . $e->getMessage(); } } } // 6. Toggle Order Payment Method Action (โอนจ่ายแล้ว 💳 vs ชำระเงินสด/ยังไม่จ่าย 💵) if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['toggle_payment_method'])) { $order_id = intval($_POST['order_id'] ?? 0); if ($order_id > 0) { try { $stmt_curr = $pdo->prepare("SELECT payment_method, is_paid FROM orders WHERE id = :id AND store_id = :store_id"); $stmt_curr->execute([':id' => $order_id, ':store_id' => $store_id]); $curr_pay = $stmt_curr->fetch(PDO::FETCH_ASSOC); if ($curr_pay) { $next_method = ($curr_pay['payment_method'] === 'transfer' || $curr_pay['is_paid'] == 1) ? 'cash' : 'transfer'; $next_is_paid = ($next_method === 'transfer') ? 1 : 0; $stmt_tog = $pdo->prepare("UPDATE orders SET payment_method = :pm, is_paid = :ip WHERE id = :id AND store_id = :store_id"); $stmt_tog->execute([':pm' => $next_method, ':ip' => $next_is_paid, ':id' => $order_id, ':store_id' => $store_id]); $pay_label = ($next_method === 'transfer') ? 'โอนจ่ายแล้ว 💳' : 'ชำระเงินสด / ยังไม่จ่าย 💵'; $success_msg = "อัปเดตสถานะชำระเงินของ ออเดอร์ #{$order_id} เป็น '{$pay_label}' เรียบร้อยแล้ว!"; } } catch (PDOException $e) { $error_msg = "เกิดข้อผิดพลาดในการอัปเดตช่องทางชำระเงิน: " . $e->getMessage(); } } } // Fetch categories $store_categories = []; try { $stmt_cats = $pdo->prepare("SELECT * FROM categories WHERE store_id = :store_id ORDER BY sort_order ASC, id ASC"); $stmt_cats->execute([':store_id' => $store_id]); $store_categories = $stmt_cats->fetchAll(); } catch (PDOException $e) {} // Fetch menus $menus = []; try { $stmt_menus = $pdo->prepare("SELECT * FROM menus WHERE store_id = :store_id ORDER BY sort_order ASC, menu_id DESC"); $stmt_menus->execute([':store_id' => $store_id]); $menus = $stmt_menus->fetchAll(); } catch (PDOException $e) { $error_msg = "ไม่สามารถเชื่อมต่อข้อมูลเมนูอาหารได้"; } $total_menus = count($menus); // Fetch total orders count for store $total_orders = 0; try { $stmt_ord_cnt = $pdo->prepare("SELECT COUNT(*) FROM orders WHERE store_id = :store_id"); $stmt_ord_cnt->execute([':store_id' => $store_id]); $total_orders = intval($stmt_ord_cnt->fetchColumn()); } catch (PDOException $e) {} // Fetch tables QR $tables = []; try { $stmt_tables = $pdo->prepare("SELECT * FROM tables_qr WHERE store_id = :store_id ORDER BY table_id ASC"); $stmt_tables->execute([':store_id' => $store_id]); $tables = $stmt_tables->fetchAll(); } catch (PDOException $e) { $error_msg = "ไม่สามารถเชื่อมต่อข้อมูลโต๊ะอาหารได้"; } // Fetch active table groups (active orders grouped by table) $active_table_groups = []; $pending_order_count = 0; try { $stmt_active = $pdo->prepare("SELECT o.*, DATE_FORMAT(o.created_at, '%H:%i:%s') as order_time FROM orders o WHERE o.store_id = :store_id AND o.status IN ('unpaid', 'pending', 'preparing', 'ready', 'served') ORDER BY o.id ASC"); $stmt_active->execute([':store_id' => $store_id]); $active_orders_raw = $stmt_active->fetchAll(PDO::FETCH_ASSOC); foreach ($active_orders_raw as $ord) { if ($ord['status'] === 'pending' || $ord['status'] === 'unpaid') { $pending_order_count++; } $stmt_items = $pdo->prepare("SELECT oi.*, m.name as menu_name FROM order_items oi LEFT JOIN menus m ON oi.menu_id = m.menu_id WHERE oi.order_id = :order_id"); $stmt_items->execute([':order_id' => $ord['id']]); $ord['items'] = $stmt_items->fetchAll(PDO::FETCH_ASSOC); $tbl_key = preg_replace('/[^0-9]/', '', $ord['table_number']); if (empty($tbl_key)) $tbl_key = $ord['table_number']; if (!isset($active_table_groups[$tbl_key])) { $active_table_groups[$tbl_key] = [ 'table_display' => $ord['table_number'], 'table_num' => $tbl_key, 'grand_total' => 0, 'total_transfer_paid' => 0, 'total_unpaid_cash' => 0, 'orders' => [], 'all_items' => [] ]; } $ord_sum = 0; $is_ord_transfer = (isset($ord['payment_method']) && $ord['payment_method'] === 'transfer') || (!empty($ord['is_paid'])); foreach ($ord['items'] as $it) { $item_line_tot = ($it['price'] * $it['quantity']); $ord_sum += $item_line_tot; $active_table_groups[$tbl_key]['all_items'][] = [ 'name' => $it['menu_name'] ?: 'รายการอาหาร', 'quantity' => $it['quantity'], 'price' => $it['price'], 'spice_level' => $it['spice_level'] ?? '', 'note' => $it['note'] ?? '', 'is_transfer_paid' => $is_ord_transfer, 'payment_method' => $ord['payment_method'] ?? 'cash' ]; if ($is_ord_transfer) { $active_table_groups[$tbl_key]['total_transfer_paid'] += $item_line_tot; } else { $active_table_groups[$tbl_key]['total_unpaid_cash'] += $item_line_tot; } } $ord['order_sum'] = $ord_sum; $active_table_groups[$tbl_key]['grand_total'] += $ord_sum; $active_table_groups[$tbl_key]['orders'][] = $ord; } } catch (PDOException $e) { $active_table_groups = []; } // Selected Date for Sales Receipt History and Daily Reports (Supports DD/MM/YYYY, YYYY-MM-DD, or calendar selection) $raw_date_param = $_GET['sales_date'] ?? $_GET['history_date'] ?? date('Y-m-d'); if (preg_match('/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/', trim($raw_date_param), $m)) { $selected_date = sprintf('%04d-%02d-%02d', $m[3], $m[2], $m[1]); } elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', trim($raw_date_param))) { $selected_date = trim($raw_date_param); } else { $selected_date = date('Y-m-d'); } $selected_history_date = $selected_date; // Search & Filter parameters for Backdated Receipt / Order History $history_start_date = $_GET['history_start_date'] ?? $selected_date; $history_end_date = $_GET['history_end_date'] ?? $selected_date; $history_search = trim($_GET['history_search'] ?? ''); $history_payment = trim($_GET['history_payment'] ?? 'all'); // Fetch closed & historical sales orders maintaining original item price snapshot $closed_orders = []; try { $sql_hist = "SELECT o.*, DATE_FORMAT(o.created_at, '%d/%m/%Y %H:%i:%s') as order_time FROM orders o WHERE o.store_id = :store_id"; $params_hist = [':store_id' => $store_id]; if (!empty($history_start_date) && !empty($history_end_date)) { $sql_hist .= " AND DATE(o.created_at) BETWEEN :h_start AND :h_end"; $params_hist[':h_start'] = $history_start_date; $params_hist[':h_end'] = $history_end_date; } else { $sql_hist .= " AND DATE(o.created_at) = :h_date"; $params_hist[':h_date'] = $selected_date; } if ($history_payment === 'transfer') { $sql_hist .= " AND (o.payment_method = 'transfer' OR o.is_paid = 1)"; } elseif ($history_payment === 'cash') { $sql_hist .= " AND (o.payment_method = 'cash' AND o.is_paid = 0)"; } if (!empty($history_search)) { $search_id = intval(preg_replace('/[^0-9]/', '', $history_search)); if ($search_id > 0) { $sql_hist .= " AND (o.id = :search_id OR o.table_number LIKE :search_tbl)"; $params_hist[':search_id'] = $search_id; $params_hist[':search_tbl'] = '%' . $history_search . '%'; } else { $sql_hist .= " AND o.table_number LIKE :search_tbl"; $params_hist[':search_tbl'] = '%' . $history_search . '%'; } } $sql_hist .= " ORDER BY o.id DESC LIMIT 150"; $stmt_closed = $pdo->prepare($sql_hist); $stmt_closed->execute($params_hist); $closed_orders = $stmt_closed->fetchAll(PDO::FETCH_ASSOC); foreach ($closed_orders as &$cord) { // Retrieve itemized snapshot with price at transaction time $stmt_items = $pdo->prepare("SELECT oi.*, m.name as menu_name, m.category FROM order_items oi LEFT JOIN menus m ON oi.menu_id = m.menu_id WHERE oi.order_id = :order_id"); $stmt_items->execute([':order_id' => $cord['id']]); $cord['items'] = $stmt_items->fetchAll(PDO::FETCH_ASSOC); // Compute exact historical total using stored order_items.price $c_total = 0; foreach ($cord['items'] as $cit) { $c_total += (floatval($cit['price']) * intval($cit['quantity'])); } $cord['calculated_total'] = $c_total; } unset($cord); } catch (PDOException $e) { $closed_orders = []; } // Hourly Sales & Order Volume Breakdown for Hourly Trend Chart $hourly_sales_raw = []; try { $stmt_hourly = $pdo->prepare(" SELECT HOUR(o.created_at) as order_hour, COUNT(DISTINCT o.id) as order_count, COALESCE(SUM(oi.price * oi.quantity), 0) as total_revenue FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.store_id = :store_id AND o.status = 'completed' AND DATE(o.created_at) = :selected_date GROUP BY HOUR(o.created_at) ORDER BY order_hour ASC "); $stmt_hourly->execute([':store_id' => $store_id, ':selected_date' => $selected_date]); $hourly_sales_raw = $stmt_hourly->fetchAll(PDO::FETCH_ASSOC); } catch (PDOException $e) { $hourly_sales_raw = []; } $hourly_map = []; foreach ($hourly_sales_raw as $h) { $hourly_map[intval($h['order_hour'])] = [ 'revenue' => floatval($h['total_revenue']), 'count' => intval($h['order_count']) ]; } $hourly_chart_labels = []; $hourly_chart_revenues = []; $hourly_chart_counts = []; for ($hr = 8; $hr <= 23; $hr++) { $hourly_chart_labels[] = sprintf('%02d:00 น.', $hr); $hourly_chart_revenues[] = isset($hourly_map[$hr]) ? $hourly_map[$hr]['revenue'] : 0; $hourly_chart_counts[] = isset($hourly_map[$hr]) ? $hourly_map[$hr]['count'] : 0; } // Fetch daily sales statistics STRICTLY for selected date $total_revenue = 0; $total_completed = 0; $cancelled_count = 0; $daily_items = []; try { $stmt_daily = $pdo->prepare(" SELECT COUNT(DISTINCT o.id) as total_completed_orders, COALESCE(SUM(oi.price * oi.quantity), 0) as total_revenue FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.store_id = :store_id AND o.status = 'completed' AND DATE(o.created_at) = :selected_date "); $stmt_daily->execute([':store_id' => $store_id, ':selected_date' => $selected_date]); $daily_summary = $stmt_daily->fetch(PDO::FETCH_ASSOC); $total_revenue = floatval($daily_summary['total_revenue'] ?? 0); $total_completed = intval($daily_summary['total_completed_orders'] ?? 0); // Cancelled count for store on selected date $stmt_can = $pdo->prepare("SELECT COUNT(*) FROM orders WHERE store_id = :store_id AND status = 'cancelled' AND DATE(created_at) = :selected_date"); $stmt_can->execute([':store_id' => $store_id, ':selected_date' => $selected_date]); $cancelled_count = intval($stmt_can->fetchColumn()); // Itemized menu breakdown STRICTLY for selected date $stmt_itemized = $pdo->prepare(" SELECT m.name as menu_name, m.category, SUM(oi.quantity) as total_qty, SUM(oi.price * oi.quantity) as total_amount FROM orders o JOIN order_items oi ON o.id = oi.order_id LEFT JOIN menus m ON oi.menu_id = m.menu_id WHERE o.store_id = :store_id AND o.status = 'completed' AND DATE(o.created_at) = :selected_date GROUP BY oi.menu_id, m.name, m.category ORDER BY total_amount DESC "); $stmt_itemized->execute([':store_id' => $store_id, ':selected_date' => $selected_date]); $daily_items = $stmt_itemized->fetchAll(PDO::FETCH_ASSOC); // Category sales breakdown for Doughnut Chart $daily_categories = []; foreach ($daily_items as $di) { $cat = $di['category'] ?: 'ทั่วไป'; if (!isset($daily_categories[$cat])) { $daily_categories[$cat] = 0; } $daily_categories[$cat] += floatval($di['total_amount']); } } catch (PDOException $e) {} $vat_7 = $total_revenue * (7 / 107); $net_revenue = $total_revenue - $vat_7; $avg_order = $total_completed > 0 ? ($total_revenue / $total_completed) : 0; // Today and All-time revenue for Store Dashboard stat card $today_date = date('Y-m-d'); $today_revenue = 0; $all_time_revenue = 0; try { $stmt_today = $pdo->prepare("SELECT COALESCE(SUM(oi.price * oi.quantity), 0) FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.store_id = :store_id AND o.status = 'completed' AND DATE(o.created_at) = :today"); $stmt_today->execute([':store_id' => $store_id, ':today' => $today_date]); $today_revenue = floatval($stmt_today->fetchColumn()); $stmt_all = $pdo->prepare("SELECT COALESCE(SUM(oi.price * oi.quantity), 0) FROM orders o JOIN order_items oi ON o.id = oi.order_id WHERE o.store_id = :store_id AND o.status = 'completed'"); $stmt_all->execute([':store_id' => $store_id]); $all_time_revenue = floatval($stmt_all->fetchColumn()); } catch (PDOException $e) {} $display_dashboard_revenue = ($today_revenue > 0) ? $today_revenue : $all_time_revenue; // Order Status Distribution Chart Data for Selected Date $raw_chart_date = $_GET['chart_date'] ?? date('Y-m-d'); if (preg_match('/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/', trim($raw_chart_date), $cm)) { $selected_chart_date = sprintf('%04d-%02d-%02d', $cm[3], $cm[2], $cm[1]); } elseif (preg_match('/^\d{4}-\d{2}-\d{2}$/', trim($raw_chart_date))) { $selected_chart_date = trim($raw_chart_date); } else { $selected_chart_date = date('Y-m-d'); } $status_data = []; $total_chart_orders = 0; try { if ($store_id > 0) { $stmt_status_chart = $pdo->prepare(" SELECT status, COUNT(*) as count FROM orders WHERE store_id = :store_id AND DATE(created_at) = :chart_date GROUP BY status "); $stmt_status_chart->execute([':store_id' => $store_id, ':chart_date' => $selected_chart_date]); } else { // Super Admin / System Admin Multi-Tenant Aggregation (All Stores) $stmt_status_chart = $pdo->prepare(" SELECT status, COUNT(*) as count FROM orders WHERE DATE(created_at) = :chart_date GROUP BY status "); $stmt_status_chart->execute([':chart_date' => $selected_chart_date]); } $status_data = $stmt_status_chart->fetchAll(PDO::FETCH_ASSOC); foreach ($status_data as $sd) { $total_chart_orders += intval($sd['count']); } } catch (PDOException $e) { $status_data = []; } ?> <!DOCTYPE html> <html lang="th"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Store Admin Dashboard - SME Restaurant OS</title> <!-- Chart.js CDN --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <!-- SweetAlert2 CDN --> <script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <!-- Navigation Sound & SweetAlert Engine --> <script src="js/nav_sound_swal.js"></script> <style> :root { --primary: #1e293b; /* Modern Navy Blue */ --accent: #00E5FF; --dark-bg: #0f172a; /* Deep Slate Background */ --card-bg: #1e293b; --border-glass: rgba(0, 229, 255, 0.15); --text-primary: #f8fafc; /* Crisp light text */ --text-secondary: #94a3b8; } body { background-color: var(--dark-bg); color: var(--text-primary); font-family: 'Sarabun', 'Inter', sans-serif; margin: 0; padding: 0; } /* Dashboard Layout */ .dashboard-wrapper { display: flex; width: 100%; min-height: 100vh; } /* Sidebar Styles */ .sidebar { width: 280px; background: var(--primary); border-right: 1px solid rgba(255,255,255,0.06); display: flex; flex-direction: column; flex-shrink: 0; position: relative; z-index: 100; pointer-events: auto !important; } .menu-btn { pointer-events: auto !important; cursor: pointer !important; } .sidebar-header { padding: 30px 24px; border-bottom: 1px solid rgba(255,255,255,0.06); display: flex; align-items: center; gap: 12px; } .sidebar-header img { height: 40px; width: auto; object-fit: contain; } .sidebar-header h1 { font-size: 15px; margin: 0; font-weight: 800; color: var(--text-primary); } .sidebar-header p { font-size: 11px; margin: 2px 0 0 0; color: var(--text-secondary); } .sidebar-menu { padding: 24px 16px; display: flex; flex-direction: column; gap: 8px; flex-grow: 1; } .menu-btn { display: flex; align-items: center; gap: 12px; padding: 12px 16px; color: var(--text-secondary); text-decoration: none; font-size: 14px; font-weight: 700; border-radius: 8px; border: none; background: transparent; cursor: pointer; text-align: left; width: 100%; transition: all 0.2s ease; } .menu-btn:hover { background: rgba(255,255,255,0.04); color: var(--text-primary); } .menu-btn.active { background: var(--accent); color: #0A192F; } .sidebar-footer { padding: 20px; border-top: 1px solid rgba(255,255,255,0.06); } .sidebar-header { padding: 30px 24px; border-bottom: 1px solid rgba(255,255,255,0.06); display: flex; align-items: center; gap: 12px; } .sidebar-header img { height: 40px; width: auto; object-fit: contain; } .sidebar-header h1 { font-size: 15px; margin: 0; font-weight: 800; color: var(--text-primary); } .sidebar-header p { font-size: 11px; margin: 2px 0 0 0; color: var(--text-secondary); } .sidebar-menu { padding: 24px 16px; display: flex; flex-direction: column; gap: 8px; flex-grow: 1; } .menu-btn { display: flex; align-items: center; gap: 12px; padding: 12px 16px; color: var(--text-secondary); text-decoration: none; font-size: 14px; font-weight: 700; border-radius: 8px; border: none; background: transparent; cursor: pointer; text-align: left; width: 100%; transition: all 0.2s ease; } .menu-btn:hover { background: rgba(255,255,255,0.04); color: var(--text-primary); } .menu-btn.active { background: var(--accent); color: #0A192F; } .sidebar-footer { padding: 20px; border-top: 1px solid rgba(255,255,255,0.06); } /* Main Content Panel */ .main-content { flex-grow: 1; padding: 40px; box-sizing: border-box; overflow-y: auto; } /* Tab Sections */ .tab-sec { display: none; } .tab-sec.active { display: block; } /* Stats Grid */ .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 20px; margin-bottom: 35px; } .stat-card { background: var(--card-bg); border: 1px solid rgba(255,255,255,0.04); border-radius: 12px; padding: 24px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } .stat-info h3 { margin: 0 0 6px 0; font-size: 13px; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; } .stat-info div { font-size: 26px; font-weight: 800; color: var(--accent); } .stat-icon { font-size: 32px; } /* Split Grid Layout */ .split-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 30px; } @media (max-width: 992px) { .split-grid { grid-template-columns: 1fr; } } .card { background: var(--card-bg); border: 1px solid rgba(255,255,255,0.04); border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); margin-bottom: 30px; } /* Visual Charts Wrapper */ .charts-row { display: grid; grid-template-columns: 1fr; gap: 30px; margin-bottom: 40px; } .chart-card { background: var(--card-bg); border: 1px solid rgba(255,255,255,0.04); border-radius: 12px; padding: 25px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } .chart-card h3 { margin-top: 0; margin-bottom: 20px; font-size: 15px; color: var(--accent); border-left: 4px solid var(--accent); padding-left: 10px; } .chart-container { position: relative; height: 300px; width: 100%; max-width: 100%; overflow: visible; box-sizing: border-box; } .chart-container canvas { display: block; height: 100% !important; max-width: 100% !important; box-sizing: border-box; } /* Interactive controls */ .form-group { margin-bottom: 16px; } .form-label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--text-secondary); font-weight: 700; } .form-control { width: 100%; padding: 10px 12px; background: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.08); border-radius: 6px; color: #fff; box-sizing: border-box; font-family: inherit; } .form-control:focus { border-color: var(--accent); outline: none; } .btn-primary { background: var(--accent); color: #0A192F; border: none; padding: 12px 20px; font-weight: 800; border-radius: 6px; cursor: pointer; width: 100%; transition: 0.2s ease; } .btn-primary:hover { background: #00B4D8; } .btn-danger { background: #ff4d4d; color: #fff; padding: 6px 12px; font-size: 11px; border-radius: 4px; font-weight: bold; text-decoration: none; display: inline-block; border: none; cursor: pointer; } .btn-danger:hover { background: #ff3333; } .btn-sm { padding: 6px 12px; font-size: 11px; border-radius: 4px; font-weight: bold; text-decoration: none; display: inline-block; } .badge { padding: 3px 6px; border-radius: 4px; font-size: 11px; font-weight: bold; } .badge-plan { background: rgba(0, 229, 255, 0.1); color: var(--accent); border: 1px solid rgba(0, 229, 255, 0.2); } .alert-success { background: rgba(76, 175, 80, 0.1); border: 1px solid #4CAF50; color: #81C784; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 13.5px; } .alert-danger { background: rgba(244, 67, 54, 0.1); border: 1px solid #f44336; color: #E57373; padding: 12px; border-radius: 6px; margin-bottom: 20px; font-size: 13.5px; } /* .chart-container second definition removed — merged above */ html, body { overflow-x: hidden !important; max-width: 100vw !important; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box !important; } .mobile-bottom-nav { display: none !important; } /* Mobile & Smartphone Responsive UI System */ @media (max-width: 992px) { .header-console { display: none !important; } .chart-container { height: 360px !important; overflow: visible !important; } .chart-container canvas { height: 100% !important; } .mobile-topbar { display: flex !important; position: fixed !important; top: 0 !important; left: 0 !important; right: 0 !important; width: 100% !important; z-index: 99999 !important; background: #1e293b !important; box-shadow: 0 4px 15px rgba(0,0,0,0.4) !important; padding: 10px 14px !important; } .dashboard-wrapper { flex-direction: column !important; margin-top: 52px !important; } .sidebar { position: fixed !important; top: 52px !important; left: 0 !important; width: 100% !important; height: calc(100vh - 52px) !important; background: rgba(15, 23, 42, 0.98) !important; backdrop-filter: blur(12px) !important; z-index: 99998 !important; display: none !important; overflow-y: auto !important; box-sizing: border-box !important; padding: 15px !important; border-right: none !important; box-shadow: 0 20px 40px rgba(0,0,0,0.8); } .sidebar.mobile-show { display: flex !important; flex-direction: column !important; } .menu-btn { padding: 15px 18px !important; font-size: 15.5px !important; border-radius: 10px !important; margin-bottom: 6px !important; } .main-content { padding: 12px 10px 80px 10px !important; width: 100% !important; box-sizing: border-box !important; } .split-grid { grid-template-columns: 1fr !important; gap: 15px !important; } .stats-grid { grid-template-columns: 1fr 1fr !important; gap: 10px !important; } .stat-card { padding: 12px 10px !important; border-radius: 10px !important; min-width: 0 !important; box-sizing: border-box !important; } .stat-info { min-width: 0 !important; overflow: hidden !important; } .stat-info h3 { font-size: 11px !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; margin-bottom: 4px !important; } .stat-info div { font-size: 16px !important; font-weight: 800 !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } .stat-icon { font-size: 22px !important; } .card { padding: 16px 12px !important; border-radius: 10px !important; box-sizing: border-box !important; } .card h3, .chart-card h3 { font-size: 14px !important; line-height: 1.4 !important; word-wrap: break-word !important; white-space: normal !important; } .charts-row { grid-template-columns: 1fr !important; } .menu-item-card { flex-direction: column !important; align-items: flex-start !important; } .menu-item-card img { width: 100% !important; height: 140px !important; object-fit: cover !important; border-radius: 8px !important; } table { display: block; overflow-x: auto; white-space: nowrap; } .mobile-bottom-nav { display: flex !important; position: fixed; bottom: 0; left: 0; width: 100%; height: 60px; background: rgba(15, 23, 42, 0.96); backdrop-filter: blur(14px); border-top: 1px solid rgba(0, 229, 255, 0.2); z-index: 9990; justify-content: space-around; align-items: center; box-shadow: 0 -5px 25px rgba(0,0,0,0.5); box-sizing: border-box; padding: 4px 6px; } .mobile-nav-btn { background: none; border: none; color: var(--text-secondary); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px; font-size: 10.5px; font-weight: 700; font-family: inherit; cursor: pointer; padding: 6px 10px; border-radius: 8px; transition: all 0.2s ease; } .mobile-nav-btn.active { color: var(--accent); background: rgba(0, 229, 255, 0.12); } } /* Strictly hide mobile navigation bar on Desktop / PC screens */ @media (min-width: 993px) { .mobile-bottom-nav { display: none !important; } } .header-console { background: var(--primary); border-bottom: 1px solid var(--border-glass); padding: 15px 30px; display: flex; justify-content: space-between; align-items: center; } .header-left { display: flex; align-items: center; gap: 15px; } .header-left img { height: 40px; object-fit: contain; } .header-left h1 { font-size: 20px; margin: 0; color: var(--text-primary); } .container { max-width: 1200px; margin: 40px auto; padding: 0 20px; box-sizing: border-box; } .dashboard-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; } @media (max-width: 992px) { .dashboard-grid { grid-template-columns: 1fr; } } .card { background: var(--card-bg); border: 1px solid var(--border-glass); border-radius: 12px; padding: 30px; margin-bottom: 30px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); } .card h2 { margin-top: 0; margin-bottom: 20px; font-size: 18px; color: var(--accent); border-left: 4px solid var(--accent); padding-left: 12px; } .form-group { margin-bottom: 18px; } .form-label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--text-secondary); } .form-control { width: 100%; padding: 10px 14px; background: rgba(0,0,0,0.25); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 6px; color: #fff; box-sizing: border-box; } .form-control:focus { border-color: var(--accent); outline: none; } .btn-primary { background: var(--accent); color: #0A192F; border: none; padding: 12px; font-weight: bold; border-radius: 6px; cursor: pointer; width: 100%; transition: 0.3s; } .btn-primary:hover { background: #00B4D8; } .btn-sm { padding: 6px 12px; font-size: 12px; border-radius: 4px; text-decoration: none; cursor: pointer; border: none; font-weight: bold; } .btn-danger { background: rgba(244, 67, 54, 0.15); color: #f44336; } .btn-danger:hover { background: #f44336; color: #fff; } .alert-success { background: rgba(76, 175, 80, 0.1); border: 1px solid #4CAF50; color: #81C784; padding: 15px; border-radius: 6px; margin-bottom: 25px; } .alert-danger { background: rgba(244, 67, 54, 0.1); border: 1px solid #f44336; color: #E57373; padding: 15px; border-radius: 6px; margin-bottom: 25px; } .menu-item-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px; padding: 15px; display: flex; gap: 15px; margin-bottom: 15px; align-items: center; } .menu-item-img { width: 70px; height: 70px; border-radius: 6px; object-fit: cover; background: #111; } .menu-item-info { flex-grow: 1; } .menu-item-info h4 { margin: 0 0 5px 0; font-size: 15px; } .menu-item-info div { font-size: 13px; color: var(--text-secondary); } .qr-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 15px; } .qr-card { background: rgba(255,255,255,0.02); border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 8px; padding: 15px; text-align: center; } .qr-card img { width: 120px; height: 120px; margin-bottom: 10px; background: white; padding: 5px; border-radius: 4px; } </style> <!-- Chart.js Library CDN --> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> <!-- Top Bar --> <header class="header-console"> <div class="header-left"> <img src="logo.png" alt="CMTC Logo" style="height: 40px; width: auto; object-fit: contain;"> <div> <h1>แผงควบคุมร้านอาหาร (Store Console)</h1> <small style="color: var(--text-secondary);"> <?php echo $is_logged_in ? "ร้าน: " . htmlspecialchars($store_info['store_name']) . " (" . htmlspecialchars($store_info['plan_name']) . " Plan)" : "CMTC Tech Solution Platform"; ?> </small> </div> </div> <div> <?php if ($is_logged_in): ?> <a href="store-admin.php?action=logout" class="btn-sm btn-danger" style="padding: 10px 18px;">Logout 🚪</a> <?php endif; ?> </div> </header> <!-- Message Box for Logged Out --> <?php if (!empty($success_msg) && !$is_logged_in): ?> <main class="container"><div class="alert-success">✓ <?php echo htmlspecialchars($success_msg); ?></div></main> <?php endif; ?> <?php if (!empty($error_msg) && !$is_logged_in): ?> <main class="container"><div class="alert-danger">❌ <?php echo htmlspecialchars($error_msg); ?></div></main> <?php endif; ?> <?php if (!$is_logged_in): ?> <!-- Login Card --> <main class="container"> <div style="display:flex; justify-content:center; align-items:center; min-height:60vh;"> <div class="card" style="width:100%; max-width:400px; box-sizing:border-box;"> <h2 style="text-align:center;">🍳 ล็อกอินเข้าแผงร้านอาหาร</h2> <form method="POST"> <input type="hidden" name="login_admin" value="1"> <div class="form-group"> <label class="form-label" for="username">ชื่อผู้ใช้งาน (Store Username)</label> <input type="text" name="username" id="username" class="form-control" placeholder="เช่น store1" required> </div> <div class="form-group"> <label class="form-label" for="password">รหัสผ่าน (Password)</label> <input type="password" name="password" id="password" class="form-control" placeholder="รหัสผ่านเข้าใช้" required> </div> <button type="submit" class="btn-primary">เข้าระบบแอดมินร้าน</button> </form> <a href="index.php" style="display: block; text-align: center; margin-top: 20px; color: var(--text-secondary); text-decoration: none; font-size: 13px;">🏠 กลับหน้าแรกระบบ</a> </div> </div> </main> <?php else: // Fetch store statistics $stmt_menus_count = $pdo->prepare("SELECT COUNT(*) FROM menus WHERE store_id = :store_id"); $stmt_menus_count->execute([':store_id' => $store_id]); $total_menus = $stmt_menus_count->fetchColumn(); $stmt_tables_count = $pdo->prepare("SELECT COUNT(*) FROM tables_qr WHERE store_id = :store_id"); $stmt_tables_count->execute([':store_id' => $store_id]); $total_tables = $stmt_tables_count->fetchColumn(); $stmt_orders_count = $pdo->prepare("SELECT COUNT(*) FROM orders WHERE store_id = :store_id"); $stmt_orders_count->execute([':store_id' => $store_id]); $total_orders = $stmt_orders_count->fetchColumn(); ?> <?php if (!empty($is_super_admin) && !empty($is_impersonating)): ?> <!-- Persistent Super Admin Impersonation Control Banner --> <div style="background: linear-gradient(90deg, #7C3AED 0%, #C026D3 100%); color: #fff; padding: 12px 24px; font-weight: 800; font-size: 13.5px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 999999; box-shadow: 0 4px 15px rgba(0,0,0,0.4); border-bottom: 2px solid #F472B6; font-family: 'Sarabun', sans-serif;"> <div style="display: flex; align-items: center; gap: 10px;"> <span style="font-size: 18px;">🔑</span> <span>กำลังอยู่ในโหมดสวมสิทธิ์ร้านค้า: <strong style="color: #FFE600;"><?php echo htmlspecialchars($store_info['store_name'] ?? 'ร้านค้า'); ?></strong> (Store ID: #<?php echo $store_id; ?>)</span> </div> <a href="store-admin.php?action=exit_impersonation" class="btn-sm" style="background: #EF4444; color: #fff; border: none; padding: 7px 16px; border-radius: 6px; font-weight: bold; text-decoration: none; font-size: 12.5px; display: inline-flex; align-items: center; gap: 6px; box-shadow: 0 2px 8px rgba(239,68,68,0.4);"> 🚪 ออกจากการสวมสิทธิ์ (กลับหน้า Super Admin) </a> </div> <?php endif; ?> <!-- Mobile Top Navigation Header Bar for Smartphones (Fixed Top) --> <div class="mobile-topbar" style="display: none; background: #1e293b; border-bottom: 1px solid rgba(255,255,255,0.08); padding: 10px 14px; align-items: center; justify-content: space-between; position: fixed; top: 0; left: 0; right: 0; width: 100%; z-index: 99999; box-shadow: 0 4px 15px rgba(0,0,0,0.4);"> <div style="display: flex; align-items: center; gap: 8px; min-width: 0;"> <img src="<?php echo htmlspecialchars($store_info['custom_logo_url'] ?: 'logo.png'); ?>" alt="Logo" style="height: 28px; width: 28px; border-radius: 50%; object-fit: cover;" onerror="this.src='logo.png'"> <div style="min-width: 0;"> <div style="font-weight: 800; font-size: 13.5px; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"><?php echo htmlspecialchars($store_info['store_name']); ?></div> <div style="font-size: 10.5px; color: var(--accent); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">Store Admin OS</div> </div> </div> <div style="display: flex; gap: 6px; align-items: center; flex-shrink: 0;"> <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: 6px 10px; border-radius: 6px; font-weight: 800; font-size: 11px; cursor: pointer; white-space: nowrap;"> 🔊 เสียงนำทาง: ปิด </button> <button type="button" onclick="quickGoToAddMenu()" style="background: linear-gradient(135deg, #00E5FF 0%, #00B4D8 100%); color: #0f172a; border: none; padding: 6px 11px; border-radius: 6px; font-weight: 800; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 4px; box-shadow: 0 2px 8px rgba(0,229,255,0.3); white-space: nowrap;"> + เพิ่มเมนู </button> <button type="button" onclick="toggleMobileSidebar()" style="background: rgba(255,255,255,0.1); color: #fff; border: 1px solid rgba(255,255,255,0.2); padding: 6px 11px; border-radius: 6px; font-weight: 800; font-size: 12px; cursor: pointer; display: flex; align-items: center; gap: 4px; white-space: nowrap;"> ≡ เมนู </button> </div> </div> <!-- Logged In Dashboard Wrapper --> <div class="dashboard-wrapper"> <!-- Sidebar Menu Navigation --> <aside class="sidebar"> <?php if (!empty($_SESSION['super_admin_logged_in'])): ?> <div style="padding: 15px 15px 5px 15px;"> <a href="platform-admin.php" title="กลับสู่หน้าหลัก Super Admin" style="display: flex; align-items: center; justify-content: center; gap: 6px; background: linear-gradient(135deg, #00E5FF 0%, #0080FF 100%); color: #0A192F; padding: 9px 12px; border-radius: 8px; font-size: 12px; font-weight: 800; text-decoration: none; box-shadow: 0 4px 15px rgba(0, 229, 255, 0.25);"> ⚡ กลับหน้าหลัก Super Admin </a> </div> <?php endif; ?> <a href="store-admin.php" title="กลับสู่หน้าแรกของร้านค้า" style="text-decoration: none; color: inherit; display: block;"> <div class="sidebar-header" style="cursor: pointer;"> <img src="logo.png" alt="Logo"> <div> <h1>CMTC Smart Dining Store Admin</h1> <p><?php echo htmlspecialchars($store_info['store_name']); ?></p> </div> </div> </a> <nav class="sidebar-menu"> <a href="store-admin.php?tab=store-dash" onclick="showTab('store-dash');" class="menu-btn <?php echo ($active_tab === 'store-dash' ? 'active' : ''); ?>" id="btn-store-dash"> <span>📈</span> Store Dashboard </a> <a href="store-admin.php?tab=live-orders" onclick="showTab('live-orders');" class="menu-btn <?php echo ($active_tab === 'live-orders' ? 'active' : ''); ?>" id="btn-live-orders"> <span>📦</span> ออเดอร์ตามโต๊ะ (Live Orders) <span id="live-orders-badge" class="red-dot-badge" style="<?php echo ($pending_order_count > 0) ? 'display: inline-flex;' : 'display: none;'; ?> margin-left: auto; background: #EF4444; color: #fff; padding: 2px 7px; border-radius: 10px; font-size: 11px; font-weight: 800; animation: pulse 1.5s infinite; align-items: center; justify-content: center;"><?php echo $pending_order_count; ?></span> </a> <a href="store-admin.php?tab=kds-mode" onclick="showTab('kds-mode');" class="menu-btn <?php echo ($active_tab === 'kds-mode' ? 'active' : ''); ?>" id="btn-kds-mode" style="background: rgba(255, 159, 67, 0.15); border: 1px solid rgba(255, 159, 67, 0.4); color: #FF9F43;"> <span>👨🍳</span> จอครัว KDS Mode (Smart Kitchen) <span id="kds-orders-badge" class="red-dot-badge" style="<?php echo ($pending_order_count > 0) ? 'display: inline-flex;' : 'display: none;'; ?> margin-left: auto; background: #EF4444; color: #fff; padding: 2px 7px; border-radius: 10px; font-size: 11px; font-weight: 800; animation: pulse 1.5s infinite; align-items: center; justify-content: center;"><?php echo $pending_order_count; ?></span> </a> <a href="store-admin.php?tab=daily-sales" onclick="showTab('daily-sales');" class="menu-btn <?php echo ($active_tab === 'daily-sales' ? 'active' : ''); ?>" id="btn-daily-sales"> <span>💵</span> สรุปยอดขายรายวัน (Daily Sales) </a> <a href="store-admin.php?tab=menu-mgr" onclick="showTab('menu-mgr');" class="menu-btn <?php echo ($active_tab === 'menu-mgr' ? 'active' : ''); ?>" id="btn-menu-mgr"> <span>🍽️</span> Menu & Price Manager </a> <a href="store-admin.php?tab=promo" onclick="showTab('promo');" class="menu-btn <?php echo ($active_tab === 'promo' ? 'active' : ''); ?>" id="btn-promo"> <span>🎁</span> Promotions & Policy </a> <a href="store-admin.php?tab=qrs" onclick="showTab('qrs');" class="menu-btn <?php echo ($active_tab === 'qrs' ? 'active' : ''); ?>" id="btn-qrs"> <span>📱</span> QR Code & Tables </a> <?php $encoded_shop_name = urlencode($store_info['store_name'] ?? ''); ?> <a href="menu.php?store_id=<?php echo $store_id; ?>&shop_name=<?php echo $encoded_shop_name; ?>" target="_blank" class="menu-btn" style="text-decoration: none; color: var(--accent); margin-top: 8px;"> <span>📱</span> ดูตัวอย่างหน้าเมนูอาหารสำหรับลูกค้า 🔗 </a> </nav> <div style="padding: 15px 20px; border-top: 1px solid rgba(255,255,255,0.06); text-align: center;"> <span class="badge badge-plan" style="margin-bottom: 8px; display: inline-block;"><?php echo htmlspecialchars($store_info['plan_name']); ?> Plan</span> <button type="button" onclick="toggleNavSound()" class="nav-sound-toggle-btn" style="width: 100%; text-align: center; padding: 7px 12px; border-radius: 8px; font-size: 11.5px; font-weight: bold; cursor: pointer; transition: all 0.2s ease; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.08); color: #94a3b8;">🔊 เสียงนำทาง: ปิด</button> </div> <div class="sidebar-footer"> <a href="store-admin.php?action=logout" class="btn-sm btn-danger" style="text-align: center; display: block; padding: 10px; border-radius: 6px;">Logout 🚪</a> </div> </aside> <!-- Workspace panel --> <main class="main-content"> <?php if (!empty($success_msg)): ?> <script> document.addEventListener('DOMContentLoaded', function() { if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'success', title: 'สำเร็จ!', text: <?php echo json_encode($success_msg, JSON_UNESCAPED_UNICODE); ?>, confirmButtonColor: '#00E5FF', timer: 2500, timerProgressBar: true }); if (typeof window.playNavSound === 'function') window.playNavSound('success'); } }); </script> <?php endif; ?> <?php if (!empty($error_msg)): ?> <script> document.addEventListener('DOMContentLoaded', function() { if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'error', title: 'แจ้งเตือนระบบ', text: <?php echo json_encode($error_msg, JSON_UNESCAPED_UNICODE); ?>, confirmButtonColor: '#EF4444' }); } }); </script> <?php endif; ?> <?php if (!empty($_SESSION['super_admin_logged_in'])): ?> <div style="background: rgba(0, 229, 255, 0.12); border: 1px solid var(--accent); color: #00E5FF; padding: 10px 16px; border-radius: 10px; font-weight: 700; font-size: 13px; margin-bottom: 20px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px;"> <div style="display: flex; align-items: center; gap: 8px;"> <span style="font-size: 16px;">⚡</span> <span>คุณกำลังสวมสิทธิ์เข้าจัดการแทนร้าน <strong><?php echo htmlspecialchars($store_info['store_name']); ?></strong> (โหมด Super Admin)</span> </div> <a href="platform-admin.php" class="btn-sm" style="background: var(--accent); color: #0A192F; font-weight: 800; text-decoration: none; padding: 6px 14px; border-radius: 6px; font-size: 12.5px; display: inline-flex; align-items: center; gap: 4px;"> 🔙 กลับหน้าหลัก Super Admin </a> </div> <?php endif; ?> <?php if ($store_st === 'trial' && !$is_trial_expired && $trial_days_left > 0): ?> <div style="background: linear-gradient(90deg, #0284C7 0%, #0369A1 100%); color: #fff; padding: 12px 20px; border-radius: 10px; font-weight: 700; font-size: 13.5px; margin-bottom: 20px; display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 10px; box-shadow: 0 4px 15px rgba(2,132,199,0.3);"> <div style="display: flex; align-items: center; gap: 10px;"> <span style="font-size: 20px;">⏳</span> <span>ร้านค้าของท่านกำลังอยู่ในช่วง <strong>ทดลองใช้งานฟรี (Free Trial)</strong> (เหลือเวลาอีก <strong style="color: #FFE600; font-size: 15px;"><?php echo $trial_days_left; ?> วัน</strong> • หมดอายุวันที่ <?php echo date('d/m/Y H:i', $trial_ends_ts); ?>)</span> </div> <button type="button" onclick="showTab('promo')" style="background: #FFE600; color: #0f172a; border: none; padding: 6px 14px; border-radius: 6px; font-weight: 800; font-size: 12px; cursor: pointer;"> 💳 สมัครแพ็กเกจล่วงหน้า </button> </div> <?php endif; ?> <?php if ($is_trial_expired): ?> <!-- Trial Expired Block & Subscription Form --> <div style="background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 100%); border: 2px solid #EF4444; border-radius: 16px; padding: 30px; margin-bottom: 30px; box-shadow: 0 10px 30px rgba(239,68,68,0.25);"> <div style="text-align: center; margin-bottom: 25px;"> <div style="font-size: 48px; margin-bottom: 10px;">⏳</div> <h2 style="color: #EF4444; font-size: 24px; font-weight: 800; margin: 0 0 10px 0;">ระยะเวลาทดลองใช้งานฟรี หมดอายุแล้ว (Trial Expired)</h2> <p style="color: #cbd5e1; font-size: 14.5px; max-width: 650px; margin: 0 auto; line-height: 1.6;"> ร้านค้า <strong><?php echo htmlspecialchars($store_info['store_name']); ?></strong> หมดระยะเวลาทดลองใช้งานเรียบร้อยแล้ว (หมดอายุเมื่อ: <?php echo !empty($store_info['trial_ends_at']) ? date('d/m/Y H:i', strtotime($store_info['trial_ends_at'])) : 'ไม่ระบุ'; ?>) <br> กรุณาเลือกสมัครแพ็กเกจ subscription ด้านล่างเพื่อปลดล็อกการใช้งานและเปิดรับออเดอร์หน้าร้านต่อ </p> </div> <form method="POST" enctype="multipart/form-data" style="max-width: 800px; margin: 0 auto; background: rgba(15, 23, 42, 0.85); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 25px;"> <input type="hidden" name="submit_subscription_payment" value="1"> <h3 style="color: var(--accent); margin-top: 0; font-size: 16px; margin-bottom: 15px; text-align: center;">💳 เลือกแพ็กเกจและแนบสลิปชำระเงินเพื่อเปิดใช้งาน</h3> <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px;"> <label style="background: rgba(255,255,255,0.05); border: 2px solid var(--accent); border-radius: 10px; padding: 15px; cursor: pointer; display: block; text-align: center;"> <input type="radio" name="plan_id" value="1" style="margin-bottom: 8px;"> <div style="font-weight: 800; font-size: 16px; color: #fff;">Starter Plan</div> <div style="font-size: 20px; font-weight: 800; color: var(--accent); margin: 4px 0;">฿490 / เดือน</div> <div style="font-size: 12px; color: var(--text-secondary);">สูงสุด 10 โต๊ะอาหาร</div> </label> <label style="background: rgba(255,255,255,0.05); border: 2px solid var(--accent); border-radius: 10px; padding: 15px; cursor: pointer; display: block; text-align: center;"> <input type="radio" name="plan_id" value="2" checked style="margin-bottom: 8px;"> <div style="font-weight: 800; font-size: 16px; color: #fff;">Standard Plan ⭐</div> <div style="font-size: 20px; font-weight: 800; color: var(--accent); margin: 4px 0;">฿990 / เดือน</div> <div style="font-size: 12px; color: var(--text-secondary);">สูงสุด 30 โต๊ะอาหาร</div> </label> <label style="background: rgba(255,255,255,0.05); border: 2px solid var(--accent); border-radius: 10px; padding: 15px; cursor: pointer; display: block; text-align: center;"> <input type="radio" name="plan_id" value="3" style="margin-bottom: 8px;"> <div style="font-weight: 800; font-size: 16px; color: #fff;">Premium Plan</div> <div style="font-size: 20px; font-weight: 800; color: var(--accent); margin: 4px 0;">฿1,590 / เดือน</div> <div style="font-size: 12px; color: var(--text-secondary);">สูงสุด 100 โต๊ะอาหาร</div> </label> </div> <div style="background: rgba(0,229,255,0.05); border: 1px dashed var(--accent); padding: 16px; border-radius: 10px; margin-bottom: 20px; display: flex; gap: 15px; align-items: center; flex-wrap: wrap;"> <img src="https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=PromptPay_CMTC_Solution_0105565012345" alt="PromptPay QR Code" style="width: 110px; height: 110px; border-radius: 8px; background: #fff; padding: 4px; flex-shrink: 0; box-shadow: 0 4px 10px rgba(0,0,0,0.3);"> <div style="font-size: 13px; color: #cbd5e1; line-height: 1.6;"> <div style="font-weight: 800; color: var(--accent); font-size: 14px; margin-bottom: 4px;">📲 ช่องทางโอนชำระเงิน (PromptPay QR & ธนาคาร)</div> <div><strong>ธนาคารกสิกรไทย (KBANK)</strong></div> <div>เลขที่บัญชี: <strong style="color: #00E5FF; font-size: 15px;">012-3-45678-9</strong></div> <div>ชื่อบัญชี: <strong>บจก. ซีเอ็มทีซี เทค โซลูชั่น</strong></div> </div> </div> <div class="form-group" style="margin-bottom: 20px;"> <label class="form-label" style="color: #fff; font-weight: bold;">แนบไฟล์สลิปโอนเงินจริง (จากเครื่อง/มือถือ) *</label> <input type="file" name="slip_file" accept="image/jpeg,image/png,image/webp" class="form-control" style="padding: 8px;" required> </div> <button type="submit" class="btn-primary" style="width: 100%; padding: 12px; font-size: 15px; font-weight: 800;"> 💳 ยืนยันแจ้งชำระเงินเพื่อสมัครแพ็กเกจ </button> </form> </div> <?php endif; ?> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px;"> <div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;"> <?php if (!empty($_SESSION['super_admin_logged_in'])): ?> <a href="platform-admin.php" class="btn-sm" style="background: var(--accent); color: #0A192F; font-weight: 800; text-decoration: none; padding: 6px 12px; border-radius: 6px; font-size: 12.5px; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 2px 10px rgba(0,229,255,0.3);" title="กลับสู่หน้าหลัก Super Admin"> ⚡ กลับ Super Admin </a> <?php endif; ?> <h2 style="margin: 0; font-weight: 800;">แผงควบคุมร้าน: <?php echo htmlspecialchars($store_info['store_name']); ?></h2> </div> <a href="store-admin.php" style="color: var(--accent); font-weight: 700; text-decoration: none; font-size: 13.5px;">🏠 หน้าแรกของร้าน</a> </div> <!-- Merchant Easy-Operational Guide Banner --> <div style="background: linear-gradient(135deg, rgba(30, 41, 59, 0.9), rgba(15, 23, 42, 0.9)); border: 1px solid var(--accent); border-radius: 12px; padding: 18px 22px; margin-bottom: 25px; box-shadow: 0 4px 15px rgba(0,229,255,0.08);"> <div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;"> <div style="display: flex; align-items: center; gap: 14px;"> <div style="background: rgba(0,229,255,0.15); width: 46px; height: 46px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 24px; color: var(--accent);"> 💡 </div> <div> <h4 style="margin: 0 0 4px; font-size: 15px; font-weight: 800; color: #fff;">คู่มือการใช้งานด่วนสำหรับเจ้าของร้าน (Merchant Quick Guide)</h4> <p style="margin: 0; font-size: 12.5px; color: var(--text-secondary);">3 ขั้นตอนง่ายๆ ในการเริ่มใช้งาน: 1. เพิ่มเมนูอาหาร ➔ 2. พิมพ์ตั้งโต๊ะ QR Code ➔ 3. ดูออเดอร์สด & เช็กบิลเงินสด/โอน</p> </div> </div> <div style="display: flex; gap: 8px; flex-wrap: wrap;"> <button type="button" onclick="showTab('menu-mgr')" class="btn-sm" style="background: var(--accent); color: #0f172a; font-weight: bold; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 12.5px;"> 🍔 1. เพิ่มเมนู </button> <button type="button" onclick="showTab('qrs')" class="btn-sm" style="background: #3b82f6; color: #fff; font-weight: bold; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 12.5px;"> 📱 2. ตั้งโต๊ะ QR </button> <button type="button" onclick="showTab('live-orders')" class="btn-sm" style="background: #10b981; color: #fff; font-weight: bold; border: none; padding: 8px 14px; border-radius: 6px; cursor: pointer; font-size: 12.5px;"> 📦 3. ดูออเดอร์สด </button> </div> </div> </div> <!-- 1. Store Dashboard Tab --> <section id="store-dash-sec" class="tab-sec <?php echo ($active_tab === 'store-dash' || empty($active_tab)) ? 'active' : ''; ?>"> <!-- Stats Row --> <div class="stats-grid"> <div class="stat-card" style="border-top: 4px solid #10B981;"> <div class="stat-info"> <h3><?php echo ($today_revenue > 0) ? 'ยอดขายวันนี้' : 'ยอดขายสะสม'; ?></h3> <div style="color: #10B981; font-weight: 800;">฿<?php echo number_format($display_dashboard_revenue, 2); ?></div> </div> <div class="stat-icon" style="color: #10B981;">💵</div> </div> <div class="stat-card" style="border-top: 4px solid var(--accent);"> <div class="stat-info"> <h3>จำนวนออเดอร์</h3> <div style="color: var(--accent); font-weight: 800;"><?php echo number_format($total_orders); ?> รายการ</div> </div> <div class="stat-icon" style="color: var(--accent);">📝</div> </div> <div class="stat-card" style="border-top: 4px solid #3B82F6;"> <div class="stat-info"> <h3>รายการเมนู</h3> <div style="color: #3B82F6; font-weight: 800;"><?php echo number_format($total_menus); ?> รายการ</div> </div> <div class="stat-icon" style="color: #3B82F6;">🍔</div> </div> <div class="stat-card" style="border-top: 4px solid #A855F7;"> <div class="stat-info"> <h3>สถานะร้านค้า</h3> <div style="color: #A855F7; font-weight: 800;"> <?php echo ($store_info['status'] === 'active') ? '🟢 เปิดบริการ (Active)' : '🔴 ปิดบริการ'; ?> </div> <small style="font-size: 11px; color: var(--text-secondary); display: block;">(<?php echo htmlspecialchars($store_info['plan_name'] ?: 'Standard'); ?> Plan)</small> </div> <div class="stat-icon" style="color: #A855F7;">🏪</div> </div> </div> <!-- Chart section --> <div class="charts-row" style="grid-template-columns: 1fr;"> <div class="chart-card"> <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-bottom: 15px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 15px;"> <div> <h3 style="margin: 0; font-size: 16px; font-weight: 800;">📊 อัตราสัดส่วนสถานะออเดอร์ (Order Status Distribution)</h3> <div style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;"> แสดงข้อมูลบิลประจำวันที่ <strong style="color: var(--accent);"><?php echo date('d/m/Y', strtotime($selected_chart_date)); ?></strong> (รวมทั้งสิ้น <strong><?php echo intval($total_chart_orders); ?></strong> บิล) </div> </div> <!-- Calendar Date Picker Form (Matching exact screenshot design) --> <form method="GET" action="store-admin.php" style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;"> <input type="hidden" name="tab" value="store-dash"> <div style="display: flex; align-items: center; gap: 8px;"> <button type="button" onclick="triggerChartCalendarPicker()" title="คลิกเพื่อเปิดปฏิทินเลือกวันที่" style="background: transparent; border: none; font-size: 26px; cursor: pointer; padding: 0; line-height: 1; display: inline-flex; align-items: center; transition: transform 0.15s ease;" onmouseover="this.style.transform='scale(1.2)'" onmouseout="this.style.transform='scale(1)'"> 🗓️ </button> <input type="date" name="chart_date" id="chart_date" class="form-control" value="<?php echo htmlspecialchars($selected_chart_date); ?>" style="padding: 8px 14px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 8px; font-weight: bold; font-size: 14px; cursor: pointer; color-scheme: dark; user-select: none;" onchange="this.form.submit()" onclick="triggerChartCalendarPicker()" onkeydown="return false;" title="คลิกเพื่อเปิดปฏิทินเลือกวันที่"> </div> <div style="display: flex; gap: 6px; align-items: center;"> <a href="store-admin.php?tab=store-dash&chart_date=<?php echo date('Y-m-d'); ?>" class="btn-sm" style="background: rgba(0, 229, 255, 0.15); color: var(--accent); border: 1px solid var(--border-glass); padding: 7px 12px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px;"> 📌 วันนี้ </a> <a href="store-admin.php?tab=store-dash&chart_date=<?php echo date('Y-m-d', strtotime('-1 day')); ?>" class="btn-sm" style="background: rgba(255, 255, 255, 0.05); color: #cbd5e1; border: 1px solid var(--border-glass); padding: 7px 12px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px;"> ⏪ เมื่อวาน </a> </div> </form> </div> <div class="chart-container" style="height: 320px;"> <canvas id="orderStatusChart"></canvas> </div> </div> </div> </section> <!-- 1.5 Live Orders Tab (ออเดอร์ตามโต๊ะ) --> <section id="live-orders-sec" class="tab-sec <?php echo ($active_tab === 'live-orders') ? 'active' : ''; ?>"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> <div> <h3 style="margin:0; font-weight:800;">📦 รายการออเดอร์ตามโต๊ะที่กำลังรับประทาน (Active Table Sessions)</h3> <small style="color: #cbd5e1;">รวมยอดสแกนสั่งทุกรอบประจำโต๊ะ • เช็กบิลแล้วระบบจะเคลียร์โต๊ะและรีเซ็ต QR Code ให้อัตโนมัติ</small> </div> <button type="button" onclick="location.reload()" class="btn-sm btn-primary" style="padding: 8px 16px; border-radius: 6px; font-weight: bold; border:none; cursor:pointer; background: var(--accent); color: #0f172a;"> 🔄 รีเฟรชออเดอร์สด </button> </div> <?php if (empty($active_table_groups)): ?> <div class="card" style="text-align: center; padding: 50px; color: var(--text-secondary);"> 📭 ไม่มีโต๊ะที่กำลังรับประทานอาหารในขณะนี้ โต๊ะทั้งหมดพร้อมเปิดรับลูกค้าใหม่! </div> <?php else: ?> <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(360px, 1fr)); gap: 20px; margin-bottom: 40px;"> <?php foreach ($active_table_groups as $tbl_key => $group): $all_items_json = rawurlencode(json_encode($group['all_items'], JSON_UNESCAPED_UNICODE)); $current_time_str = date('d/m/Y H:i:s'); ?> <div class="card" style="border: 2px solid var(--accent); background: #1e293b; border-radius: 12px; padding: 20px; display: flex; flex-direction: column; justify-content: space-between; box-shadow: 0 4px 20px rgba(0,229,255,0.1);"> <div> <!-- Card Header: Table Number & Session Summary --> <div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed rgba(255,255,255,0.15); padding-bottom: 12px; margin-bottom: 15px;"> <div> <span style="font-size: 20px; font-weight: 800; color: var(--accent);">🪑 โต๊ะ <?php echo htmlspecialchars($group['table_display']); ?></span> <div style="font-size: 12px; color: var(--text-secondary); margin-top: 2px;"><?php echo count($group['orders']); ?> รอบการสแกนสั่งซื้อ</div> </div> <span style="background: rgba(0, 229, 255, 0.15); color: var(--accent); border: 1px solid var(--border-glass); padding: 4px 10px; border-radius: 20px; font-size: 12px; font-weight: bold;"> ● กำลังรับประทาน </span> </div> <!-- Scan Rounds Breakdown --> <?php foreach ($group['orders'] as $ord_index => $ord): $status_bg = '#334155'; $status_text = 'รอดำเนินการ'; if ($ord['status'] === 'pending') { $status_bg = '#FF9F43'; $status_text = '⏳ รอรับรายการ'; } elseif ($ord['status'] === 'preparing') { $status_bg = '#00E5FF'; $status_text = '🍳 กำลังปรุง'; } elseif ($ord['status'] === 'served' || $ord['status'] === 'ready') { $status_bg = '#28C76F'; $status_text = '🔔 เสิร์ฟแล้ว'; } $is_tp = (($ord['payment_method'] ?? 'cash') === 'transfer' || !empty($ord['is_paid'])); ?> <div style="background: rgba(0,0,0,0.25); border-radius: 8px; padding: 12px; margin-bottom: 12px; border: 1px solid rgba(255,255,255,0.05);"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; flex-wrap: wrap; gap: 6px;"> <div style="display: flex; align-items: center; gap: 8px;"> <span style="font-size: 12px; font-weight: bold; color: var(--text-secondary);"> รอบที่ <?php echo ($ord_index + 1); ?> (#<?php echo $ord['id']; ?> • <?php echo $ord['order_time']; ?> น.) </span> <?php if ($is_tp): ?> <button type="button" onclick="viewSlipImage('<?php echo htmlspecialchars($ord['slip_url'] ?? '', ENT_QUOTES); ?>', '<?php echo $ord['id']; ?>')" style="background: rgba(16,185,129,0.2); color: #10B981; border: 1px solid #10B981; padding: 2px 10px; border-radius: 12px; font-size: 11px; font-weight: bold; display: inline-flex; align-items: center; gap: 4px; cursor: pointer;" title="คลิกเพื่อดูสลิปโอนเงิน"> 💳 โอนจ่ายแล้ว </button> <?php else: ?> <span style="background: rgba(239,68,68,0.25); color: #FF4D4F; border: 1px solid #FF4D4F; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold; display: inline-flex; align-items: center; gap: 3px;"> 🔴 ยังไม่ชำระ (ชำระเงินสดกับพนักงาน) </span> <?php endif; ?> </div> <span style="background: <?php echo $status_bg; ?>; color: #fff; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;"> <?php echo $status_text; ?> </span> </div> <ul style="list-style: none; padding: 0; margin: 0 0 10px 0; display: flex; flex-direction: column; gap: 6px;"> <?php foreach ($ord['items'] as $it): $line_sum = $it['price'] * $it['quantity']; ?> <li style="display: flex; justify-content: space-between; align-items: flex-start; background: rgba(0,0,0,0.2); padding: 8px 12px; border-radius: 6px; font-size: 13px;"> <div> <div> <strong style="color: #fff; font-size: 14px;"><?php echo htmlspecialchars($it['menu_name'] ?: 'รายการอาหาร'); ?></strong> <span style="color: var(--accent); font-weight: bold; white-space: nowrap;"> x<?php echo $it['quantity']; ?></span> </div> <?php if (!empty($it['spice_level'])): ?> <div style="margin-top: 4px;"> <span style="background: rgba(255,159,67,0.2); color: #FF9F43; border: 1px solid #FF9F43; padding: 2px 8px; border-radius: 4px; font-size: 11.5px; font-weight: bold; display: inline-block;"><?php echo htmlspecialchars($it['spice_level']); ?></span> </div> <?php endif; ?> <?php if (!empty($it['note'])): ?> <div style="font-size: 12px; color: #FFD166; font-weight: bold; margin-top: 3px; background: rgba(255,209,102,0.1); padding: 2px 6px; border-radius: 4px; display: inline-block;"> คำขอพิเศษ: <?php echo htmlspecialchars($it['note']); ?> </div> <?php endif; ?> </div> <span style="font-weight: bold; color: #fff;">฿<?php echo number_format($line_sum, 2); ?></span> </li> <?php endforeach; ?> </ul> <!-- Individual Status Transition Buttons & View Slip Button --> <div style="display: flex; justify-content: space-between; align-items: center; gap: 6px; margin-top: 8px; flex-wrap: wrap;"> <form method="POST" style="display: flex; gap: 6px; flex: 1;"> <input type="hidden" name="tab" value="live-orders"> <input type="hidden" name="update_order_status" value="1"> <input type="hidden" name="order_id" value="<?php echo $ord['id']; ?>"> <?php if ($ord['status'] === 'pending' || $ord['status'] === 'unpaid'): ?> <button type="submit" name="new_status" value="preparing" class="btn-sm" style="flex: 1; background: #00E5FF; color: #0f172a; font-weight: bold; border:none; padding: 5px 10px; border-radius: 6px; cursor: pointer; font-size: 11.5px;"> 🍳 รับออเดอร์/เริ่มปรุง </button> <?php elseif ($ord['status'] === 'preparing'): ?> <button type="submit" name="new_status" value="served" class="btn-sm" style="flex: 1; background: #28C76F; color: #fff; font-weight: bold; border:none; padding: 5px 10px; border-radius: 6px; cursor: pointer; font-size: 11.5px;"> 🛎️ ปรุงเสร็จ/พร้อมเสิร์ฟ </button> <?php endif; ?> <button type="submit" name="new_status" value="cancelled" onclick="return confirm('ยกเลิกรอบสั่งซื้อนี้?')" class="btn-sm btn-danger" style="padding: 5px 10px; border-radius: 6px; font-size: 11px; cursor: pointer; background: rgba(234,84,85,0.2); color: #EA5455; border: 1px solid #EA5455;"> ❌ ยกเลิก </button> </form> <form method="POST" style="display: inline-block;"> <input type="hidden" name="tab" value="live-orders"> <input type="hidden" name="toggle_payment_method" value="1"> <input type="hidden" name="order_id" value="<?php echo $ord['id']; ?>"> <button type="submit" class="btn-sm" style="background: rgba(245, 158, 11, 0.2); color: #F59E0B; border: 1px solid #F59E0B; padding: 5px 10px; border-radius: 6px; font-size: 11px; font-weight: bold; cursor: pointer;" title="สลับสถานะชำระเงิน (โอน / เงินสด)"> 🔄 สลับสถานะชำระ </button> </form> <?php if ($is_tp): ?> <button type="button" onclick="viewSlipImage('<?php echo htmlspecialchars($ord['slip_url'] ?? '', ENT_QUOTES); ?>', '<?php echo $ord['id']; ?>')" class="btn-sm" style="background: rgba(0, 229, 255, 0.2); color: #00E5FF; border: 1px solid #00E5FF; padding: 5px 12px; border-radius: 6px; font-size: 11.5px; font-weight: bold; cursor: pointer; display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; box-shadow: 0 2px 8px rgba(0,229,255,0.25);"> 🖼️ ดูสลิป </button> <?php endif; ?> </div> </div> <?php endforeach; ?> </div> <div> <div style="background: rgba(0,229,255,0.08); border: 1px solid var(--accent); padding: 14px 16px; border-radius: 10px; margin-bottom: 15px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px; font-size: 13px; color: var(--text-secondary);"> <span>ยอดรวมทั้งหมด (Total):</span> <span>฿<?php echo number_format($group['grand_total'], 2); ?></span> </div> <?php if ($group['total_transfer_paid'] > 0): ?> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; font-size: 13px; color: #10B981; font-weight: bold;"> <span>โอนชำระเงินแล้ว 💳:</span> <span>-฿<?php echo number_format($group['total_transfer_paid'], 2); ?></span> </div> <?php endif; ?> <div style="display: flex; justify-content: space-between; align-items: center; border-top: 1px dashed rgba(255,255,255,0.2); padding-top: 6px; margin-top: 4px;"> <span style="font-size: 14px; font-weight: 800; color: #fff;">ยอดคงชำระ (เงินสด/ค้างจ่าย):</span> <span style="font-size: 22px; font-weight: 800; color: <?php echo ($group['total_unpaid_cash'] > 0) ? '#FF5722' : '#10B981'; ?>;"> ฿<?php echo number_format($group['total_unpaid_cash'], 2); ?> </span> </div> </div> <!-- Table Level Actions: Print Receipt & Checkout --> <div style="display: flex; gap: 8px;"> <button type="button" onclick="printReceipt('<?php echo rawurlencode($store_info['store_name']); ?>', '<?php echo rawurlencode($group['table_display']); ?>', '<?php echo $group['grand_total']; ?>', '<?php echo $all_items_json; ?>', '<?php echo rawurlencode($current_time_str); ?>', '<?php echo $group['total_transfer_paid']; ?>', '<?php echo $group['total_unpaid_cash']; ?>')" class="btn-sm" style="flex: 1; background: #3b82f6; color: #fff; font-weight: bold; border:none; padding: 10px; border-radius: 6px; cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 4px;"> 🧾 พิมพ์ใบเสร็จ </button> <form method="POST" style="flex: 1;" onsubmit="return handleCheckoutSubmit(this, '<?php echo rawurlencode($store_info['store_name']); ?>', '<?php echo rawurlencode($group['table_display']); ?>', '<?php echo $group['grand_total']; ?>', '<?php echo $all_items_json; ?>', '<?php echo rawurlencode($current_time_str); ?>', '<?php echo $group['total_transfer_paid']; ?>', '<?php echo $group['total_unpaid_cash']; ?>')"> <input type="hidden" name="tab" value="live-orders"> <input type="hidden" name="checkout_table" value="1"> <input type="hidden" name="table_number" value="<?php echo htmlspecialchars($group['table_display']); ?>"> <button type="submit" class="btn-sm" style="width: 100%; background: #10B981; color: #fff; font-weight: bold; border:none; padding: 10px; border-radius: 6px; cursor: pointer; font-size: 13px; display: flex; align-items: center; justify-content: center; gap: 4px;"> ✅ เช็กบิล & เคลียร์โต๊ะ </button> </form> </div> </div> </div> <?php endforeach; ?> </div> <?php endif; ?> <!-- Sales History / Receipt Archive Section --> <div class="card" style="margin-top: 30px;"> <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; margin-bottom: 18px; border-bottom: 1px solid rgba(255,255,255,0.08); padding-bottom: 15px;"> <div> <h3 style="margin: 0; font-weight: 800; color: var(--accent); font-size: 18px;">📜 ประวัติการเช็กบิลและใบเสร็จ (Sales Receipt History)</h3> <div style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;"> แสดงประวัติใบเสร็จประจำวันที่ <strong style="color: var(--accent);"><?php echo date('d/m/Y', strtotime($selected_history_date)); ?></strong> (พบทั้งสิ้น <strong><?php echo count($closed_orders); ?></strong> รายการ) </div> </div> <!-- Calendar Date Picker Form (Matching exact screenshot design) --> <form method="GET" action="store-admin.php" style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap;"> <input type="hidden" name="tab" value="live-orders"> <div style="display: flex; align-items: center; gap: 8px;"> <button type="button" onclick="triggerHistoryCalendarPicker()" title="คลิกเพื่อเปิดปฏิทินเลือกวันที่" style="background: transparent; border: none; font-size: 26px; cursor: pointer; padding: 0; line-height: 1; display: inline-flex; align-items: center; transition: transform 0.15s ease;" onmouseover="this.style.transform='scale(1.2)'" onmouseout="this.style.transform='scale(1)'"> 🗓️ </button> <input type="date" name="history_date" id="history_date" class="form-control" value="<?php echo htmlspecialchars($selected_history_date); ?>" style="padding: 8px 14px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 8px; font-weight: bold; font-size: 14px; cursor: pointer; color-scheme: dark; user-select: none;" onchange="this.form.submit()" onclick="triggerHistoryCalendarPicker()" onkeydown="return false;" title="คลิกเพื่อเปิดปฏิทินเลือกวันที่"> </div> <div style="display: flex; gap: 6px; align-items: center;"> <a href="store-admin.php?tab=live-orders&history_date=<?php echo date('Y-m-d'); ?>" class="btn-sm" style="background: rgba(0, 229, 255, 0.15); color: var(--accent); border: 1px solid var(--border-glass); padding: 7px 12px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px;"> 📌 วันนี้ </a> <a href="store-admin.php?tab=live-orders&history_date=<?php echo date('Y-m-d', strtotime('-1 day')); ?>" class="btn-sm" style="background: rgba(255, 255, 255, 0.05); color: #cbd5e1; border: 1px solid var(--border-glass); padding: 7px 12px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px;"> ⏪ เมื่อวาน </a> </div> </form> </div> <?php if (empty($closed_orders)): ?> <p style="color: var(--text-secondary); text-align: center; padding: 25px 0;">📭 ไม่พบประวัติการเช็กบิลในวันที่ <?php echo date('d/m/Y', strtotime($selected_history_date)); ?></p> <?php else: ?> <div style="display: flex; flex-direction: column; gap: 10px;"> <?php foreach ($closed_orders as $cord): $c_total = 0; $c_transfer_paid = 0; $c_unpaid_cash = 0; $c_items_arr = []; $is_ord_transfer = (isset($cord['payment_method']) && $cord['payment_method'] === 'transfer') || (!empty($cord['is_paid'])); foreach ($cord['items'] as $cit) { $item_line_tot = ($cit['price'] * $cit['quantity']); $c_total += $item_line_tot; if ($is_ord_transfer) { $c_transfer_paid += $item_line_tot; } else { $c_unpaid_cash += $item_line_tot; } $c_items_arr[] = [ 'name' => $cit['menu_name'] ?: 'รายการอาหาร', 'quantity' => $cit['quantity'], 'price' => $cit['price'], 'spice_level' => $cit['spice_level'] ?? '', 'note' => $cit['note'] ?? '', 'is_transfer_paid' => $is_ord_transfer, 'payment_method' => $cord['payment_method'] ?? 'cash' ]; } $c_json = rawurlencode(json_encode($c_items_arr, JSON_UNESCAPED_UNICODE)); ?> <div style="background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.05); padding: 12px 18px; border-radius: 8px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;"> <div> <strong>ออเดอร์ #<?php echo $cord['id']; ?> • โต๊ะ <?php echo htmlspecialchars($cord['table_number']); ?></strong> <span style="font-size: 12px; color: var(--text-secondary); margin-left: 10px;"><?php echo $cord['order_time']; ?></span> <?php $st_badge_bg = '#10B981'; $st_badge_lbl = 'ชำระแล้ว (เสร็จสิ้น)'; if ($cord['status'] === 'cancelled') { $st_badge_bg = '#EA5455'; $st_badge_lbl = 'ยกเลิก'; } elseif ($cord['status'] === 'preparing') { $st_badge_bg = '#00E5FF'; $st_badge_lbl = 'กำลังปรุง'; } elseif ($cord['status'] === 'pending' || $cord['status'] === 'unpaid') { $st_badge_bg = '#FF9F43'; $st_badge_lbl = 'รอดำเนินการ'; } elseif ($cord['status'] === 'served' || $cord['status'] === 'ready') { $st_badge_bg = '#3B82F6'; $st_badge_lbl = 'เสิร์ฟแล้ว'; } ?> <span style="background: <?php echo $st_badge_bg; ?>; color: #fff; padding: 2px 8px; border-radius: 10px; font-size: 11px; margin-left: 8px; font-weight: bold;"> <?php echo $st_badge_lbl; ?> </span> </div> <div style="display: flex; align-items: center; gap: 15px;"> <span style="font-weight: 800; font-size: 16px; color: var(--accent);">฿<?php echo number_format($c_total, 2); ?></span> <button type="button" onclick="printReceipt('<?php echo rawurlencode($store_info['store_name']); ?>', '<?php echo rawurlencode($cord['table_number']); ?>', '<?php echo $c_total; ?>', '<?php echo $c_json; ?>', '<?php echo rawurlencode($cord['order_time']); ?>', '<?php echo $c_transfer_paid; ?>', '<?php echo $c_unpaid_cash; ?>')" class="btn-sm" style="background: #334155; color: #fff; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;"> 🧾 พิมพ์ใบเสร็จย้อนหลัง </button> </div> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> </section> <!-- 1.6 Kitchen Display System (KDS Mode for Chefs & Cooks) --> <section id="kds-mode-sec" class="tab-sec <?php echo ($active_tab === 'kds-mode') ? 'active' : ''; ?>"> <div class="card" style="background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border: 2px solid #FF9F43; padding: 22px; margin-bottom: 25px; border-radius: 12px;"> <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px;"> <div> <h3 style="margin: 0 0 4px; color: #FF9F43; font-weight: 800; font-size: 20px;">👨🍳 หน้าจอห้องครัว Smart KDS (Kitchen Display System)</h3> <small style="color: var(--text-secondary);">โหมดแสดงผลสำหรับเชฟและพ่อครัวในห้องครัว ปุ่มกดขนาดยักษ์ รองรับจอสัมผัส อัปเดตสถานะการปรุงอาหารทันที</small> </div> <div style="display: flex; gap: 10px; align-items: center;"> <button type="button" onclick="playKdsAlertSound()" class="btn-sm" style="background: rgba(255,159,67,0.2); color: #FF9F43; border: 1px solid #FF9F43; padding: 8px 14px; border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 13px;"> 🔊 ทดสอบเสียงแจ้งออเดอร์ </button> <button type="button" onclick="location.reload()" class="btn-sm" style="background: var(--accent); color: #0f172a; border: none; padding: 8px 16px; border-radius: 8px; font-weight: bold; cursor: pointer; font-size: 13px;"> 🔄 รีเฟรชรายการ </button> </div> </div> </div> <!-- Active Kitchen Pending/Preparing Orders --> <?php $kds_active_orders = []; try { $stmt_kds = $pdo->prepare("SELECT o.*, DATE_FORMAT(o.created_at, '%H:%i:%s') as order_time FROM orders o WHERE o.store_id = :store_id AND o.status IN ('pending', 'preparing') ORDER BY o.id ASC"); $stmt_kds->execute([':store_id' => $store_id]); $kds_active_orders = $stmt_kds->fetchAll(PDO::FETCH_ASSOC); foreach ($kds_active_orders as &$kord) { $stmt_kitems = $pdo->prepare("SELECT oi.*, m.name as menu_name FROM order_items oi LEFT JOIN menus m ON oi.menu_id = m.menu_id WHERE oi.order_id = :order_id"); $stmt_kitems->execute([':order_id' => $kord['id']]); $kord['items'] = $stmt_kitems->fetchAll(PDO::FETCH_ASSOC); } unset($kord); } catch (Exception $e) {} ?> <?php if (empty($kds_active_orders)): ?> <div class="card" style="text-align: center; padding: 60px; color: var(--text-secondary); background: #1e293b; border-radius: 12px;"> <div style="font-size: 40px; margin-bottom: 10px;">👨🍳✨</div> <h3 style="margin: 0; color: #10B981;">ไม่มีรายการอาหารค้างทำในห้องครัวขณะนี้!</h3> <p style="margin: 8px 0 0; font-size: 14px;">รายการอาหารทั้งหมดถูกปรุงเสร็จและเสิร์ฟเรียบร้อยแล้ว</p> </div> <?php else: ?> <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 20px;"> <?php foreach ($kds_active_orders as $kord): $is_pending = ($kord['status'] === 'pending'); $card_border = $is_pending ? '#FF9F43' : '#00E5FF'; $status_label = $is_pending ? '⏳ ออเดอร์ใหม่ - รอรับรายการ' : '🍳 กำลังปรุงอาหาร'; ?> <div class="card" style="border: 2px solid <?php echo $card_border; ?>; background: #1e293b; border-radius: 12px; padding: 18px; display: flex; flex-direction: column; justify-content: space-between; box-shadow: 0 8px 25px rgba(0,0,0,0.3);"> <div> <div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 12px; margin-bottom: 12px;"> <div> <span style="font-size: 22px; font-weight: 800; color: var(--accent);">🪑 โต๊ะ <?php echo htmlspecialchars($kord['table_number']); ?></span> <div style="font-size: 12px; color: var(--text-secondary); margin-top: 2px;">ออเดอร์ #<?php echo $kord['id']; ?> • เวลา <?php echo $kord['order_time']; ?> น.</div> </div> <span style="background: <?php echo $is_pending ? 'rgba(255,159,67,0.2)' : 'rgba(0,229,255,0.2)'; ?>; color: <?php echo $card_border; ?>; border: 1px solid <?php echo $card_border; ?>; padding: 4px 10px; border-radius: 20px; font-size: 11.5px; font-weight: bold;"> <?php echo $status_label; ?> </span> </div> <!-- Menu Items List --> <ul style="list-style: none; padding: 0; margin: 0 0 15px 0; display: flex; flex-direction: column; gap: 8px;"> <?php foreach ($kord['items'] as $kit): ?> <li style="display: flex; justify-content: space-between; align-items: center; background: rgba(0,0,0,0.35); padding: 12px 14px; border-radius: 8px; border-left: 4px solid <?php echo $card_border; ?>;"> <div> <div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap;"> <span style="font-size: 16px; font-weight: 800; color: #fff;"> <?php echo htmlspecialchars($kit['menu_name'] ?: 'รายการอาหาร'); ?> </span> <?php if (!empty($kit['spice_level'])): ?> <span style="background: rgba(255,159,67,0.25); color: #FF9F43; border: 1.5px solid #FF9F43; padding: 2px 8px; border-radius: 6px; font-size: 12px; font-weight: 800;"> <?php echo htmlspecialchars($kit['spice_level']); ?> </span> <?php endif; ?> </div> <?php if (!empty($kit['note'])): ?> <div style="font-size: 13px; color: #FFE600; font-weight: 800; margin-top: 5px; background: rgba(255,230,0,0.15); padding: 4px 8px; border-radius: 6px; border: 1px solid rgba(255,230,0,0.3); display: inline-block;"> คำขอพิเศษ: <?php echo htmlspecialchars($kit['note']); ?> </div> <?php endif; ?> </div> <span style="font-size: 18px; font-weight: 800; color: var(--accent); background: rgba(0,229,255,0.15); padding: 6px 12px; border-radius: 8px; white-space: nowrap; flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; margin-left: 10px;">x<?php echo $kit['quantity']; ?></span> </li> <?php endforeach; ?> </ul> </div> <!-- Kitchen Touch Action Form --> <form method="POST" style="margin-top: 10px;"> <input type="hidden" name="update_order_status" value="1"> <input type="hidden" name="order_id" value="<?php echo $kord['id']; ?>"> <?php if ($is_pending): ?> <button type="submit" name="new_status" value="preparing" class="btn-sm" style="width: 100%; background: #FF9F43; color: #0f172a; font-size: 15px; font-weight: 800; padding: 12px; border-radius: 8px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px;"> 🍳 กดรับออเดอร์ & เริ่มปรุงอาหาร </button> <?php else: ?> <button type="submit" name="new_status" value="served" class="btn-sm" style="width: 100%; background: #10B981; color: #fff; font-size: 15px; font-weight: 800; padding: 12px; border-radius: 8px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px;"> 🔔 ปรุงเสร็จแล้ว / พร้อมเสิร์ฟ </button> <?php endif; ?> </form> </div> <?php endforeach; ?> </div> <?php endif; ?> </section> <!-- 1.8 Daily Sales & Tax Report Tab --> <section id="daily-sales-sec" class="tab-sec <?php echo ($active_tab === 'daily-sales') ? 'active' : ''; ?>"> <!-- Header & Dual-Mode Date Picker Filter --> <div class="card" style="margin-bottom: 25px; background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); border: 1px solid var(--accent); padding: 22px;"> <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 20px;"> <div> <h3 style="margin: 0 0 4px; color: var(--accent); font-weight: 800; font-size: 20px;">💵 สรุปยอดขายและภาษีรายวัน (Daily Sales & Tax Report)</h3> <small style="color: var(--text-secondary);">สามารถพิมพ์ระบุวันที่ หรือกดปุ่มปฏิทินเลือกวันที่ต้องการดูยอดรวม คำนวณภาษีมูลค่าเพิ่ม (VAT 7%) และออกรายงานปิดยอดได้ทันที</small> </div> <!-- Quick Date Shortcut Pills & Export CSV --> <div style="display: flex; gap: 8px; flex-wrap: wrap; align-items: center;"> <a href="store-admin.php?export_sales_csv=1" class="btn-sm" style="background: #10B981; color: #fff; border: none; padding: 7px 14px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px; display: inline-flex; align-items: center; gap: 4px;"> 📊 ส่งออกไฟล์ Excel (CSV) </a> <a href="store-admin.php?tab=daily-sales&sales_date=<?php echo date('Y-m-d'); ?>" class="btn-sm" style="background: rgba(0, 229, 255, 0.15); color: var(--accent); border: 1px solid var(--border-glass); padding: 6px 12px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px;"> 📌 วันนี้ (<?php echo date('d/m/Y'); ?>) </a> <a href="store-admin.php?tab=daily-sales&sales_date=<?php echo date('Y-m-d', strtotime('-1 day')); ?>" class="btn-sm" style="background: rgba(255, 255, 255, 0.05); color: #cbd5e1; border: 1px solid var(--border-glass); padding: 6px 12px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px;"> ⏪ เมื่อวาน (<?php echo date('d/m/Y', strtotime('-1 day')); ?>) </a> </div> </div> <hr style="border: 0; border-top: 1px solid rgba(255,255,255,0.08); margin: 18px 0;"> <form method="GET" action="store-admin.php" style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 15px;"> <input type="hidden" name="tab" value="daily-sales"> <div style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; flex: 1;"> <div style="display: flex; align-items: center; gap: 8px;"> <button type="button" onclick="triggerSalesCalendarPicker()" title="คลิกเพื่อเปิดปฏิทินเลือกวันที่" style="background: transparent; border: none; font-size: 26px; cursor: pointer; padding: 0; line-height: 1; display: inline-flex; align-items: center; transition: transform 0.15s ease;" onmouseover="this.style.transform='scale(1.2)'" onmouseout="this.style.transform='scale(1)'"> 🗓️ </button> <input type="date" name="sales_date" id="sales_date" class="form-control" value="<?php echo htmlspecialchars($selected_date); ?>" style="padding: 8px 14px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 8px; font-weight: bold; font-size: 14px; cursor: pointer; color-scheme: dark; user-select: none;" onchange="this.form.submit()" onclick="triggerSalesCalendarPicker()" onkeydown="return false;" title="คลิกเพื่อเปิดปฏิทินเลือกวันที่"> </div> <div style="display: flex; gap: 6px; align-items: center;"> <a href="store-admin.php?tab=daily-sales&sales_date=<?php echo date('Y-m-d'); ?>" class="btn-sm" style="background: rgba(0, 229, 255, 0.15); color: var(--accent); border: 1px solid var(--border-glass); padding: 7px 12px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px;"> 📌 วันนี้ </a> <a href="store-admin.php?tab=daily-sales&sales_date=<?php echo date('Y-m-d', strtotime('-1 day')); ?>" class="btn-sm" style="background: rgba(255, 255, 255, 0.05); color: #cbd5e1; border: 1px solid var(--border-glass); padding: 7px 12px; border-radius: 20px; font-weight: bold; text-decoration: none; font-size: 12.5px;"> ⏪ เมื่อวาน </a> </div> </div> <div style="background: rgba(16, 185, 129, 0.15); border: 1px solid #10B981; padding: 8px 16px; border-radius: 8px; color: #10B981; font-weight: bold; font-size: 13.5px;"> 📅 วันที่ดูยอดขาย: <?php echo date('d/m/Y', strtotime($selected_date)); ?> </div> </form> </div> <!-- Financial Summary Stat Cards Grid --> <div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; margin-bottom: 30px;"> <div class="stat-card" style="border-top: 4px solid #10B981;"> <div class="stat-info"> <h3>ยอดขายรวมสุทธิ</h3> <div style="font-size: 24px; font-weight: 800; color: #10B981;">฿<?php echo number_format($total_revenue, 2); ?></div> <small style="color: var(--text-secondary);">ประจำวันที่ <?php echo date('d/m/Y', strtotime($selected_date)); ?></small> </div> <div class="stat-icon" style="color: #10B981;">💰</div> </div> <div class="stat-card" style="border-top: 4px solid #00E5FF;"> <div class="stat-info"> <h3>จำนวนบิลที่เช็กบิลแล้ว</h3> <div style="font-size: 24px; font-weight: 800; color: #00E5FF;"><?php echo number_format($total_completed); ?> บิล</div> <small style="color: var(--text-secondary);">ยกเลิก <?php echo $cancelled_count; ?> บิล</small> </div> <div class="stat-icon" style="color: #00E5FF;">🧾</div> </div> <div class="stat-card" style="border-top: 4px solid #FF9F43;"> <div class="stat-info"> <h3>ประมาณการ VAT 7%</h3> <div style="font-size: 24px; font-weight: 800; color: #FF9F43;">฿<?php echo number_format($vat_7, 2); ?></div> <small style="color: var(--text-secondary);">สำหรับนำส่งยื่นภาษีประจำวัน</small> </div> <div class="stat-icon" style="color: #FF9F43;">🏛️</div> </div> <div class="stat-card" style="border-top: 4px solid #A855F7;"> <div class="stat-info"> <h3>รายได้หลังหักภาษี (Net)</h3> <div style="font-size: 24px; font-weight: 800; color: #A855F7;">฿<?php echo number_format($net_revenue, 2); ?></div> <small style="color: var(--text-secondary);">เฉลี่ยบิลละ ฿<?php echo number_format($avg_order, 2); ?></small> </div> <div class="stat-icon" style="color: #A855F7;">📈</div> </div> </div> <!-- Hourly Sales Revenue & Order Volume Combination Chart --> <div class="card" style="margin-bottom: 30px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; flex-wrap: wrap; gap: 10px;"> <div> <h3 style="margin: 0; color: var(--accent); font-weight: 800; font-size: 16px;">📈 กราฟแนวโน้มยอดขายและจำนวนออเดอร์รายชั่วโมง (Hourly Revenue & Order Trend)</h3> <small style="color: var(--text-secondary);">เปรียบเทียบยอดขายรวม (บาท ฿) และจำนวนออเดอร์ (รายการ) ในแต่ละช่วงเวลาของวัน</small> </div> <span style="background: rgba(0,229,255,0.1); color: var(--accent); border: 1px solid var(--border-glass); padding: 4px 10px; border-radius: 12px; font-size: 12px; font-weight: bold;"> 📅 <?php echo date('d/m/Y', strtotime($selected_date)); ?> </span> </div> <?php if (empty($hourly_sales_raw)): ?> <div style="text-align: center; padding: 45px 20px; color: var(--text-secondary); background: rgba(0,0,0,0.2); border-radius: 10px; border: 1px dashed rgba(255,255,255,0.1);"> <div style="font-size: 32px; margin-bottom: 8px;">📭</div> <div style="font-weight: bold; font-size: 15px; color: #cbd5e1;">ไม่มีข้อมูลยอดขายและออเดอร์ในช่วงเวลานี้</div> <div style="font-size: 12.5px; margin-top: 4px;">เลือกระบุวันที่อื่นจากปฏิทินเพื่อดูรายงานยอดขายย้อนหลัง</div> </div> <?php else: ?> <div class="chart-container" style="height: 320px; position: relative;"> <canvas id="hourlySalesTrendChart"></canvas> </div> <?php endif; ?> </div> <!-- Daily Sales Doughnut Charts Row --> <div class="charts-row" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 20px; margin-bottom: 30px;"> <!-- Doughnut Chart: Sales Share by Menu --> <div class="chart-card card" style="margin-bottom: 0;"> <h3 style="margin: 0 0 15px; color: var(--accent); font-weight: 800; font-size: 15px;">🍩 สัดส่วนยอดขายแยกตามรายการอาหาร (Menu Revenue Share)</h3> <?php if (empty($daily_items)): ?> <p style="color: var(--text-secondary); text-align: center; padding: 50px;">ไม่มีข้อมูลยอดขายในวันที่เลือก</p> <?php else: ?> <div class="chart-container" style="height: 280px; position: relative;"> <canvas id="dailySalesDonutChart"></canvas> </div> <?php endif; ?> </div> <!-- Doughnut Chart: Sales Share by Category --> <div class="chart-card card" style="margin-bottom: 0;"> <h3 style="margin: 0 0 15px; color: var(--accent); font-weight: 800; font-size: 15px;">🍹 สัดส่วนยอดขายแยกตามหมวดหมู่ (Category Share)</h3> <?php if (empty($daily_items)): ?> <p style="color: var(--text-secondary); text-align: center; padding: 50px;">ไม่มีข้อมูลยอดขายในวันที่เลือก</p> <?php else: ?> <div class="chart-container" style="height: 280px; position: relative;"> <canvas id="dailyCategoryDonutChart"></canvas> </div> <?php endif; ?> </div> </div> <!-- Itemized Sales Breakdown Table --> <div class="card" style="margin-bottom: 30px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; flex-wrap: wrap; gap: 15px;"> <h3 style="margin:0; color: var(--accent); font-weight: 800;">📊 สรุปยอดขายจำแนกตามเมนูอาหาร (Itemized Sales Report)</h3> <button type="button" onclick="printDailyZReport('<?php echo htmlspecialchars($store_info['store_name'], ENT_QUOTES); ?>', '<?php echo $selected_date; ?>', '<?php echo $total_revenue; ?>', '<?php echo $total_completed; ?>', '<?php echo $vat_7; ?>', '<?php echo $net_revenue; ?>', '<?php echo htmlspecialchars(json_encode($daily_items, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE), ENT_QUOTES); ?>')" class="btn-sm" style="background: #10B981; color: #fff; border:none; padding: 10px 18px; border-radius: 6px; font-weight: bold; cursor: pointer; font-size: 13.5px; display: flex; align-items: center; gap: 6px;"> 🖨️ พิมพ์รายงานปิดยอดขายประจำวัน (Z-Report / Tax Summary) </button> </div> <?php if (empty($daily_items)): ?> <p style="color: var(--text-secondary); text-align: center; padding: 40px;">ไม่มีข้อมูลยอดขายในวันที่เลือก</p> <?php else: ?> <div style="overflow-x: auto;"> <table style="width: 100%; border-collapse: collapse; font-size: 14px; text-align: left;"> <thead> <tr style="border-bottom: 2px solid var(--border-glass); color: var(--accent);"> <th style="padding: 12px;">#</th> <th style="padding: 12px;">ชื่อเมนูอาหาร</th> <th style="padding: 12px;">หมวดหมู่</th> <th style="padding: 12px; text-align: center;">จำนวนที่ขายได้</th> <th style="padding: 12px; text-align: right;">ยอดรวมเงิน (บาท)</th> <th style="padding: 12px; text-align: right;">สัดส่วนยอดขาย</th> </tr> </thead> <tbody> <?php foreach ($daily_items as $idx => $di): $pct = $total_revenue > 0 ? ($di['total_amount'] / $total_revenue * 100) : 0; ?> <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"> <td style="padding: 12px; font-weight: bold; color: var(--text-secondary);"><?php echo ($idx + 1); ?></td> <td style="padding: 12px; font-weight: bold; color: #fff;"><?php echo htmlspecialchars($di['menu_name'] ?: 'รายการอาหาร'); ?></td> <td style="padding: 12px; color: var(--text-secondary);"><?php echo htmlspecialchars($di['category'] ?: 'ทั่วไป'); ?></td> <td style="padding: 12px; text-align: center; font-weight: bold; color: var(--accent);"><?php echo number_format($di['total_qty']) . ' ' . (function_exists('getItemUnitPHP') ? getItemUnitPHP($di['menu_name'], $di['category']) : (preg_match('/(เครื่องดื่ม|น้ำ|ชา|กาแฟ|นม|โซดา)/i', $di['menu_name'].$di['category']) ? 'แก้ว' : 'จาน')); ?></td> <td style="padding: 12px; text-align: right; font-weight: bold; color: #fff;">฿<?php echo number_format($di['total_amount'], 2); ?></td> <td style="padding: 12px; text-align: right;"> <span style="background: rgba(0,229,255,0.1); color: var(--accent); padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: bold;"> <?php echo number_format($pct, 1); ?>% </span> </td> </tr> <?php endforeach; ?> </tbody> <tfoot> <tr style="border-top: 2px solid var(--accent); font-weight: 800; font-size: 15px; color: #fff; background: rgba(0,229,255,0.05);"> <td colspan="3" style="padding: 14px;">รวมทั้งสิ้น (Grand Total)</td> <td style="padding: 14px; text-align: center; color: var(--accent);"><?php echo number_format(array_sum(array_column($daily_items, 'total_qty'))); ?> รายการ</td> <td style="padding: 14px; text-align: right; color: #10B981; font-size: 18px;">฿<?php echo number_format($total_revenue, 2); ?></td> <td style="padding: 14px; text-align: right; color: var(--accent);">100.0%</td> </tr> </tfoot> </table> </div> <?php endif; ?> </div> <!-- Searchable Historical Orders & Thermal Receipt Management --> <div class="card" style="margin-bottom: 30px;"> <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; flex-wrap: wrap; gap: 15px;"> <div> <h3 style="margin: 0; color: var(--accent); font-weight: 800;">🧾 ประวัติใบเสร็จรับเงินย้อนหลัง (Backdated Order & Receipt History)</h3> <small style="color: var(--text-secondary);">รักษาราคาและรายการสินค้าตามจริง ณ วันเวลาสั่งซื้อ (Original Price Snapshot) • พิมพ์ใบเสร็จซ้ำได้ตลอดเวลา</small> </div> </div> <!-- Filter & Search Toolbar --> <form method="GET" action="store-admin.php" style="background: rgba(0,0,0,0.25); padding: 15px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.08); margin-bottom: 20px; display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; align-items: end;"> <input type="hidden" name="tab" value="daily-sales"> <input type="hidden" name="sales_date" value="<?php echo htmlspecialchars($selected_date); ?>"> <div> <label style="font-size: 12.5px; font-weight: bold; color: var(--text-secondary); display: block; margin-bottom: 4px;">🗓️ จากวันที่:</label> <input type="date" name="history_start_date" class="form-control" value="<?php echo htmlspecialchars($history_start_date); ?>" style="padding: 8px 12px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 6px; font-size: 13px; width: 100%; cursor: pointer; color-scheme: dark; user-select: none;" onkeydown="return false;" onclick="if(typeof this.showPicker==='function') this.showPicker();" title="คลิกเพื่อเลือกวันที่"> </div> <div> <label style="font-size: 12.5px; font-weight: bold; color: var(--text-secondary); display: block; margin-bottom: 4px;">🗓️ ถึงวันที่:</label> <input type="date" name="history_end_date" class="form-control" value="<?php echo htmlspecialchars($history_end_date); ?>" style="padding: 8px 12px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 6px; font-size: 13px; width: 100%; cursor: pointer; color-scheme: dark; user-select: none;" onkeydown="return false;" onclick="if(typeof this.showPicker==='function') this.showPicker();" title="คลิกเพื่อเลือกวันที่"> </div> <div> <label style="font-size: 12.5px; font-weight: bold; color: var(--text-secondary); display: block; margin-bottom: 4px;">💳 ช่องชำระเงิน:</label> <select name="history_payment" class="form-control" style="padding: 8px 12px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 6px; font-size: 13px; width: 100%;"> <option value="all" <?php echo $history_payment === 'all' ? 'selected' : ''; ?>>ทั้งหมด (All Methods)</option> <option value="transfer" <?php echo $history_payment === 'transfer' ? 'selected' : ''; ?>>💳 โอนจ่าย (PromptPay QR)</option> <option value="cash" <?php echo $history_payment === 'cash' ? 'selected' : ''; ?>>💵 เงินสด (Cash)</option> </select> </div> <div> <label style="font-size: 12.5px; font-weight: bold; color: var(--text-secondary); display: block; margin-bottom: 4px;">🔍 ค้นหา (ID / โต๊ะ):</label> <input type="text" name="history_search" class="form-control" value="<?php echo htmlspecialchars($history_search); ?>" placeholder="เช่น #43 หรือ 1" style="padding: 8px 12px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 6px; font-size: 13px; width: 100%;"> </div> <div> <button type="submit" class="btn-sm" style="background: var(--accent); color: #0f172a; border: none; padding: 9px 16px; border-radius: 6px; font-weight: 800; cursor: pointer; font-size: 13px; width: 100%;"> 🔍 ค้นหาประวัติ </button> </div> </form> <?php if (empty($closed_orders)): ?> <p style="color: var(--text-secondary); text-align: center; padding: 40px; background: rgba(0,0,0,0.2); border-radius: 8px;">ไม่พบประวัติคำสั่งซื้อ/ใบเสร็จรับเงินตามเงื่อนไขที่เลือก</p> <?php else: ?> <div style="overflow-x: auto;"> <table style="width: 100%; border-collapse: collapse; font-size: 13.5px; text-align: left;"> <thead> <tr style="border-bottom: 2px solid var(--border-glass); color: var(--accent);"> <th style="padding: 10px;">บิล #</th> <th style="padding: 10px;">โต๊ะ</th> <th style="padding: 10px;">วัน-เวลาที่สั่งซื้อ</th> <th style="padding: 10px;">ช่องทางชำระ</th> <th style="padding: 10px;">รายการอาหาร (Snapshot Price)</th> <th style="padding: 10px; text-align: right;">ยอดรวมสุทธิ</th> <th style="padding: 10px; text-align: center;">จัดการ / พิมพ์ใบเสร็จ</th> </tr> </thead> <tbody> <?php foreach ($closed_orders as $hord): $is_h_transfer = (($hord['payment_method'] ?? 'cash') === 'transfer' || !empty($hord['is_paid'])); $items_json = json_encode($hord['items'], JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); $h_vat = $hord['calculated_total'] * (7 / 107); $h_net = $hord['calculated_total'] - $h_vat; ?> <tr style="border-bottom: 1px solid rgba(255,255,255,0.05);"> <td style="padding: 10px; font-weight: bold; color: var(--accent);">#<?php echo $hord['id']; ?></td> <td style="padding: 10px; font-weight: bold; color: #fff;">โต๊ะ <?php echo htmlspecialchars($hord['table_number']); ?></td> <td style="padding: 10px; color: var(--text-secondary); font-size: 12.5px;"><?php echo $hord['order_time']; ?> น.</td> <td style="padding: 10px;"> <?php if ($is_h_transfer): ?> <span style="background: rgba(16,185,129,0.2); color: #10B981; border: 1px solid #10B981; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;">💳 โอนจ่าย (PromptPay)</span> <?php else: ?> <span style="background: rgba(255,159,67,0.2); color: #FF9F43; border: 1px solid #FF9F43; padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: bold;">💵 เงินสด (Cash)</span> <?php endif; ?> </td> <td style="padding: 10px; color: #cbd5e1; max-width: 260px;"> <ul style="margin:0; padding-left:14px; font-size: 12px;"> <?php foreach ($hord['items'] as $hit): ?> <li> <strong><?php echo htmlspecialchars($hit['menu_name'] ?: 'รายการอาหาร'); ?></strong> x<?php echo $hit['quantity']; ?> <span style="color: var(--text-secondary);">(฿<?php echo number_format($hit['price'], 2); ?>)</span> </li> <?php endforeach; ?> </ul> </td> <td style="padding: 10px; text-align: right; font-weight: bold; color: #10B981; font-size: 15px;"> ฿<?php echo number_format($hord['calculated_total'], 2); ?> </td> <td style="padding: 10px; text-align: center;"> <div style="display: flex; gap: 6px; justify-content: center; flex-wrap: wrap;"> <button type="button" onclick="printSingleThermalReceipt('<?php echo rawurlencode($store_info['store_name']); ?>', '<?php echo $hord['id']; ?>', '<?php echo rawurlencode($hord['table_number']); ?>', '<?php echo rawurlencode($hord['order_time']); ?>', '<?php echo rawurlencode($is_h_transfer ? 'โอนจ่าย' : 'เงินสด'); ?>', '<?php echo rawurlencode(json_encode($hord['items'], JSON_UNESCAPED_UNICODE)); ?>', '<?php echo $hord['calculated_total']; ?>', '<?php echo $h_vat; ?>', '<?php echo $h_net; ?>', '<?php echo rawurlencode($store_info['address'] ?? ''); ?>')" class="btn-sm" style="background: rgba(0, 229, 255, 0.2); color: #00E5FF; border: 1px solid #00E5FF; padding: 4px 10px; border-radius: 6px; font-size: 11.5px; font-weight: bold; cursor: pointer; display: inline-flex; align-items: center; gap: 4px;"> 🖨️ พิมพ์ใบเสร็จ </button> <?php if (!empty($hord['slip_url'])): ?> <button type="button" onclick="viewSlipImage('<?php echo rawurlencode($hord['slip_url']); ?>', '<?php echo $hord['id']; ?>')" class="btn-sm" style="background: rgba(16,185,129,0.2); color: #10B981; border: 1px solid #10B981; padding: 4px 8px; border-radius: 6px; font-size: 11px; font-weight: bold; cursor: pointer;"> 🖼️ สลิป </button> <?php endif; ?> </div> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <?php endif; ?> </div> </section> <!-- 2. Menu & Price Manager Tab --> <section id="menu-mgr-sec" class="tab-sec <?php echo ($active_tab === 'menu-mgr') ? 'active' : ''; ?>"> <!-- Category Management Card --> <div class="card" style="margin-bottom: 25px;"> <div style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:12px; margin-bottom: 15px;"> <div> <h3 style="margin:0; color:var(--accent);">🏷️ ระบบจัดการหมวดหมู่อาหารและการเรียงลำดับ (Category Manager)</h3> <small style="color:var(--text-secondary);">เพิ่ม แก้ไข ลบ เปิด/ปิดการใช้งานหมวดหมู่ และกำหนดลำดับแสดงผล (Sort Order)</small> </div> <button type="button" onclick="saveCategoryOrder()" class="btn-sm" style="background:#10B981; color:#fff; border:none; padding:8px 16px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:13px; display:inline-flex; align-items:center; gap:6px; box-shadow: 0 2px 8px rgba(16,185,129,0.3);"> 💾 บันทึกลำดับหมวดหมู่ </button> </div> <!-- Add Category Form --> <form method="POST" style="display:flex; gap:10px; margin-bottom: 15px; align-items:center; flex-wrap:wrap;"> <input type="hidden" name="add_category" value="1"> <input type="text" name="category_name" class="form-control" placeholder="ระบุชื่อหมวดหมู่ใหม่ เช่น อาหารจานเดียว, เครื่องดื่ม, ของทานเล่น" style="flex:1; min-width:240px;" required> <button type="submit" class="btn-primary" style="white-space:nowrap; padding:10px 18px; font-weight:bold; background:var(--accent); color:#0f172a; border-radius:8px;">+ เพิ่มหมวดหมู่</button> </form> <!-- Categories List Table --> <?php if (empty($store_categories)): ?> <p style="color:var(--text-secondary); text-align:center; padding:15px; background:rgba(0,0,0,0.2); border-radius:8px;">ยังไม่มีหมวดหมู่อาหารในระบบ</p> <?php else: ?> <div style="overflow-x:auto;"> <table style="width:100%; border-collapse:collapse; font-size:13.5px; text-align:left;"> <thead> <tr style="border-bottom:2px solid var(--border-glass); color:var(--accent);"> <th style="padding:10px; width:80px; text-align:center;">ลำดับ</th> <th style="padding:10px;">ชื่อหมวดหมู่</th> <th style="padding:10px; text-align:center;">สถานะใช้งาน</th> <th style="padding:10px; text-align:right;">จัดการ</th> </tr> </thead> <tbody> <?php foreach ($store_categories as $cat): ?> <tr style="border-bottom:1px solid rgba(255,255,255,0.05);"> <td style="padding:8px; text-align:center;"> <input type="number" class="cat-sort-input form-control" data-id="<?php echo $cat['id']; ?>" value="<?php echo (int)$cat['sort_order']; ?>" style="width:60px; text-align:center; padding:4px; font-weight:bold; color:var(--accent); background:#0f172a; margin:0 auto; border:1px solid var(--border-glass); border-radius:6px;"> </td> <td style="padding:8px; font-weight:bold; color:#fff;"> <?php echo htmlspecialchars($cat['name']); ?> </td> <td style="padding:8px; text-align:center;"> <a href="store-admin.php?action=toggle_category&category_id=<?php echo $cat['id']; ?>" style="text-decoration:none;" title="คลิกเพื่อเปิด/ปิดหมวดหมู่"> <?php if ($cat['is_active']): ?> <span style="background:rgba(16,185,129,0.15); color:#10B981; padding:3px 10px; border-radius:12px; font-size:11.5px; font-weight:bold;">🟢 เปิดใช้งาน</span> <?php else: ?> <span style="background:rgba(244,67,54,0.15); color:#f44336; padding:3px 10px; border-radius:12px; font-size:11.5px; font-weight:bold;">🔴 ปิดใช้งาน</span> <?php endif; ?> </a> </td> <td style="padding:8px; text-align:right;"> <button type="button" onclick="editCategoryPrompt(<?php echo $cat['id']; ?>, '<?php echo htmlspecialchars(addslashes($cat['name']), ENT_QUOTES); ?>')" class="btn-sm" style="background:var(--accent); color:#0f172a; border:none; padding:4px 10px; border-radius:4px; font-weight:bold; cursor:pointer; margin-right:6px;">แก้ไข</button> <a href="store-admin.php?action=delete_category&category_id=<?php echo $cat['id']; ?>" onclick="return confirm('ยืนยันลบหมวดหมู่ <?php echo htmlspecialchars(addslashes($cat['name']), ENT_QUOTES); ?>?')" style="color:#f44336; text-decoration:none; font-weight:bold; font-size:12px;">ลบ</a> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <?php endif; ?> </div> <div class="split-grid"> <!-- Left: List --> <div class="card"> <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:15px; flex-wrap:wrap; gap:10px;"> <h3 style="margin:0;">📋 รายการอาหารทั้งหมดในร้าน (<?php echo count($menus); ?> รายการ)</h3> <button type="button" onclick="saveMenuOrder()" class="btn-sm" style="background:#10B981; color:#fff; border:none; padding:8px 16px; border-radius:6px; font-weight:bold; cursor:pointer; font-size:13px; box-shadow: 0 2px 8px rgba(16,185,129,0.3);"> 💾 บันทึกลำดับเมนู </button> </div> <?php if (empty($menus)): ?> <p style="color: var(--text-secondary); text-align: center;">ยังไม่มีรายการอาหารในร้านอาหารของท่าน</p> <?php else: ?> <div style="display: flex; flex-direction: column; gap: 15px;"> <?php foreach ($menus as $m): ?> <div class="menu-item-card" style="display:flex; align-items:center; gap:12px;"> <div style="display:flex; flex-direction:column; align-items:center; justify-content:center;"> <span style="font-size:10px; color:var(--text-secondary); margin-bottom:2px;">ลำดับ</span> <input type="number" class="menu-sort-input form-control" data-id="<?php echo (int)$m['menu_id']; ?>" value="<?php echo (int)($m['sort_order'] ?? 0); ?>" title="ลำดับแสดงผล" style="width:52px; text-align:center; padding:4px; font-weight:bold; color:var(--accent); background:#0f172a; border:1px solid var(--border-glass); border-radius:6px;"> </div> <img src="<?php echo htmlspecialchars($m['image_url'] ?: 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=120&auto=format&fit=crop&q=60'); ?>" alt="Food Image" class="menu-item-img" onerror="this.onerror=null; this.src='https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=600&auto=format&fit=crop&q=80';" loading="lazy" decoding="async"> <div class="menu-item-info" style="flex:1;"> <h4 style="margin: 0; color: #fff;"><?php echo htmlspecialchars($m['name']); ?></h4> <div style="color: var(--text-secondary); font-size: 13px; margin-top: 4px;"> <strong>ราคา:</strong> ฿<?php echo number_format($m['price'], 2); ?> / <?php echo htmlspecialchars($m['unit'] ?? getItemUnitPHP($m['name'], $m['category'])); ?> | <strong>หมวดหมู่:</strong> <?php echo htmlspecialchars($m['category']); ?> </div> <div style="color: var(--text-secondary); font-size: 12px; margin-top: 2px;"> <strong>ตัวเลือกรสชาติ:</strong> <span style="color: var(--accent);"><?php echo htmlspecialchars($m['spice_options'] ?: 'เผ็ดปกติ,ไม่เผ็ด,เผ็ดน้อย,เผ็ดมาก'); ?></span> </div> <div style="margin-top: 6px; font-size: 12px;"> <strong>สถานะการขาย:</strong> <?php if ($m['is_available']): ?> <span style="color:#4CAF50; font-weight:bold;">เปิดจำหน่าย</span> <?php else: ?> <span style="color:#f44336; font-weight:bold;">ของหมดชั่วคราว</span> <?php endif; ?> </div> </div> <div style="display: flex; flex-direction: column; gap: 8px; align-items: flex-end;"> <button type="button" class="btn-sm btn-edit-menu-trigger" data-id="<?php echo (int)$m['menu_id']; ?>" data-name="<?php echo htmlspecialchars($m['name'], ENT_QUOTES, 'UTF-8'); ?>" data-price="<?php echo (float)$m['price']; ?>" data-category="<?php echo htmlspecialchars($m['category'], ENT_QUOTES, 'UTF-8'); ?>" data-image="<?php echo htmlspecialchars($m['image_url'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" data-available="<?php echo (int)$m['is_available']; ?>" data-spice="<?php echo htmlspecialchars($m['spice_options'] ?? '', ENT_QUOTES, 'UTF-8'); ?>" data-unit="<?php echo htmlspecialchars($m['unit'] ?? getItemUnitPHP($m['name'], $m['category']), ENT_QUOTES, 'UTF-8'); ?>" style="background: var(--accent); color: #0f172a; border: none; padding: 6px 14px; border-radius: 6px; font-weight: 800; cursor: pointer; font-size: 12.5px; display: inline-flex; align-items: center; gap: 4px; box-shadow: 0 2px 8px rgba(0,229,255,0.2);"> แก้ไข </button> <a href="store-admin.php?action=delete_menu&menu_id=<?php echo $m['menu_id']; ?>" onclick="return confirm('ยืนยันลบเมนู <?php echo htmlspecialchars(addslashes($m['name']), ENT_QUOTES); ?> ออกจากระบบ?')" class="btn-sm btn-danger" style="display: inline-flex; align-items: center; gap: 4px; padding: 6px 14px; border-radius: 6px; font-size: 12.5px; font-weight: bold; text-decoration: none;"> ลบ </a> </div> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> <!-- Right: Add Form --> <div class="card" id="add-menu-card-section"> <h3>เพิ่มรายการอาหารในเมนู (Add Menu)</h3> <form method="POST" enctype="multipart/form-data"> <input type="hidden" name="add_menu_item" value="1"> <div class="form-group"> <label class="form-label" for="menu_name">ชื่อรายการอาหาร *</label> <input type="text" name="menu_name" id="menu_name" class="form-control" placeholder="เช่น ข้าวกะเพราหมูสับไข่ดาว" required> </div> <div class="form-group" style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px;"> <div> <label class="form-label" for="menu_price">ราคา (บาท) *</label> <input type="number" step="0.01" name="menu_price" id="menu_price" class="form-control" placeholder="50.00" required> </div> <div> <label class="form-label" for="menu_category">หมวดหมู่รายการ *</label> <?php if (!empty($store_categories)): ?> <select name="menu_category" id="menu_category" class="form-control" required style="padding:10px 12px; background:#0f172a; color:#fff; border:1px solid var(--accent); border-radius:8px;"> <option value="">-- เลือกหมวดหมู่ --</option> <?php foreach ($store_categories as $c): ?> <option value="<?php echo htmlspecialchars($c['name']); ?>"><?php echo htmlspecialchars($c['name']); ?></option> <?php endforeach; ?> </select> <?php else: ?> <input type="text" name="menu_category" id="menu_category" class="form-control" placeholder="เช่น อาหารจานเดียว, เครื่องดื่ม" required> <?php endif; ?> </div> <div> <label class="form-label" for="menu_unit">หน่วยนับ</label> <input type="text" name="menu_unit" id="menu_unit" class="form-control" placeholder="เช่น จาน, แก้ว, ชิ้น, ถ้วย, ขวด"> </div> </div> <div class="form-group" style="margin-top: 15px;"> <label class="form-label" for="add_menu_spice_options">ตัวเลือกรสชาติเพิ่มเติม</label> <input type="text" name="menu_spice_options" id="add_menu_spice_options" class="form-control" placeholder="เช่น หวานปกติ, หวานน้อย, ไม่หวาน หรือ เผ็ดปกติ, เผ็ดน้อย"> <small style="color: var(--text-secondary); display: block; margin-top: 4px;">คั่นแต่ละตัวเลือกด้วยเครื่องหมายจุลภาค (,) หากเว้นว่างไว้จะใช้ค่าเริ่มต้น</small> </div> <div class="form-group" style="border: 1px dashed var(--accent); padding: 15px; border-radius: 8px; background: rgba(0,229,255,0.03);"> <label class="form-label" for="menu_image_file" style="color: var(--accent); font-weight: bold; display: flex; align-items: center; gap: 6px;"> แนบไฟล์รูปภาพอาหาร (อัปโหลดจากเครื่อง) </label> <input type="file" name="menu_image_file" id="menu_image_file" class="form-control" accept="image/*" style="padding: 6px;"> <small style="color: var(--text-secondary); display: block; margin-top: 4px;">รองรับไฟล์ JPG, PNG, WEBP หรือ GIF</small> </div> <div id="add_menu_preview_container" style="display: none; margin-bottom: 15px; text-align: center; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);"> <div id="add_menu_preview_status" style="font-size: 12px; color: var(--accent); margin-bottom: 6px; font-weight: bold;">ตัวอย่างรูปภาพเมนูใหม่ที่จะอัปโหลด:</div> <img id="add_menu_img_preview" src="" alt="Food Preview" style="max-height: 100px; border-radius: 8px; object-fit: cover;"> </div> <div class="form-group"> <label class="form-label" for="menu_image_url">หรือ ระบุ URL ลิงก์รูปภาพอาหาร (Image URL)</label> <input type="text" name="menu_image_url" id="menu_image_url" class="form-control" placeholder="https://images.unsplash.com/photo-1512058564366... หรือ uploads/menus/food.jpg"> </div> <div class="form-group" style="display:flex; align-items:center; gap: 10px; margin-top: 20px;"> <input type="checkbox" name="menu_is_available" id="menu_is_available" checked value="1"> <label for="menu_is_available" style="font-size: 13.5px; user-select:none; cursor:pointer;">เปิดให้สั่งเมนูนี้ (Is Available)</label> </div> <button type="submit" class="btn-primary" style="margin-top: 15px; width: 100%; font-weight: bold; background: var(--accent); color: #0f172a; padding: 12px; font-size: 15px; border-radius: 8px;">บันทึกและอัปโหลดเมนูอาหาร</button> </form> </div> </div> </section> <!-- 3. Promotions & Policy Tab --> <section id="promo-sec" class="tab-sec <?php echo ($active_tab === 'promo') ? 'active' : ''; ?>"> <div class="card" style="max-width: 750px; margin: 0 auto;"> <h3>⚙️ ตั้งค่ารายละเอียดร้านค้า โลโก้ ภาพปก และนโยบาย</h3> <form method="POST" enctype="multipart/form-data"> <input type="hidden" name="update_store_profile" value="1"> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;"> <div class="form-group"> <label class="form-label" for="store_name">ชื่อร้านอาหาร *</label> <input type="text" name="store_name" id="store_name" class="form-control" value="<?php echo htmlspecialchars($store_info['store_name']); ?>" required> </div> <div class="form-group"> <label class="form-label" for="category">หมวดหมู่อาหาร/ประเภทร้าน</label> <input type="text" name="category" id="category" class="form-control" value="<?php echo htmlspecialchars($store_info['category']); ?>" placeholder="เช่น อาหารตามสั่ง / ชาและเครื่องดื่ม"> </div> </div> <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;"> <div class="form-group"> <label class="form-label" for="phone">📞 เบอร์โทรศัพท์ติดต่อร้านค้า</label> <input type="text" name="phone" id="phone" class="form-control" value="<?php echo htmlspecialchars($store_info['phone'] ?? ''); ?>" placeholder="053-123456 / 081-2345678"> </div> <div class="form-group"> <label class="form-label" for="line_id">💬 LINE ID / Contact</label> <input type="text" name="line_id" id="line_id" class="form-control" value="<?php echo htmlspecialchars($store_info['line_id'] ?? ''); ?>" placeholder="@store_name"> </div> </div> <!-- Logo Upload & URL --> <div class="form-group" style="background: rgba(255,255,255,0.03); border: 1px dashed var(--border-glass); border-radius: 12px; padding: 15px; margin-bottom: 20px;"> <label class="form-label" style="color: var(--accent); font-weight: 800;">🖼️ อัปโหลดโลโก้ร้านค้า (Store Logo)</label> <div id="logo_preview_container" style="display: flex; align-items: center; gap: 15px; margin-bottom: 12px; background: rgba(15, 23, 42, 0.6); padding: 10px 14px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.08);"> <img id="logo_preview_img" src="<?php echo htmlspecialchars($store_info['custom_logo_url'] ?: 'logo.png'); ?>" alt="Logo Preview" style="width: 65px; height: 65px; object-fit: contain; border-radius: 10px; background: #fff; padding: 4px; border: 1px solid rgba(255,255,255,0.2);" onerror="this.src='https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=120&auto=format&fit=crop&q=60'"> <div> <div id="logo_preview_status" style="font-size: 12px; color: <?php echo !empty($store_info['custom_logo_url']) ? '#10B981' : '#94a3b8'; ?>; font-weight: 800;"> <?php echo !empty($store_info['custom_logo_url']) ? '🟢 มีรูปโลโก้ปัจจุบันในระบบแล้ว' : 'ℹ️ ยังไม่ได้อัปโหลดโลโก้ร้านค้า'; ?> </div> <div style="font-size: 11px; color: var(--text-secondary); margin-top: 3px;">รูปภาพจะแสดงตัวอย่างทันทีเมื่อเลือกไฟล์ใหม่หรือวางลิงก์</div> </div> </div> <input type="file" name="logo_file" id="logo_file_input" accept="image/*" class="form-control" style="margin-bottom: 8px;"> <input type="url" name="custom_logo_url" id="custom_logo_url" class="form-control" value="<?php echo htmlspecialchars($store_info['custom_logo_url'] ?: ''); ?>" placeholder="หรือระบุ URL ลิงก์รูปภาพโลโก้ https://..."> </div> <!-- Cover Banner Upload & URL --> <div class="form-group" style="background: rgba(255,255,255,0.03); border: 1px dashed var(--border-glass); border-radius: 12px; padding: 15px; margin-bottom: 20px;"> <label class="form-label" style="color: #FF9F43; font-weight: 800;">🏞️ อัปโหลดรูปภาพปกแบนเนอร์หน้าร้าน (Store Banner Cover Image)</label> <div id="banner_preview_container" style="margin-bottom: 12px; background: rgba(15, 23, 42, 0.6); padding: 10px; border-radius: 10px; border: 1px solid rgba(255,255,255,0.08);"> <img id="banner_preview_img" src="<?php echo htmlspecialchars($store_info['store_banner_url'] ?: ''); ?>" alt="Banner Preview" style="width: 100%; height: 120px; object-fit: cover; border-radius: 8px; <?php echo empty($store_info['store_banner_url']) ? 'display:none;' : ''; ?>" onerror="this.style.display='none';"> <div id="banner_preview_status" style="font-size: 12px; color: <?php echo !empty($store_info['store_banner_url']) ? '#10B981' : '#94a3b8'; ?>; font-weight: 800; margin-top: 4px;"> <?php echo !empty($store_info['store_banner_url']) ? '🟢 มีภาพปกแบนเนอร์ปัจจุบันแล้ว' : 'ℹ️ ยังไม่ได้อัปโหลดภาพปกแบนเนอร์'; ?> </div> </div> <input type="file" name="banner_file" id="banner_file_input" accept="image/*" class="form-control" style="margin-bottom: 8px;"> <input type="url" name="store_banner_url" id="store_banner_url" class="form-control" value="<?php echo htmlspecialchars($store_info['store_banner_url'] ?? ''); ?>" placeholder="หรือระบุ URL ลิงก์ภาพแบนเนอร์ https://images.unsplash.com/..."> </div> <div class="form-group"> <label class="form-label" for="address">📍 ที่ตั้งสาขา / พิกัดร้านค้า</label> <textarea name="address" id="address" class="form-control" rows="2"><?php echo htmlspecialchars($store_info['address']); ?></textarea> </div> <div class="form-group"> <label class="form-label" for="promo_banner">📢 ข้อความแบนเนอร์โปรโมชันประจำร้าน (Promo Banner Text)</label> <input type="text" name="promo_banner" id="promo_banner" class="form-control" value="<?php echo htmlspecialchars($store_info['promo_banner']); ?>" placeholder="เช่น แถมฟรีไข่ดาวทุกจานเมื่อสั่งผ่านเว็บ!"> </div> <div class="form-group"> <label class="form-label" for="policy_text">📜 นโยบายร้านค้าและการรับประทาน (Store Policy) *</label> <textarea name="policy_text" id="policy_text" class="form-control" rows="3" required><?php echo htmlspecialchars($store_info['policy_text']); ?></textarea> </div> <button type="submit" class="btn-primary" style="margin-top: 15px; width: 100%; font-size: 15px; font-weight: 800;">✓ บันทึกการตั้งค่าข้อมูลและรูปภาพร้านค้า</button> </form> </div> </section> <!-- 4. QR Codes & Tables Tab --> <section id="qrs-sec" class="tab-sec <?php echo ($active_tab === 'qrs') ? 'active' : ''; ?>"> <div class="split-grid"> <!-- Left: Generator --> <div class="card"> <h3>📊 ระบบสร้าง QR Code ประจำโต๊ะอาหาร</h3> <div style="background: rgba(0, 229, 255, 0.05); border: 1px solid var(--border-glass); border-radius: 8px; padding: 15px; margin-bottom: 20px; font-size: 13.5px; line-height: 1.6;"> <strong>แพ็กเกจปัจจุบัน:</strong> <?php echo htmlspecialchars($store_info['plan_name']); ?><br> <strong>สิทธิ์จำนวนโต๊ะสูงสุด:</strong> <?php echo $store_info['limit_tables']; ?> โต๊ะอาหาร </div> <form method="POST"> <input type="hidden" name="generate_tables" value="1"> <div class="form-group"> <label class="form-label" for="table_count">จำนวนโต๊ะที่ต้องการเปิดใช้งานในระบบ *</label> <input type="number" name="table_count" id="table_count" class="form-control" min="1" max="<?php echo $store_info['limit_tables']; ?>" value="<?php echo count($tables) ?: 2; ?>" required> </div> <button type="submit" class="btn-primary" style="margin-top: 10px;">🔄 สร้างและอัปเดตชุด QR Code โต๊ะอาหาร</button> </form> </div> <!-- Right: Display QRs --> <div class="card"> <h3>📋 ป้าย QR Code โต๊ะอาหารของร้าน</h3> <?php if (empty($tables)): ?> <p style="color: var(--text-secondary); text-align: center;">ยังไม่มีโต๊ะในระบบร้านค้า</p> <?php else: ?> <div class="qr-grid"> <?php $scheme = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http"); $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $base_dir = rtrim(dirname($_SERVER['PHP_SELF']), '/\\'); foreach ($tables as $t): $table_num_only = preg_replace('/[^0-9]/', '', $t['table_number']); if (empty($table_num_only)) $table_num_only = $t['table_number']; // Static QR URL — permanent, never changes regardless of checkout $full_qr_url = $scheme . "://" . $host . ($base_dir ? $base_dir : '') . "/menu.php?store_id=" . $store_id . "&table=" . urlencode($table_num_only); $qr_image_api = "https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=" . urlencode($full_qr_url); $tbl_status = $t['table_status'] ?? 'available'; $tbl_guest = $t['table_guest_name'] ?? ''; ?> <div class="qr-card"> <img src="<?php echo $qr_image_api; ?>" alt="Table QR"> <div style="font-weight: bold; margin-bottom: 3px; font-size: 14px; color:#fff;"><?php echo htmlspecialchars($t['table_number']); ?></div> <!-- Live Table Status Badge --> <div style="margin-bottom: 6px;"> <?php if ($tbl_status === 'occupied'): ?> <span style="background:#10B981; color:#fff; font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px;">🟢 กำลังใช้งาน</span> <?php if (!empty($tbl_guest)): ?> <div style="font-size:10px; color:var(--text-secondary); margin-top:2px;">👤 <?php echo htmlspecialchars($tbl_guest); ?></div> <?php endif; ?> <?php else: ?> <span style="background:rgba(148,163,184,0.2); color:#94a3b8; font-size:10px; font-weight:700; padding:2px 8px; border-radius:20px;">⚪ ว่าง / พร้อมรับลูกค้าใหม่</span> <?php endif; ?> </div> <div style="font-size: 11px; margin-bottom: 8px;"> <a href="<?php echo htmlspecialchars($full_qr_url); ?>" target="_blank" style="color: var(--accent); text-decoration:none; font-weight:700;">เปิดลิ้งก์โต๊ะ 🔗</a> </div> <div style="display: flex; gap: 6px; justify-content: center; flex-wrap: wrap;"> <button type="button" onclick="printQRCode('<?php echo rawurlencode($store_info['store_name']); ?>', '<?php echo rawurlencode($t['table_number']); ?>', '<?php echo rawurlencode($qr_image_api); ?>', '<?php echo rawurlencode($full_qr_url); ?>')" class="btn-sm btn-secondary" style="font-size: 11px; padding: 4px 8px; border-radius: 4px; background: #334155; color: #fff; border: none; cursor: pointer;"> 🖨️ พิมพ์ </button> <form method="POST" style="display:inline;"> <input type="hidden" name="reset_table_qr" value="1"> <input type="hidden" name="table_id" value="<?php echo $t['table_id']; ?>"> <input type="hidden" name="table_number" value="<?php echo htmlspecialchars($t['table_number']); ?>"> <button type="submit" onclick="return confirm('สร้าง QR Code ใหม่สำหรับโต๊ะนี้? (ลูกค้าเดิมจะไม่สามารถใช้ลิงก์เก่าสั่งอาหารได้อีก)')" class="btn-sm" style="font-size: 11px; padding: 4px 8px; border-radius: 4px; background: rgba(0,229,255,0.15); color: var(--accent); border: 1px solid var(--border-glass); cursor: pointer; font-weight: bold;"> 🔄 รีเซ็ต QR </button> </form> </div> </div> <?php endforeach; ?> </div> <?php endif; ?> </div> </div> </main> </div> <!-- Edit Menu Modal: single authoritative modal at bottom of page to prevent duplicate IDs --> <!-- Mobile Floating Bottom App Navigation Bar (App Experience) --> <div class="mobile-bottom-nav"> <button type="button" class="mobile-nav-btn active" id="mnav-store-dash" onclick="showTab('store-dash')"> <span style="font-size: 18px;">🏠</span> <span>หน้าแรก</span> </button> <button type="button" class="mobile-nav-btn" id="mnav-live-orders" onclick="showTab('live-orders')"> <span style="font-size: 18px;">📦</span> <span>ออเดอร์</span> </button> <button type="button" class="mobile-nav-btn" id="mnav-kds-mode" onclick="showTab('kds-mode')"> <span style="font-size: 18px;">👨🍳</span> <span>จอครัว</span> </button> <button type="button" class="mobile-nav-btn" id="mnav-daily-sales" onclick="showTab('daily-sales')"> <span style="font-size: 18px;">💵</span> <span>ยอดขาย</span> </button> <button type="button" class="mobile-nav-btn" id="mnav-menu-mgr" onclick="showTab('menu-mgr')"> <span style="font-size: 18px;">🍽️</span> <span>เมนู</span> </button> </div> <!-- Dashboard JavaScript Tab and Chart controls --> <script> // Native Calendar Picker Popup Trigger Helpers function triggerCalendarPicker(inputId) { const id = inputId || 'history_date'; const el = document.getElementById(id); if (el) { if (typeof el.showPicker === 'function') { el.showPicker(); } else { el.focus(); el.click(); } } } function triggerHistoryCalendarPicker() { triggerCalendarPicker('history_date'); } function triggerChartCalendarPicker() { triggerCalendarPicker('chart_date'); } function triggerSalesCalendarPicker() { triggerCalendarPicker('sales_date'); } // Sidebar Dropdown Accordion Toggle (Bi-directional Toggle & Auto-collapse Others) function toggleSidebarDropdown(groupId) { const targetGroup = document.getElementById(groupId); const targetArrow = document.getElementById(groupId + '-arrow'); if (!targetGroup) return; const computedDisplay = window.getComputedStyle(targetGroup).display; const isOpen = (computedDisplay !== 'none'); // Toggle target cleanly targetGroup.style.display = isOpen ? 'none' : 'flex'; if (targetArrow) { targetArrow.style.transform = isOpen ? 'rotate(0deg)' : 'rotate(180deg)'; } } // Tab switcher logic (Saves active tab to localStorage & syncs URL history) function showTab(tabId) { document.querySelectorAll('.tab-sec').forEach(sec => { sec.classList.remove('active'); }); const targetSec = document.getElementById(tabId + '-sec'); if (targetSec) targetSec.classList.add('active'); // Highlight Active Menu Button & Expand Parent Accordion Category document.querySelectorAll('.menu-btn').forEach(btn => { btn.classList.remove('active'); }); document.querySelectorAll('.sidebar-category-header').forEach(hdr => { hdr.classList.remove('active'); }); const targetBtn = document.getElementById('btn-' + tabId); if (targetBtn) { targetBtn.classList.add('active'); const parentGroup = targetBtn.closest('.sidebar-subgroup'); if (parentGroup) { parentGroup.style.display = 'flex'; const prevHeader = parentGroup.previousElementSibling; if (prevHeader && prevHeader.classList.contains('sidebar-category-header')) { prevHeader.classList.add('active'); const arrow = prevHeader.querySelector('span[id$="-arrow"]'); if (arrow) arrow.style.transform = 'rotate(180deg)'; } } } // Highlight active bottom nav button document.querySelectorAll('.mobile-nav-btn').forEach(mnav => { mnav.classList.remove('active'); }); const activeMNav = document.getElementById('mnav-' + tabId); if (activeMNav) activeMNav.classList.add('active'); localStorage.setItem('store-admin-active-tab', tabId); if (tabId === 'store-dash' && window.orderStatusChartInstance) { setTimeout(() => { try { window.orderStatusChartInstance.resize(); window.orderStatusChartInstance.update(); } catch(e) {} }, 50); } try { const url = new URL(window.location.href); url.searchParams.set('tab', tabId); window.history.replaceState({}, '', url.toString()); } catch(e) {} // Auto-collapse Mobile Sidebar Drawer & Smooth Scroll to Section on Mobile const sb = document.querySelector('.sidebar'); if (sb && sb.classList.contains('mobile-show')) { sb.classList.remove('mobile-show'); } if (targetSec && window.innerWidth <= 992) { setTimeout(() => { targetSec.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 50); } } // Quick shortcut to jump directly to Add Menu form on Mobile function quickGoToAddMenu() { showTab('menu-mgr'); setTimeout(() => { const target = document.getElementById('add-menu-card-section') || document.getElementById('menu_name'); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'center' }); const nameInput = document.getElementById('menu_name'); if (nameInput) { setTimeout(() => nameInput.focus(), 300); } } }, 150); } // View Customer Payment Slip Image Modal (SweetAlert2 Preview) function viewSlipImage(slipUrl, orderId) { if (!slipUrl) { Swal.fire({ icon: 'info', title: `🖼️ สลิปโอนเงิน ออเดอร์ #${orderId}`, text: 'ลูกค้ายืนยันการชำระเงินผ่าน QR Code เรียบร้อยแล้ว (ลูกค้ายินยอมชำระผ่าน QR แต่ไม่ได้แนบรูปภาพสลิปเพิ่มเติม)', confirmButtonText: 'ตกลง', confirmButtonColor: '#3b82f6' }); return; } Swal.fire({ title: `🖼️ สลิปโอนเงิน ออเดอร์ #${orderId}`, imageUrl: slipUrl, imageAlt: 'สลิปการโอนเงินของลูกค้า', imageWidth: 340, imageHeight: 'auto', html: `<div style="margin-top:10px; font-size:13px; color:#94a3b8; font-family:'Sarabun',sans-serif;">หลักฐานสลิปการโอนเงินที่ลูกค้าแนบส่งเข้ามาผ่านระบบ QR Code (#${orderId})</div> <div style="margin-top:8px;"><a href="${slipUrl}" target="_blank" style="color:#00E5FF; font-size:12.5px; font-weight:bold; text-decoration:underline;">🔍 คลิกเปิดดูรูปสลิปต้นฉบับขนาดเต็ม</a></div>`, showCloseButton: true, confirmButtonText: '❌ ปิดหน้าต่าง', confirmButtonColor: '#334155' }); } // ============================================================ // Checkout + Auto-Print Handler // Intercepts the checkout form submission, triggers printReceipt() // first (user must allow popups), then submits the form normally. function handleCheckoutSubmit(form, storeName, tableName, grandTotal, itemsJson, dateStr, totalTransferPaid, totalUnpaidCash) { storeName = safeDecodeURI(storeName); tableName = safeDecodeURI(tableName); dateStr = safeDecodeURI(dateStr); if (!confirm('ยืนยันรับชำระเงินและเคลียร์ โต๊ะ ' + tableName + '?\n(ระบบจะพิมพ์ใบเสร็จและรีเซ็ต QR Code ชุดใหม่สำหรับลูกค้าคนต่อไป)')) { return false; } // 1. Open receipt print window / modal printReceipt(storeName, tableName, grandTotal, itemsJson, dateStr, totalTransferPaid, totalUnpaidCash); // 2. Submit form via AJAX fetch so page navigation doesn't kill the print window! const formData = new FormData(form); fetch('store-admin.php', { method: 'POST', body: formData }).then(res => res.text()).then(() => { setTimeout(() => { location.reload(); }, 2000); }).catch(err => { form.submit(); }); return false; // Prevent hard page navigation! } // Safe URL/URI Component Decoder Helper function safeDecodeURI(str) { if (typeof str !== 'string') return str || ''; if (!str.includes('%')) return str; try { return decodeURIComponent(str); } catch(e) { try { return decodeURIComponent(str.replace(/%(?![0-9A-FA-f]{2})/g, '%25')); } catch(err) { return str; } } } // Thermal Receipt Printer Function (Supports 80mm/58mm Rolls, Transfer Badges, Net Cash Due, ESC/POS, Modal Preview & Iframe Printing Fallback) function printReceipt(storeName, tableName, grandTotal, itemsJson, dateStr, totalTransferPaid, totalUnpaidCash) { storeName = safeDecodeURI(storeName); tableName = safeDecodeURI(tableName); dateStr = safeDecodeURI(dateStr); let items = []; try { let raw = itemsJson; if (typeof raw === 'string') { raw = safeDecodeURI(raw); } if (typeof raw === 'string') { items = JSON.parse(raw); } else if (Array.isArray(raw)) { items = raw; } if (!Array.isArray(items)) items = []; } catch(e) { console.error('printReceipt: Failed to parse itemsJson:', e, itemsJson); items = []; } let calcTransferPaid = 0; let calcUnpaidCash = 0; items.forEach(it => { const qty = parseInt(it.quantity || 1); const price = parseFloat(it.price || 0); const lineTot = price * qty; if (it.is_transfer_paid || it.payment_method === 'transfer') { calcTransferPaid += lineTot; } else { calcUnpaidCash += lineTot; } }); if (totalTransferPaid === undefined || totalTransferPaid === null || totalTransferPaid === '') { totalTransferPaid = calcTransferPaid; } else { totalTransferPaid = parseFloat(totalTransferPaid); } if (totalUnpaidCash === undefined || totalUnpaidCash === null || totalUnpaidCash === '') { totalUnpaidCash = calcUnpaidCash; } else { totalUnpaidCash = parseFloat(totalUnpaidCash); } let itemsHtml = ''; items.forEach(it => { const qty = parseInt(it.quantity || 1); const price = parseFloat(it.price || 0); const lineTotal = (price * qty).toFixed(2); const name = it.name || it.menu_name || 'รายการอาหาร'; const isTransfer = !!(it.is_transfer_paid || it.payment_method === 'transfer'); let extraDetails = ''; if (it.spice_level) extraDetails += `<div style="font-size: 10px; font-weight: 400; color: #555; margin-top: 1px;">• ความเผ็ด: ${it.spice_level}</div>`; if (it.note) extraDetails += `<div style="font-size: 10px; font-weight: 700; color: #000; margin-top: 1px;">• คำขอพิเศษ: ${it.note}</div>`; const payBadge = isTransfer ? `<span style="font-size:10px; font-weight:800; color:#047857; background:#d1fae5; padding:1px 5px; border-radius:4px; margin-left:4px; display:inline-block;">[โอนจ่ายแล้ว 💳]</span>` : `<span style="font-size:10px; font-weight:800; color:#b91c1c; background:#fee2e2; padding:1px 5px; border-radius:4px; margin-left:4px; display:inline-block;">[เงินสด / ยังไม่จ่าย]</span>`; itemsHtml += ` <tr> <td style="padding: 4px 0; text-align: left;"> <div style="font-weight:700;">${name} ${payBadge}</div> ${extraDetails} </td> <td style="text-align: center; padding: 4px 0; vertical-align: top; font-weight:700;">x${qty}</td> <td style="text-align: right; padding: 4px 0; vertical-align: top; font-weight:700;">฿${lineTotal}</td> </tr> `; }); const fullDocHtml = `<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>ใบเสร็จรับเงินอย่างย่อ - โต๊ะ ${tableName}</title> <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;600;700;800&display=swap" rel="stylesheet"> <style> @page { size: 80mm auto; margin: 0; } body { font-family: 'Courier New', Courier, 'Sarabun', monospace, sans-serif; width: 78mm; margin: 0 auto; padding: 8mm 4mm; color: #000000; background: #ffffff; line-height: 1.35; font-size: 12px; font-weight: 700; box-sizing: border-box; -webkit-print-color-adjust: exact; } body.paper-58mm { width: 54mm; padding: 4mm 2mm; font-size: 10.5px; } .text-center { text-align: center; } .divider { border-top: 1px dashed #000; margin: 8px 0; } .store-title { font-size: 18px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.5px; } .sub-title { font-size: 11px; font-weight: 600; } .receipt-info { font-size: 11.5px; margin: 6px 0; } .item-table { width: 100%; border-collapse: collapse; margin: 6px 0; table-layout: fixed; } .item-table th { text-align: left; border-bottom: 1px solid #000; padding-bottom: 4px; font-size: 11px; } .total-box { border-top: 1px dashed #000; border-bottom: 2px solid #000; padding: 6px 0; margin-top: 6px; font-weight: 800; font-size: 15px; display: flex; justify-content: space-between; } .footer { text-align: center; font-size: 11px; margin-top: 12px; font-weight: 600; } .print-toolbar { background: #0f172a; color: #fff; padding: 10px; border-radius: 8px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; align-items: center; font-family: 'Sarabun', sans-serif; } .print-toolbar button, .print-toolbar select { padding: 6px 12px; border-radius: 6px; border: none; font-weight: bold; font-size: 12px; cursor: pointer; } .btn-print { background: #ff5722; color: #fff; } .btn-escpos { background: #10b981; color: #fff; } @media print { .no-print { display: none !important; } body { padding: 0 !important; margin: 0 !important; width: 100% !important; } } </style> </head> <body class="paper-80mm" id="receipt-body"> <div class="print-toolbar no-print"> <div style="display:flex; align-items:center; gap:6px;"> <span style="font-size:12px;">📏 ขนาดกระดาษ:</span> <select onchange="changePaperSize(this.value)" style="background:#1e293b; color:#fff;"> <option value="80mm" selected>80mm (Thermal POS)</option> <option value="58mm">58mm (Mini Printer)</option> </select> </div> <div style="display:flex; gap:6px;"> <button type="button" class="btn-escpos" onclick="downloadESCPOS()">⚡ โหลด ESC/POS RAW (.bin)</button> <button type="button" class="btn-print" onclick="window.print()">🖨️ พิมพ์ใบเสร็จ (Print)</button> </div> </div> <div class="text-center"> <div class="store-title">${storeName}</div> <div class="sub-title">ใบเสร็จรับเงินอย่างย่อ / TAX RECEIPT (ABB)</div> <div class="sub-title">CMTC Smart Dining System</div> </div> <div class="divider"></div> <div class="receipt-info"> <div><strong>โต๊ะอาหาร:</strong> โต๊ะ ${tableName}</div> <div><strong>วันที่/เวลา:</strong> ${dateStr}</div> <div><strong>สถานะชำระเงิน:</strong> ${totalUnpaidCash <= 0 ? 'ชำระเรียบร้อยทุกรายการ (PAID) ✅' : 'มีรายการค้างชำระเงินสด 💵'}</div> </div> <div class="divider"></div> <table class="item-table"> <thead> <tr> <th style="width: 55%;">รายการ</th> <th style="width: 15%; text-align: center;">จำนวน</th> <th style="width: 30%; text-align: right;">จำนวนเงิน</th> </tr> </thead> <tbody> ${itemsHtml} </tbody> </table> <div class="divider"></div> <div style="font-size:12px; margin: 6px 0;"> <div style="display:flex; justify-content:space-between; color:#475569; margin-bottom:3px;"> <span>ยอดรวมอาหารทั้งหมด:</span> <span>฿${parseFloat(grandTotal).toLocaleString('th-TH', {minimumFractionDigits:2, maximumFractionDigits:2})}</span> </div> ${totalTransferPaid > 0 ? ` <div style="display:flex; justify-content:space-between; color:#047857; margin-bottom:3px; font-weight:700;"> <span>หัก โอนชำระเงินแล้ว 💳:</span> <span>-฿${parseFloat(totalTransferPaid).toLocaleString('th-TH', {minimumFractionDigits:2, maximumFractionDigits:2})}</span> </div>` : ''} </div> <div class="total-box" style="border-top: 2px dashed #000; border-bottom: 2px solid #000; padding: 8px 0; margin-top: 4px; font-weight: 800; font-size: 15px; display: flex; justify-content: space-between; background: #f8fafc;"> <span>ยอดรวมคงชำระ (เงินสด):</span> <span style="color: ${totalUnpaidCash > 0 ? '#b91c1c' : '#047857'}; font-size: 17px;">฿${parseFloat(totalUnpaidCash).toLocaleString('th-TH', {minimumFractionDigits:2, maximumFractionDigits:2})}</span> </div> <div class="footer"> *** ขอบคุณที่อุดหนุนบริการครับ ***<br> CMTC Smart Dining Enterprise </div> <script> function changePaperSize(size) { const body = document.getElementById('receipt-body'); if (size === '58mm') { body.className = 'paper-58mm'; } else { body.className = 'paper-80mm'; } } function downloadESCPOS() { const store = ${JSON.stringify(storeName)}; const table = ${JSON.stringify(tableName)}; const total = ${JSON.stringify(grandTotal)}; const transferPaid = ${JSON.stringify(totalTransferPaid)}; const unpaidCash = ${JSON.stringify(totalUnpaidCash)}; const items = ${JSON.stringify(items)}; const date = ${JSON.stringify(dateStr)}; let cmd = []; cmd.push(0x1B, 0x40); cmd.push(0x1B, 0x61, 0x01); cmd.push(0x1D, 0x21, 0x11); const encoder = new TextEncoder(); const appendText = (str) => { const bytes = encoder.encode(str + "\\n"); for (let b of bytes) cmd.push(b); }; appendText(store); cmd.push(0x1D, 0x21, 0x00); appendText("TAX RECEIPT (ABB)"); appendText("------------------------------------------------"); cmd.push(0x1B, 0x61, 0x00); appendText("TABLE: " + table); appendText("DATE: " + date); appendText("CASH DUE: B" + parseFloat(unpaidCash).toFixed(2)); appendText("------------------------------------------------"); items.forEach(it => { const qty = parseInt(it.quantity || 1); const price = parseFloat(it.price || 0); const lineTot = (price * qty).toFixed(2); const name = it.name || it.menu_name || 'Item'; const tag = (it.is_transfer_paid || it.payment_method === 'transfer') ? "[PAID]" : "[CASH]"; appendText(qty + "x " + name + " " + tag + " B" + lineTot); }); appendText("------------------------------------------------"); appendText("SUBTOTAL: B" + parseFloat(total).toFixed(2)); if (parseFloat(transferPaid) > 0) { appendText("TRANSFER PAID: -B" + parseFloat(transferPaid).toFixed(2)); } cmd.push(0x1B, 0x45, 0x01); appendText("NET CASH DUE: B" + parseFloat(unpaidCash).toFixed(2)); cmd.push(0x1B, 0x45, 0x00); appendText("------------------------------------------------"); cmd.push(0x1B, 0x61, 0x01); appendText("THANK YOU VERY MUCH!"); appendText("\\n\\n\\n"); cmd.push(0x1D, 0x56, 0x41, 0x00); const blob = new Blob([new Uint8Array(cmd)], { type: 'application/octet-stream' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'receipt_escpos_table_' + table + '.bin'; a.click(); } window.onload = () => { setTimeout(() => { window.print(); }, 200); }; <\/script> </body> </html>`; // 1. Failsafe Printing using Hidden iframe (bypasses popup blockers) let printIframe = document.getElementById('receipt-print-iframe'); if (!printIframe) { printIframe = document.createElement('iframe'); printIframe.id = 'receipt-print-iframe'; printIframe.style.position = 'fixed'; printIframe.style.right = '0'; printIframe.style.bottom = '0'; printIframe.style.width = '0'; printIframe.style.height = '0'; printIframe.style.border = 'none'; document.body.appendChild(printIframe); } const printDoc = printIframe.contentWindow || printIframe.contentDocument; if (printDoc.document) { printDoc.document.open(); printDoc.document.write(fullDocHtml); printDoc.document.close(); // The iframe's fullDocHtml has its own window.onload calling window.print(). // We also trigger focus and print as a failsafe after 500ms setTimeout(() => { printIframe.contentWindow.focus(); printIframe.contentWindow.print(); }, 500); } } // Thermal Receipt Printer for Individual Backdated Historical Order function printSingleThermalReceipt(storeName, orderId, tableNum, orderTime, payMethod, itemsJson, totalAmt, vatAmt, netAmt, address) { try { if (typeof storeName === 'string' && storeName.includes('%')) storeName = decodeURIComponent(storeName); } catch(e) {} try { if (typeof tableNum === 'string' && tableNum.includes('%')) tableNum = decodeURIComponent(tableNum); } catch(e) {} try { if (typeof orderTime === 'string' && orderTime.includes('%')) orderTime = decodeURIComponent(orderTime); } catch(e) {} try { if (typeof payMethod === 'string' && payMethod.includes('%')) payMethod = decodeURIComponent(payMethod); } catch(e) {} try { if (typeof address === 'string' && address.includes('%')) address = decodeURIComponent(address); } catch(e) {} let items = []; try { const rawJson = (typeof itemsJson === 'string' && itemsJson.includes('%')) ? decodeURIComponent(itemsJson) : itemsJson; items = typeof rawJson === 'string' ? JSON.parse(rawJson) : rawJson; } catch(e) { items = []; } // 1. Failsafe Printing using Hidden iframe (bypasses popup blockers) let printIframe = document.getElementById('receipt-print-iframe'); if (!printIframe) { printIframe = document.createElement('iframe'); printIframe.id = 'receipt-print-iframe'; printIframe.style.position = 'fixed'; printIframe.style.right = '0'; printIframe.style.bottom = '0'; printIframe.style.width = '0'; printIframe.style.height = '0'; printIframe.style.border = 'none'; document.body.appendChild(printIframe); } const itemsRows = items.map(it => { const qty = parseInt(it.quantity || it.qty || 1); const price = parseFloat(it.price || 0); const lineSum = qty * price; const note = it.note ? `<div style="font-size:10px; color:#555;">คำขอ: ${it.note}</div>` : ''; const spice = it.spice_level ? `<span style="font-size:10px; color:#e65100;">[${it.spice_level}]</span>` : ''; return ` <tr> <td style="padding:4px 0; vertical-align:top;"> <div><strong>${it.menu_name || 'รายการอาหาร'}</strong> ${spice}</div> ${note} </td> <td style="text-align:center; padding:4px 0; vertical-align:top;">x${qty}</td> <td style="text-align:right; padding:4px 0; vertical-align:top;">฿${price.toFixed(2)}</td> <td style="text-align:right; padding:4px 0; vertical-align:top; font-weight:bold;">฿${lineSum.toFixed(2)}</td> </tr> `; }).join(''); const dateFormatted = orderTime || new Date().toLocaleString('th-TH'); const payLabel = (payMethod === 'transfer' || payMethod === 'โอนจ่าย') ? '💳 โอนชำระ (PromptPay QR)' : '💵 ชำระเงินสด (Cash)'; const fullDocHtml = ` <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>ใบเสร็จรับเงิน #${orderId} - ${storeName}</title> <style> @import url('https://fonts.googleapis.com/css2?family=Sarabun:wght@400;600;800&display=swap'); body { font-family: 'Sarabun', sans-serif; margin: 0; padding: 15px; color: #000; background: #fff; } .paper-80mm { width: 72mm; margin: 0 auto; } .paper-58mm { width: 48mm; margin: 0 auto; font-size: 11px; } .text-center { text-align: center; } .text-right { text-align: right; } .divider { border-bottom: 1px dashed #000; margin: 10px 0; } .store-title { font-size: 18px; font-weight: 800; text-transform: uppercase; } .receipt-title { font-size: 13px; font-weight: 600; margin-top: 4px; } .meta-table { width: 100%; font-size: 12px; margin: 8px 0; } .item-table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 8px 0; } .item-table th { border-bottom: 1px solid #000; padding: 4px 0; text-align: left; } .summary-table { width: 100%; font-size: 12px; margin-top: 8px; } .summary-table td { padding: 3px 0; } .total-row { font-size: 15px; font-weight: 800; border-top: 1px solid #000; border-bottom: 2px double #000; padding: 6px 0 !important; } .print-toolbar { background: #1e293b; color: #fff; padding: 10px; border-radius: 8px; margin-bottom: 15px; display: flex; justify-content: space-between; align-items: center; } .print-toolbar button, .print-toolbar select { padding: 6px 12px; border-radius: 6px; border: none; font-weight: bold; cursor: pointer; font-size: 12px; } .btn-print { background: #ff5722; color: #fff; } @media print { .no-print { display: none !important; } body { padding: 0 !important; } } </style> </head> <body class="paper-80mm" id="single-receipt-body"> <div class="print-toolbar no-print"> <select onchange="document.getElementById('single-receipt-body').className = this.value"> <option value="paper-80mm" selected>80mm Thermal</option> <option value="paper-58mm">58mm Thermal</option> <option value="">A4 / Full Page</option> </select> <button type="button" class="btn-print" onclick="window.print()">🖨️ พิมพ์ใบเสร็จ</button> </div> <div class="text-center"> <div class="store-title">${storeName}</div> <div style="font-size: 11px; color: #444;">${address || ''}</div> <div class="receipt-title">ใบเสร็จรับเงิน / Receipt (TAX INVOICE)</div> </div> <div class="divider"></div> <table class="meta-table"> <tr><td><strong>หมายเลขออเดอร์:</strong> #${orderId}</td><td class="text-right"><strong>โต๊ะ:</strong> ${tableNum}</td></tr> <tr><td colspan="2"><strong>วันที่-เวลาสั่งซื้อ:</strong> ${dateFormatted}</td></tr> <tr><td colspan="2"><strong>ช่องทางการชำระ:</strong> ${payLabel}</td></tr> </table> <div class="divider"></div> <table class="item-table"> <thead> <tr> <th>รายการ</th> <th style="text-align:center;">จำนวน</th> <th style="text-align:right;">ราคา</th> <th style="text-align:right;">รวม</th> </tr> </thead> <tbody> ${itemsRows} </tbody> </table> <div class="divider"></div> <table class="summary-table"> <tr><td>รวมเงิน (Subtotal):</td><td class="text-right">฿${parseFloat(totalAmt).toFixed(2)}</td></tr> <tr><td>ประมาณการภาษี (VAT 7%):</td><td class="text-right">฿${parseFloat(vatAmt).toFixed(2)}</td></tr> <tr class="total-row"><td>ยอดชำระสุทธิ (Net Total):</td><td class="text-right">฿${parseFloat(totalAmt).toFixed(2)}</td></tr> </table> <div class="divider"></div> <div class="text-center" style="font-size: 11px; margin-top: 15px;"> 🙏 ขอบพระคุณที่อุดหนุนโอกาสหน้าเชิญใหม่ครับ/ค่ะ<br> CMTC Smart Dining System </div> </body> </html> `; const printDoc = printIframe.contentWindow || printIframe.contentDocument; if (printDoc.document) { printDoc.document.open(); printDoc.document.write(fullDocHtml); printDoc.document.close(); setTimeout(() => { printIframe.contentWindow.focus(); printIframe.contentWindow.print(); }, 500); } } // Print Table QR Card Function function printQRCode(storeName, tableName, qrImgUrl, linkUrl) { try { if (typeof storeName === 'string' && storeName.includes('%')) storeName = decodeURIComponent(storeName); } catch(e) {} try { if (typeof tableName === 'string' && tableName.includes('%')) tableName = decodeURIComponent(tableName); } catch(e) {} try { if (typeof qrImgUrl === 'string' && qrImgUrl.includes('%')) qrImgUrl = decodeURIComponent(qrImgUrl); } catch(e) {} try { if (typeof linkUrl === 'string' && linkUrl.includes('%')) linkUrl = decodeURIComponent(linkUrl); } catch(e) {} const printWindow = window.open('', '_blank', 'width=600,height=700'); printWindow.document.write(` <!DOCTYPE html> <html> <head> <title>ป้าย QR Code - ${tableName} (${storeName})</title> <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;600;800&display=swap" rel="stylesheet"> <style> body { font-family: 'Sarabun', sans-serif; text-align: center; padding: 40px; background: #f8fafc; } .card { border: 3px solid #1e293b; border-radius: 16px; padding: 30px; background: #fff; max-width: 380px; margin: 0 auto; box-shadow: 0 10px 25px rgba(0,0,0,0.1); } .store-title { font-size: 22px; font-weight: 800; color: #1e293b; margin-bottom: 4px; } .sub-title { font-size: 14px; color: #64748b; margin-bottom: 20px; } .qr-img { width: 220px; height: 220px; border-radius: 12px; margin: 15px 0; border: 1px solid #e2e8f0; padding: 8px; } .table-badge { font-size: 32px; font-weight: 800; color: #0080FF; margin-top: 10px; text-transform: uppercase; } .instructions { font-size: 14px; color: #334155; margin-top: 15px; font-weight: 600; } .footer { font-size: 11px; color: #94a3b8; margin-top: 25px; border-top: 1px dashed #cbd5e1; padding-top: 10px; } @media print { body { background: none; padding: 0; } .card { box-shadow: none; border-width: 2px; } } </style> </head> <body> <div class="card"> <div class="store-title">${storeName}</div> <div class="sub-title">ระบบสั่งอาหารอัตโนมัติ CMTC Tech Solution</div> <img src="${qrImgUrl}" class="qr-img" alt="QR Code"> <div class="table-badge">${tableName}</div> <div class="instructions">📱 สแกน QR Code นี้เพื่อเปิดเมนูสั่งอาหาร</div> <div class="footer">Smart Dining Enterprise Platform • CMTC Solution</div> </div> <script> window.onload = () => { window.print(); }; <\/script> </body> </html> `); printWindow.document.close(); } let lastMaxOrderId = <?php echo !empty($recent_orders) ? max(array_column($recent_orders, 'id')) : 0; ?>; function playNewOrderChime() { try { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const osc = audioCtx.createOscillator(); const gain = audioCtx.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(587.33, audioCtx.currentTime); // D5 osc.frequency.setValueAtTime(880, audioCtx.currentTime + 0.12); // A5 gain.gain.setValueAtTime(0.3, audioCtx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.35); osc.connect(gain); gain.connect(audioCtx.destination); osc.start(); osc.stop(audioCtx.currentTime + 0.35); } catch(e) {} } setInterval(() => { if (document.hidden) return; fetch('store-admin.php?fetch_live_orders=1') .then(res => res.json()) .then(data => { if (data.success && data.orders) { // Filter orders with pending status const pendingOrders = data.orders.filter(o => o.status === 'pending'); const pendingCount = pendingOrders.length; const liveBadge = document.getElementById('live-orders-badge'); const kdsBadge = document.getElementById('kds-orders-badge'); if (pendingCount > 0) { if (liveBadge) { liveBadge.textContent = pendingCount; liveBadge.style.display = 'inline-flex'; } if (kdsBadge) { kdsBadge.textContent = pendingCount; kdsBadge.style.display = 'inline-flex'; } } else { if (liveBadge) liveBadge.style.display = 'none'; if (kdsBadge) kdsBadge.style.display = 'none'; } if (data.orders.length > 0) { const latestId = Math.max(...data.orders.map(o => parseInt(o.id))); if (lastMaxOrderId > 0 && latestId > lastMaxOrderId) { playNewOrderChime(); const currentActiveTab = localStorage.getItem('store-admin-active-tab') || 'store-dash'; if (currentActiveTab === 'live-orders' || currentActiveTab === 'kds-mode' || currentActiveTab === 'store-dash') { location.reload(); } } lastMaxOrderId = Math.max(lastMaxOrderId, latestId); } } }) .catch(err => {}); }, 3000); // Trigger graphical calendar popup picker for Daily Sales function triggerCalendarPicker() { const input = document.getElementById('sales_date'); if (input) { if (typeof input.showPicker === 'function') { input.showPicker(); } else { input.focus(); input.click(); } } } // Trigger graphical calendar popup picker for Order Status Chart function triggerChartCalendarPicker() { const input = document.getElementById('chart_date'); if (input) { if (typeof input.showPicker === 'function') { input.showPicker(); } else { input.focus(); input.click(); } } } // Trigger graphical calendar popup picker for Receipt History function triggerHistoryCalendarPicker() { const input = document.getElementById('history_date'); if (input) { if (typeof input.showPicker === 'function') { input.showPicker(); } else { input.focus(); input.click(); } } } // Trigger graphical calendar popup picker for Order Stat Card function triggerCardCalendarPicker() { const input = document.getElementById('stat_card_date_input'); if (input) { if (typeof input.showPicker === 'function') { input.showPicker(); } else { input.focus(); input.click(); } } } // Print Daily Z-Report / Daily Tax Summary Function (Supports Thermal & ESC/POS RAW) function printDailyZReport(storeName, salesDate, totalRev, completedBills, vat7, netRev, itemsJson) { let items = []; try { items = typeof itemsJson === 'string' ? JSON.parse(itemsJson) : itemsJson; } catch(e) { items = itemsJson || []; } let itemsRows = ''; items.forEach((it, idx) => { const amt = parseFloat(it.total_amount || 0).toFixed(2); itemsRows += ` <tr> <td style="padding: 5px 0; border-bottom: 1px dashed #ccc; font-weight:700;">${idx + 1}. ${it.menu_name || 'รายการอาหาร'} (${it.category || 'ทั่วไป'})</td> <td style="text-align: center; padding: 5px 0; border-bottom: 1px dashed #ccc; font-weight:700;">${it.total_qty}</td> <td style="text-align: right; padding: 5px 0; border-bottom: 1px dashed #ccc; font-weight:700;">฿${amt}</td> </tr> `; }); const dateFormatted = new Date(salesDate).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric' }); const printWin = window.open('', '_blank', 'width=650,height=850'); printWin.document.write(` <!DOCTYPE html> <html> <head> <title>รายงานสรุปยอดขายประจำวัน (Z-Report) - ${salesDate}</title> <link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@400;600;700;800&display=swap" rel="stylesheet"> <style> @page { size: auto; margin: 0; } body { font-family: 'Courier New', Courier, 'Sarabun', monospace, sans-serif; max-width: 580px; margin: 0 auto; padding: 20px; color: #000000; background: #ffffff; line-height: 1.4; font-size: 13px; font-weight: 700; box-sizing: border-box; -webkit-print-color-adjust: exact; } body.paper-80mm { max-width: 78mm; padding: 5mm; font-size: 11.5px; } body.paper-58mm { max-width: 54mm; padding: 3mm; font-size: 10px; } .text-center { text-align: center; } .title { font-size: 20px; font-weight: 800; text-transform: uppercase; margin-bottom: 2px; } .subtitle { font-size: 12px; color: #333; margin-bottom: 10px; font-weight: 600; } .divider { border-top: 2px dashed #000; margin: 12px 0; } .box-summary { background: #f8fafc; border: 1px solid #000; border-radius: 6px; padding: 12px; margin-bottom: 15px; } .summary-row { display: flex; justify-content: space-between; font-size: 13px; margin-bottom: 6px; } .summary-row.bold { font-weight: 800; font-size: 15px; border-top: 1px solid #000; padding-top: 6px; color: #000; } .item-table { width: 100%; border-collapse: collapse; font-size: 12px; margin: 12px 0; table-layout: fixed; } .item-table th { text-align: left; border-bottom: 2px solid #000; padding-bottom: 4px; font-size: 11px; } .sign-row { display: flex; justify-content: space-between; margin-top: 30px; text-align: center; font-size: 12px; } .sign-line { border-bottom: 1px solid #000; width: 140px; margin-bottom: 4px; height: 25px; } /* Toolbar Controls */ .print-toolbar { background: #0f172a; color: #fff; padding: 10px; border-radius: 8px; margin-bottom: 15px; display: flex; flex-wrap: wrap; gap: 8px; justify-content: space-between; align-items: center; font-family: 'Sarabun', sans-serif; } .print-toolbar button, .print-toolbar select { padding: 6px 12px; border-radius: 6px; border: none; font-weight: bold; font-size: 12px; cursor: pointer; } .btn-print { background: #ff5722; color: #fff; } .btn-escpos { background: #10b981; color: #fff; } @media print { .no-print { display: none !important; } body { padding: 0 !important; margin: 0 auto !important; } } </style> </head> <body id="zreport-body"> <div class="print-toolbar no-print"> <div style="display:flex; align-items:center; gap:6px;"> <span style="font-size:12px;">📏 รูปแบบพิมพ์:</span> <select onchange="changePaperFormat(this.value)" style="background:#1e293b; color:#fff;"> <option value="a4" selected>มาตรฐาน A4 / รายงานสรุป</option> <option value="80mm">80mm Thermal Roll</option> <option value="58mm">58mm Thermal Roll</option> </select> </div> <div style="display:flex; gap:6px;"> <button type="button" class="btn-escpos" onclick="downloadZReportESCPOS()">⚡ โหลด Z-Report ESC/POS RAW (.bin)</button> <button type="button" class="btn-print" onclick="window.print()">🖨️ พิมพ์รายงาน (Print)</button> </div> </div> <div class="text-center"> <div class="title">${storeName}</div> <div class="subtitle">รายงานสรุปปิดยอดขายประจำวัน (DAILY Z-REPORT)</div> <div class="subtitle">ประจำวันที่: <strong>${dateFormatted}</strong></div> </div> <div class="divider"></div> <div class="box-summary"> <div class="summary-row"><span>จำนวนบิลที่สำเร็จ:</span> <strong>${completedBills} บิล</strong></div> <div class="summary-row"><span>ยอดขายรวมทั้งสิ้น (Gross):</span> <strong>฿${parseFloat(totalRev).toLocaleString('th-TH', {minimumFractionDigits:2, maximumFractionDigits:2})}</strong></div> <div class="summary-row"><span>ประมาณการภาษี (VAT 7%):</span> <strong>฿${parseFloat(vat7).toLocaleString('th-TH', {minimumFractionDigits:2, maximumFractionDigits:2})}</strong></div> <div class="summary-row bold"><span>รายได้สุทธิหลังภาษี (Net Sales):</span> <span>฿${parseFloat(netRev).toLocaleString('th-TH', {minimumFractionDigits:2, maximumFractionDigits:2})}</span></div> </div> <div style="font-weight: 800; font-size: 13px; margin-bottom: 6px;">📋 รายการสินค้าและยอดขายรายเมนู:</div> <table class="item-table"> <thead> <tr> <th style="width: 55%;">รายการเมนู</th> <th style="width: 20%; text-align: center;">จำนวน</th> <th style="width: 25%; text-align: right;">จำนวนเงิน</th> </tr> </thead> <tbody> ${itemsRows} </tbody> </table> <div class="divider"></div> <div class="sign-row"> <div> <div class="sign-line"></div> <div>ลงชื่อ (ผู้ปิดยอด)</div> </div> <div> <div class="sign-line"></div> <div>ลงชื่อ (เจ้าของร้าน)</div> </div> </div> <div class="text-center" style="font-size: 11px; color: #555; margin-top: 25px;"> พิมพ์เมื่อ: ${new Date().toLocaleString('th-TH')} • CMTC Smart Dining Enterprise Platform </div> <script> function changePaperFormat(fmt) { const body = document.getElementById('zreport-body'); if (fmt === '58mm') body.className = 'paper-58mm'; else if (fmt === '80mm') body.className = 'paper-80mm'; else body.className = ''; } function downloadZReportESCPOS() { const store = ${JSON.stringify(storeName)}; const dateStr = ${JSON.stringify(salesDate)}; const total = ${JSON.stringify(totalRev)}; const bills = ${JSON.stringify(completedBills)}; const vat = ${JSON.stringify(vat7)}; const net = ${JSON.stringify(netRev)}; const items = ${JSON.stringify(items)}; let cmd = []; cmd.push(0x1B, 0x40); // Init cmd.push(0x1B, 0x61, 0x01); // Center cmd.push(0x1D, 0x21, 0x11); // Double size const encoder = new TextEncoder(); const appendText = (str) => { const bytes = encoder.encode(str + '\\n'); for (let b of bytes) cmd.push(b); }; appendText(store); cmd.push(0x1D, 0x21, 0x00); // Normal appendText("DAILY Z-REPORT SUMMARY"); appendText("DATE: " + dateStr); appendText("------------------------------------------------"); cmd.push(0x1B, 0x61, 0x00); // Left appendText("COMPLETED BILLS: " + bills); appendText("GROSS REVENUE: B" + parseFloat(total).toFixed(2)); appendText("VAT 7%: B" + parseFloat(vat).toFixed(2)); appendText("NET SALES: B" + parseFloat(net).toFixed(2)); appendText("------------------------------------------------"); items.forEach(it => { const qty = parseInt(it.total_qty || 1); const lineTot = parseFloat(it.total_amount || 0).toFixed(2); const name = it.menu_name || 'Item'; appendText(qty + "x " + name + " B" + lineTot); }); appendText("------------------------------------------------"); cmd.push(0x1B, 0x61, 0x01); // Center appendText("END OF Z-REPORT"); appendText("\\n\\n\\n"); cmd.push(0x1D, 0x56, 0x41, 0x00); // Paper Cut const blob = new Blob([new Uint8Array(cmd)], { type: 'application/octet-stream' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'zreport_escpos_' + dateStr + '.bin'; a.click(); } window.onload = () => { setTimeout(() => { window.print(); }, 250); }; <\/script> </body> </html> `); printWin.document.close(); } function initDashboard() { // --- Tab Navigation --- const urlParams = new URLSearchParams(window.location.search); const tabParam = urlParams.get('tab'); const activeTab = tabParam || localStorage.getItem('store-admin-active-tab') || 'store-dash'; showTab(activeTab); // ====================================================== // Chart.js Initialization (runs after DOM is fully ready) // ====================================================== // Custom plugin for center total text inside Order Status Doughnut const statusCenterTextPlugin = { id: 'statusCenterText', beforeDraw(chart) { if (chart.canvas.id !== 'orderStatusChart') return; const { ctx, chartArea } = chart; if (!chartArea) return; ctx.save(); const total = chart.data.datasets[0].data.reduce((a, b) => a + b, 0); const centerX = (chartArea.left + chartArea.right) / 2; const centerY = (chartArea.top + chartArea.bottom) / 2; ctx.font = 'bold 20px Sarabun, "Prompt", sans-serif'; ctx.fillStyle = '#00E5FF'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; // Only show total if real data exists const statusDataRaw = <?php echo json_encode($status_data, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>; if (statusDataRaw && statusDataRaw.length > 0) { ctx.fillText(total.toLocaleString() + ' บิล', centerX, centerY - 10); ctx.font = '12px Sarabun, "Prompt", sans-serif'; ctx.fillStyle = '#94a3b8'; ctx.fillText('ออเดอร์ทั้งหมด', centerX, centerY + 12); } ctx.restore(); } }; // Dynamic color mapping dictionary by status key const statusColorMap = { 'unpaid': '#A855F7', // 💜 Purple 'pending': '#A855F7', // 💜 Purple 'preparing': '#00E5FF', // 🍳 Cyan 'served': '#3B82F6', // 🛎️ Blue 'ready': '#3B82F6', // 🛎️ Blue 'completed': '#28C76F', // ✅ Emerald Green 'cancelled': '#EA5455' // ❌ Red }; const statusLabelMap = { 'unpaid': 'ออเดอร์ใหม่ (รอรับรายการ)', 'pending': 'ออเดอร์ใหม่ (รอรับรายการ)', 'preparing': 'กำลังเตรียมปรุง', 'served': 'พร้อมเสิร์ฟ/เสิร์ฟแล้ว', 'ready': 'พร้อมเสิร์ฟ/เสิร์ฟแล้ว', 'completed': 'เช็กบิลแล้ว (เสร็จสิ้น)', 'cancelled': 'ยกเลิกออเดอร์' }; // --- Order Status Distribution Doughnut Chart --- const statusDataRaw = <?php echo json_encode($status_data, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>; const statusCanvas = document.getElementById('orderStatusChart'); if (statusCanvas) { let labels = []; let counts = []; let colors = []; let hasRealData = false; if (statusDataRaw && statusDataRaw.length > 0) { hasRealData = true; labels = statusDataRaw.map(item => statusLabelMap[item.status] || item.label || item.status); counts = statusDataRaw.map(item => parseInt(item.count)); colors = statusDataRaw.map(item => statusColorMap[item.status] || '#A855F7'); } else { labels = ['ไม่มีข้อมูลออเดอร์ในวันที่เลือก']; counts = [1]; colors = ['#334155']; } const isChartMobile = window.innerWidth <= 992; const chartLegendPos = isChartMobile ? 'bottom' : 'right'; if (window.orderStatusChartInstance) { window.orderStatusChartInstance.destroy(); } const ctxStatus = statusCanvas.getContext('2d'); window.orderStatusChartInstance = new Chart(ctxStatus, { type: 'doughnut', data: { labels: labels, datasets: [{ data: counts, backgroundColor: colors, borderColor: '#1e293b', borderWidth: 3, hoverOffset: 6 }] }, plugins: [statusCenterTextPlugin], options: { responsive: true, maintainAspectRatio: false, cutout: isChartMobile ? '60%' : '68%', plugins: { legend: { position: chartLegendPos, labels: { color: '#cbd5e1', font: { family: 'Sarabun', size: isChartMobile ? 11 : 13, weight: '600' }, padding: isChartMobile ? 10 : 16, usePointStyle: true, pointStyle: 'circle' } }, tooltip: { callbacks: { label: function(context) { if (hasRealData) { const total = context.dataset.data.reduce((a, b) => a + b, 0); const value = context.parsed; const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0; return `${context.label}: ${value} บิล (${percentage}%)`; } else { return 'ไม่มีออเดอร์ในวันที่เลือก'; } } } } } } }); } // --- Daily Sales Doughnut Chart (by Menu) --- const dailyItemsData = <?php echo json_encode($daily_items ?? [], JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>; const dailySalesCanvas = document.getElementById('dailySalesDonutChart'); if (dailyItemsData && dailyItemsData.length > 0 && dailySalesCanvas) { const menuLabels = dailyItemsData.map(item => item.menu_name || 'รายการอาหาร'); const menuAmounts = dailyItemsData.map(item => parseFloat(item.total_amount || 0)); const isChartMobile = window.innerWidth <= 992; if (window.dailySalesDonutChartInstance) window.dailySalesDonutChartInstance.destroy(); const ctxDailyMenu = dailySalesCanvas.getContext('2d'); window.dailySalesDonutChartInstance = new Chart(ctxDailyMenu, { type: 'doughnut', data: { labels: menuLabels, datasets: [{ data: menuAmounts, backgroundColor: [ '#00E5FF', '#28C76F', '#FF9F43', '#A855F7', '#EC4899', '#3B82F6', '#F59E0B', '#10B981', '#6366F1', '#8B5CF6' ], borderColor: '#1e293b', borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: isChartMobile ? 'bottom' : 'right', labels: { color: '#94a3b8', font: { family: 'Sarabun', size: isChartMobile ? 11 : 12 } } }, tooltip: { callbacks: { label: function(context) { let label = context.label || ''; if (label) label += ': '; if (context.parsed !== null) { label += '฿' + context.parsed.toLocaleString('th-TH', {minimumFractionDigits: 2}); } return label; } } } } } }); } // --- Daily Category Sales Doughnut Chart --- const dailyCatData = <?php echo json_encode($daily_categories ?? [], JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>; const dailyCatCanvas = document.getElementById('dailyCategoryDonutChart'); if (dailyCatData && Object.keys(dailyCatData).length > 0 && dailyCatCanvas) { const catLabels = Object.keys(dailyCatData); const catAmounts = Object.values(dailyCatData); const isChartMobile = window.innerWidth <= 992; if (window.dailyCategoryDonutChartInstance) window.dailyCategoryDonutChartInstance.destroy(); const ctxDailyCat = dailyCatCanvas.getContext('2d'); window.dailyCategoryDonutChartInstance = new Chart(ctxDailyCat, { type: 'doughnut', data: { labels: catLabels, datasets: [{ data: catAmounts, backgroundColor: [ '#3B82F6', '#10B981', '#F59E0B', '#EC4899', '#8B5CF6', '#00E5FF', '#FF9F43', '#28C76F', '#A855F7', '#6366F1' ], borderColor: '#1e293b', borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: isChartMobile ? 'bottom' : 'right', labels: { color: '#94a3b8', font: { family: 'Sarabun', size: isChartMobile ? 11 : 12 } } }, tooltip: { callbacks: { label: function(context) { let label = context.label || ''; if (label) label += ': '; if (context.parsed !== null) { label += '฿' + context.parsed.toLocaleString('th-TH', {minimumFractionDigits: 2}); } return label; } } } } } }); } // Auto update chart legend position on window resize window.addEventListener('resize', function() { const isMobileNow = window.innerWidth <= 992; const posNow = isMobileNow ? 'bottom' : 'right'; [window.orderStatusChartInstance, window.dailySalesDonutChartInstance, window.dailyCategoryDonutChartInstance].forEach(chart => { if (chart && chart.options && chart.options.plugins && chart.options.plugins.legend) { if (chart.options.plugins.legend.position !== posNow) { chart.options.plugins.legend.position = posNow; chart.update(); } } }); }); // end DOMContentLoaded (duplicate removed) // Native Web Audio API Chime Sound Generator for Kitchen KDS Mode function playKdsAlertSound() { try { const AudioCtx = window.AudioContext || window.webkitAudioContext; if (!AudioCtx) return; const ctx = new AudioCtx(); const osc1 = ctx.createOscillator(); const gain1 = ctx.createGain(); osc1.type = 'sine'; osc1.frequency.setValueAtTime(587.33, ctx.currentTime); 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(880, ctx.currentTime); gain2.gain.setValueAtTime(0.4, ctx.currentTime); gain2.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4); osc2.connect(gain2); gain2.connect(ctx.destination); osc2.start(); osc2.stop(ctx.currentTime + 0.4); }, 150); } catch(e) {} } // Edit Menu Item Modal Helpers function openEditMenuModal(id, name, price, category, imageUrl, isAvailable, spiceOptions, unit) { try { if (typeof name === 'string' && name.includes('%')) name = decodeURIComponent(name); } catch(e) {} try { if (typeof category === 'string' && category.includes('%')) category = decodeURIComponent(category); } catch(e) {} try { if (typeof imageUrl === 'string' && imageUrl.includes('%')) imageUrl = decodeURIComponent(imageUrl); } catch(e) {} try { if (typeof spiceOptions === 'string' && spiceOptions.includes('%')) spiceOptions = decodeURIComponent(spiceOptions); } catch(e) {} try { if (typeof unit === 'string' && unit.includes('%')) unit = decodeURIComponent(unit); } catch(e) {} const elId = document.getElementById('edit_menu_id'); const elName = document.getElementById('edit_menu_name'); const elPrice = document.getElementById('edit_menu_price'); const elCat = document.getElementById('edit_menu_category'); const elUnit = document.getElementById('edit_menu_unit'); const elSpice = document.getElementById('edit_menu_spice_options'); const elUrl = document.getElementById('edit_menu_image_url'); const elAvail = document.getElementById('edit_menu_is_available'); const elPreview = document.getElementById('edit_menu_img_preview'); if (elId) elId.value = id; if (elName) elName.value = name; if (elPrice) elPrice.value = price; if (elCat) elCat.value = category; if (elUnit) elUnit.value = unit || 'จาน'; if (elSpice) elSpice.value = spiceOptions || 'เผ็ดปกติ,ไม่เผ็ด,เผ็ดน้อย,เผ็ดมาก'; if (elUrl) elUrl.value = imageUrl || ''; if (elAvail) elAvail.checked = (isAvailable == 1); if (elPreview) { elPreview.src = imageUrl || 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=120&auto=format&fit=crop&q=60'; } const modal = document.getElementById('editMenuModal'); if (modal) modal.style.display = 'flex'; } function closeEditMenuModal() { const modal = document.getElementById('editMenuModal'); if (modal) modal.style.display = 'none'; } // NOTE: Edit Menu click handler moved to standalone script block below the modal HTML (end of page) // to ensure the modal DOM elements exist before the listener is registered. // Live Image Preview Bindings function initLiveImagePreviews() { function bindPreview(fileId, urlId, imgId, statusId, containerId) { const fileInput = document.getElementById(fileId); const urlInput = document.getElementById(urlId); const imgEl = document.getElementById(imgId); const statusEl = document.getElementById(statusId); const containerEl = document.getElementById(containerId); if (fileInput) { fileInput.addEventListener('change', function(e) { const file = e.target.files[0]; if (file && imgEl) { const objectUrl = URL.createObjectURL(file); imgEl.src = objectUrl; imgEl.style.display = 'inline-block'; if (containerEl) containerEl.style.display = 'flex'; if (statusEl) { statusEl.innerHTML = 'แสดงตัวอย่างรูปภาพใหม่ที่เลือก (กดบันทึกเพื่ออัปเดต)'; statusEl.style.color = '#00E5FF'; } } }); } if (urlInput) { urlInput.addEventListener('input', function(e) { const val = e.target.value.trim(); if (val && imgEl) { imgEl.src = val; imgEl.style.display = 'inline-block'; if (containerEl) containerEl.style.display = 'flex'; if (statusEl) { statusEl.innerHTML = 'แสดงตัวอย่างจาก URL ที่ระบุ (กดบันทึกเพื่ออัปเดต)'; statusEl.style.color = '#00E5FF'; } } }); } } bindPreview('logo_file_input', 'custom_logo_url', 'logo_preview_img', 'logo_preview_status', 'logo_preview_container'); bindPreview('banner_file_input', 'store_banner_url', 'banner_preview_img', 'banner_preview_status', 'banner_preview_container'); bindPreview('menu_image_file', 'menu_image_url', 'add_menu_img_preview', 'add_menu_preview_status', 'add_menu_preview_container'); bindPreview('edit_menu_image_file', 'edit_menu_image_url', 'edit_menu_img_preview', 'edit_image_preview_title', 'edit_image_preview_box'); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initLiveImagePreviews); } else { initLiveImagePreviews(); } function toggleMobileSidebar() { const sb = document.querySelector('.sidebar'); if (sb) sb.classList.toggle('mobile-show'); } // Chart.js render for Hourly Sales Revenue & Order Volume Trend (Combination Bar + Line Chart with Dual Y-Axes) const hourlyLabels = <?php echo json_encode($hourly_chart_labels, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>; const hourlyRevenues = <?php echo json_encode($hourly_chart_revenues, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>; const hourlyCounts = <?php echo json_encode($hourly_chart_counts, JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>; if (document.getElementById('hourlySalesTrendChart')) { const ctxHourly = document.getElementById('hourlySalesTrendChart').getContext('2d'); window.hourlySalesTrendChartInstance = new Chart(ctxHourly, { type: 'bar', data: { labels: hourlyLabels, datasets: [ { type: 'bar', label: 'ยอดขายรวม (บาท ฿)', data: hourlyRevenues, backgroundColor: 'rgba(0, 229, 255, 0.45)', borderColor: '#00E5FF', borderWidth: 2, borderRadius: 6, yAxisID: 'y' }, { type: 'line', label: 'จำนวนออเดอร์ (รายการ)', data: hourlyCounts, borderColor: '#FF9F43', backgroundColor: '#FF9F43', borderWidth: 3, pointRadius: 4, pointHoverRadius: 6, tension: 0.35, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { position: 'top', labels: { color: '#cbd5e1', font: { family: 'Sarabun', size: 13, weight: 'bold' }, usePointStyle: true } }, tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) label += ': '; if (context.datasetIndex === 0) { label += '฿' + context.parsed.y.toLocaleString('th-TH', {minimumFractionDigits: 2}); } else { label += context.parsed.y + ' รายการ'; } return label; } } } }, scales: { x: { title: { display: true, text: 'ช่วงเวลาสั่งซื้อ (ชั่วโมง / เวลา)', color: '#94a3b8', font: { family: 'Sarabun', size: 12, weight: 'bold' } }, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#cbd5e1', font: { family: 'Sarabun', size: 11 } } }, y: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'ยอดขายรวม (บาท ฿)', color: '#00E5FF', font: { family: 'Sarabun', size: 12, weight: 'bold' } }, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { color: '#00E5FF', font: { family: 'Sarabun', size: 11 }, callback: function(value) { return '฿' + value.toLocaleString('th-TH'); } } }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'จำนวนออเดอร์ (รายการ)', color: '#FF9F43', font: { family: 'Sarabun', size: 12, weight: 'bold' } }, grid: { drawOnChartArea: false }, ticks: { color: '#FF9F43', font: { family: 'Sarabun', size: 11 }, callback: function(value) { return value + ' รายการ'; } } } } } }); } } </script> <?php endif; ?> <!-- Modal for Edit Menu Item --> <div id="editMenuModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10, 25, 47, 0.85); backdrop-filter: blur(6px); z-index: 9999; justify-content: center; align-items: center; padding: 15px;"> <div style="background: #1E293B; border: 1px solid var(--accent); border-radius: 16px; width: 100%; max-width: 520px; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 20px 50px rgba(0,0,0,0.6); animation: fadeIn 0.25s ease-out;"> <div style="display: flex; justify-content: space-between; align-items: center; padding: 16px 20px; border-bottom: 1px solid rgba(255,255,255,0.1); background: rgba(15, 23, 42, 0.9); flex-shrink: 0;"> <h3 style="margin: 0; color: var(--accent); font-weight: 800; font-size: 17px;">แก้ไขรายละเอียดและรูปภาพเมนูอาหาร</h3> <button type="button" onclick="closeEditMenuModal()" style="background: rgba(255,255,255,0.08); border: none; color: #fff; width: 32px; height: 32px; border-radius: 50%; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center;">×</button> </div> <form method="POST" enctype="multipart/form-data" style="display: flex; flex-direction: column; overflow: hidden; flex: 1; margin: 0;"> <input type="hidden" name="edit_menu_item" value="1"> <input type="hidden" name="menu_id" id="edit_menu_id" value=""> <!-- Scrollable Body --> <div style="flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 14px;"> <div class="form-group" style="margin-bottom: 0;"> <label class="form-label" for="edit_menu_name" style="font-weight: bold; color: #fff;">ชื่อรายการอาหาร *</label> <input type="text" name="menu_name" id="edit_menu_name" class="form-control" required style="padding: 10px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 8px;"> </div> <div class="form-group" style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 10px; margin-bottom: 0;"> <div> <label class="form-label" for="edit_menu_price" style="font-weight: bold; color: #fff;">ราคา (บาท) *</label> <input type="number" step="0.01" name="menu_price" id="edit_menu_price" class="form-control" required style="padding: 10px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 8px;"> </div> <div> <label class="form-label" for="edit_menu_category" style="font-weight: bold; color: #fff;">หมวดหมู่รายการ *</label> <input type="text" name="menu_category" id="edit_menu_category" class="form-control" required style="padding: 10px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 8px;"> </div> <div> <label class="form-label" for="edit_menu_unit" style="font-weight: bold; color: #fff;">หน่วยนับ *</label> <input type="text" name="menu_unit" id="edit_menu_unit" class="form-control" required placeholder="เช่น จาน, แก้ว, ชิ้น, ถ้วย, ขวด" style="padding: 10px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 8px;"> </div> </div> <div class="form-group" style="margin-bottom: 0;"> <label class="form-label" for="edit_menu_spice_options" style="font-weight: bold; color: #fff;">ตัวเลือกรสชาติเพิ่มเติม</label> <input type="text" name="menu_spice_options" id="edit_menu_spice_options" class="form-control" placeholder="เช่น หวานปกติ, หวานน้อย, ไม่หวาน หรือ เผ็ดปกติ, เผ็ดน้อย" style="padding: 10px; background: #0f172a; color: #fff; border: 1px solid var(--accent); border-radius: 8px;"> <small style="color: var(--text-secondary); display: block; margin-top: 4px;">คั่นแต่ละตัวเลือกด้วยเครื่องหมายจุลภาค (,) เช่น หวานปกติ, หวานน้อย, ไม่หวาน</small> </div> <!-- Current Image Preview --> <div id="edit_image_preview_box" style="margin-bottom: 0; text-align: center; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.1);"> <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 6px;">รูปภาพปัจจุบัน:</div> <img id="edit_menu_img_preview" src="" alt="Current Food Image" style="max-height: 85px; border-radius: 8px; object-fit: cover;"> </div> <div class="form-group" style="border: 1px dashed var(--accent); padding: 12px; border-radius: 8px; background: rgba(0,229,255,0.03); margin-bottom: 0;"> <label class="form-label" for="edit_menu_image_file" style="color: var(--accent); font-weight: bold; display: flex; align-items: center; gap: 6px;"> อัปโหลดรูปภาพอาหารใหม่ (จากคอมพิวเตอร์/มือถือ) </label> <input type="file" name="menu_image_file" id="edit_menu_image_file" class="form-control" accept="image/*" style="padding: 6px;"> <small style="color: var(--text-secondary); display: block; margin-top: 4px;">หากต้องการเปลี่ยนรูป ให้แนบไฟล์ใหม่ที่นี่</small> </div> <div class="form-group" style="margin-bottom: 0;"> <label class="form-label" for="edit_menu_image_url">หรือ ระบุ URL ลิงก์รูปภาพอาหารใหม่</label> <input type="text" name="menu_image_url" id="edit_menu_image_url" class="form-control" placeholder="https://domain.com/new-food.jpg หรือ uploads/menus/food.jpg"> </div> <div class="form-group" style="display:flex; align-items:center; gap: 10px; margin-bottom: 0;"> <input type="checkbox" name="menu_is_available" id="edit_menu_is_available" value="1"> <label for="edit_menu_is_available" style="font-size: 14px; user-select:none; cursor:pointer; color: #fff; font-weight: bold;">เปิดให้สั่งเมนูนี้ (Is Available)</label> </div> </div> <!-- Pinned Footer Buttons --> <div style="padding: 14px 20px; background: rgba(15, 23, 42, 0.95); border-top: 1px solid rgba(255,255,255,0.1); display: flex; gap: 10px; justify-content: flex-end; flex-shrink: 0;"> <button type="button" onclick="closeEditMenuModal()" class="btn-sm" style="background: #334155; color: #fff; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-weight: bold;">ยกเลิก</button> <button type="submit" class="btn-sm" style="background: var(--accent); color: #0f172a; border: none; padding: 10px 24px; border-radius: 8px; cursor: pointer; font-weight: 800; font-size: 14px;">บันทึกการแก้ไขเมนู</button> </div> </form> </div> </div> <!-- ============================================================ EDIT MENU MODAL - Standalone JS (runs AFTER modal DOM exists) This is the single source of truth for Edit Menu functionality. ============================================================ --> <script> (function() { 'use strict'; // ---- Populate & open the Edit Menu modal ---- function openEditMenuModal(id, name, price, category, imageUrl, isAvailable, spiceOptions, unit) { console.log('[EditMenu] Opening modal for Menu ID:', id, '| Name:', name); // Safe decode helper function safeDecode(s) { if (typeof s !== 'string' || !s.includes('%')) return s || ''; try { return decodeURIComponent(s); } catch(e) { return s; } } name = safeDecode(name); category = safeDecode(category); imageUrl = safeDecode(imageUrl); spiceOptions = safeDecode(spiceOptions); unit = safeDecode(unit); var elId = document.getElementById('edit_menu_id'); var elName = document.getElementById('edit_menu_name'); var elPrice = document.getElementById('edit_menu_price'); var elCat = document.getElementById('edit_menu_category'); var elUnit = document.getElementById('edit_menu_unit'); var elSpice = document.getElementById('edit_menu_spice_options'); var elUrl = document.getElementById('edit_menu_image_url'); var elAvail = document.getElementById('edit_menu_is_available'); var elPrev = document.getElementById('edit_menu_img_preview'); // Debug: verify elements are found console.log('[EditMenu] DOM elements found:', { id: !!elId, name: !!elName, price: !!elPrice, cat: !!elCat, unit: !!elUnit, spice: !!elSpice, url: !!elUrl, avail: !!elAvail }); if (elId) elId.value = id; if (elName) elName.value = name; if (elPrice) elPrice.value = price; if (elCat) elCat.value = category; if (elUnit) elUnit.value = unit || 'จาน'; if (elSpice) elSpice.value = spiceOptions || 'เผ็ดปกติ,ไม่เผ็ด,เผ็ดน้อย,เผ็ดมาก'; if (elUrl) elUrl.value = imageUrl || ''; if (elAvail) elAvail.checked = (String(isAvailable) === '1'); if (elPrev) elPrev.src = imageUrl || 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?w=120&auto=format&fit=crop&q=60'; var modal = document.getElementById('editMenuModal'); if (modal) { modal.style.display = 'flex'; console.log('[EditMenu] Modal displayed successfully.'); } else { console.error('[EditMenu] CRITICAL: #editMenuModal element NOT FOUND in DOM!'); // SweetAlert2 fallback if modal DOM is missing if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'error', title: 'เกิดข้อผิดพลาด', text: 'ไม่พบกล่อง Modal แก้ไขเมนู กรุณารีเฟรชหน้าเว็บแล้วลองใหม่', confirmButtonColor: '#00E5FF' }); } } } // ---- Close the modal ---- function closeEditMenuModal() { var modal = document.getElementById('editMenuModal'); if (modal) modal.style.display = 'none'; } // ---- Expose to global scope so inline onclick calls still work ---- window.openEditMenuModal = openEditMenuModal; window.closeEditMenuModal = closeEditMenuModal; // ---- Single delegated click listener for all .btn-edit-menu-trigger buttons ---- document.addEventListener('click', function(e) { var btn = e.target.closest('.btn-edit-menu-trigger'); if (!btn) return; e.preventDefault(); e.stopPropagation(); var id = btn.getAttribute('data-id'); var name = btn.getAttribute('data-name'); var price = btn.getAttribute('data-price'); var category = btn.getAttribute('data-category'); var imageUrl = btn.getAttribute('data-image'); var isAvailable = btn.getAttribute('data-available'); var spiceOptions = btn.getAttribute('data-spice'); var unit = btn.getAttribute('data-unit'); console.log('[EditMenu] Edit button clicked. Raw data-id =', id); if (!id || id === '0') { console.error('[EditMenu] data-id is missing or zero on button:', btn); return; } openEditMenuModal(id, name, price, category, imageUrl, isAvailable, spiceOptions, unit); }); // ---- Live file/URL preview inside edit modal ---- document.addEventListener('change', function(e) { if (e.target && e.target.id === 'edit_menu_image_file') { var file = e.target.files[0]; if (file) { var prev = document.getElementById('edit_menu_img_preview'); if (prev) prev.src = URL.createObjectURL(file); } } }); document.addEventListener('input', function(e) { if (e.target && e.target.id === 'edit_menu_image_url') { var val = e.target.value.trim(); var prev = document.getElementById('edit_menu_img_preview'); if (prev && val) prev.src = val; } }); console.log('[EditMenu] Standalone edit-menu script initialised OK.'); })(); // Category and Menu Reordering & Edit Prompt Helpers function editCategoryPrompt(id, currentName) { if (typeof Swal !== 'undefined') { Swal.fire({ title: '✏️ แก้ไขชื่อหมวดหมู่', input: 'text', inputValue: currentName, showCancelButton: true, confirmButtonText: 'บันทึก', cancelButtonText: 'ยกเลิก', confirmButtonColor: '#00E5FF', cancelButtonColor: '#475569', inputValidator: (value) => { if (!value || !value.trim()) { return 'กรุณากรอกชื่อหมวดหมู่!'; } } }).then((result) => { if (result.isConfirmed) { const form = document.createElement('form'); form.method = 'POST'; form.innerHTML = ` <input type="hidden" name="edit_category" value="1"> <input type="hidden" name="category_id" value="${id}"> <input type="hidden" name="category_name" value="${result.value.trim()}"> `; document.body.appendChild(form); form.submit(); } }); } else { const newName = prompt('แก้ไขชื่อหมวดหมู่:', currentName); if (newName && newName.trim()) { const form = document.createElement('form'); form.method = 'POST'; form.innerHTML = ` <input type="hidden" name="edit_category" value="1"> <input type="hidden" name="category_id" value="${id}"> <input type="hidden" name="category_name" value="${newName.trim()}"> `; document.body.appendChild(form); form.submit(); } } } function saveCategoryOrder() { const inputs = document.querySelectorAll('.cat-sort-input'); const items = []; inputs.forEach(inp => { items.push({ id: parseInt(inp.getAttribute('data-id')), sort_order: parseInt(inp.value) || 0 }); }); if (items.length === 0) return; fetch('api/menu/reorder/index.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'category', items: items }) }) .then(res => res.json()) .then(data => { if (data.success) { if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'success', title: 'สำเร็จ', text: data.message, timer: 1500, showConfirmButton: false }) .then(() => location.reload()); } else { alert(data.message); location.reload(); } } else { if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'error', title: 'ผิดพลาด', text: data.message }); } else { alert(data.message); } } }) .catch(err => { alert('เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์'); }); } function saveMenuOrder() { const inputs = document.querySelectorAll('.menu-sort-input'); const items = []; inputs.forEach(inp => { items.push({ id: parseInt(inp.getAttribute('data-id')), sort_order: parseInt(inp.value) || 0 }); }); if (items.length === 0) return; fetch('api/menu/reorder/index.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'menu', items: items }) }) .then(res => res.json()) .then(data => { if (data.success) { if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'success', title: 'สำเร็จ', text: data.message, timer: 1500, showConfirmButton: false }) .then(() => location.reload()); } else { alert(data.message); location.reload(); } } else { if (typeof Swal !== 'undefined') { Swal.fire({ icon: 'error', title: 'ผิดพลาด', text: data.message }); } else { alert(data.message); } } }) .catch(err => { alert('เกิดข้อผิดพลาดในการเชื่อมต่อเซิร์ฟเวอร์'); }); } </script> <!-- Chart.js Initializer Script --> <script> document.addEventListener('DOMContentLoaded', () => { // 1. Daily Sales Menu Revenue Share Doughnut Chart const ctxMenu = document.getElementById('dailySalesDonutChart'); if (ctxMenu && typeof Chart !== 'undefined') { const labels = <?php echo json_encode(array_column($daily_items ?? [], 'menu_name'), JSON_UNESCAPED_UNICODE) ?: '[]'; ?>; const data = <?php echo json_encode(array_map('floatval', array_column($daily_items ?? [], 'total_amount'))) ?: '[]'; ?>; if (labels.length > 0) { new Chart(ctxMenu, { type: 'doughnut', data: { labels: labels, datasets: [{ data: data, backgroundColor: ['#00E5FF', '#FF9F43', '#10B981', '#7C3AED', '#FF5722', '#3B82F6', '#EC4899', '#EAB308'], borderWidth: 2, borderColor: '#0f172a' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#cbd5e1', font: { family: 'Sarabun', size: 12 } } }, tooltip: { callbacks: { label: (ctx) => ` ฿${ctx.parsed.toLocaleString('th-TH', {minimumFractionDigits: 2})}` } } } } }); } } // 2. Daily Sales Category Revenue Share Doughnut Chart const ctxCat = document.getElementById('dailyCategoryDonutChart'); if (ctxCat && typeof Chart !== 'undefined') { <?php $cat_agg = []; foreach ($daily_items ?? [] as $di) { $cname = $di['category'] ?: 'ทั่วไป'; $cat_agg[$cname] = ($cat_agg[$cname] ?? 0) + floatval($di['total_amount']); } ?> const catLabels = <?php echo json_encode(array_keys($cat_agg), JSON_UNESCAPED_UNICODE) ?: '[]'; ?>; const catData = <?php echo json_encode(array_values($cat_agg)) ?: '[]'; ?>; if (catLabels.length > 0) { new Chart(ctxCat, { type: 'doughnut', data: { labels: catLabels, datasets: [{ data: catData, backgroundColor: ['#10B981', '#00E5FF', '#FF9F43', '#EC4899', '#7C3AED', '#3B82F6'], borderWidth: 2, borderColor: '#0f172a' }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#cbd5e1', font: { family: 'Sarabun', size: 12 } } }, tooltip: { callbacks: { label: (ctx) => ` ฿${ctx.parsed.toLocaleString('th-TH', {minimumFractionDigits: 2})}` } } } } }); } } }); </script> <!-- Footer --> <footer style="text-align: center; padding: 30px; color: var(--text-secondary); font-size: 13px; border-top: 1px solid var(--border-glass); margin-top: 50px; background: var(--dark-bg); position: relative; z-index: 10;"> CMTC Tech Solution Platform • Chiang Mai Technical College © 2026. </footer> </body> </html>
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.26 |
proxy
|
phpinfo
|
Settings