if (!defined('ABSPATH')) { exit; } // 驗證權限與 Nonce function qc_pos_verify_ajax_request() { check_ajax_referer('qc_pos_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => '無存取權限'), 403); } } // 1. 建立現場銷售單 (POS Direct Checkout) add_action('wp_ajax_qc_pos_create_pos_order', 'qc_pos_create_pos_order_handler'); function qc_pos_create_pos_order_handler() { qc_pos_verify_ajax_request(); if (!class_exists('WooCommerce')) { wp_send_json_error(array('message' => 'WooCommerce 未啟用')); } $pickup_date = sanitize_text_field($_POST['pickup_date'] ?? ''); $total_amount = intval($_POST['total_amount'] ?? 0); $payment_method_title = isset($_POST['payment_method_title']) ? sanitize_text_field($_POST['payment_method_title']) : '現場付款'; $items_raw = $_POST['items'] ?? ''; $items = json_decode(stripslashes($items_raw), true); // 現場銷售若日期缺失,預設為今日 if (empty($pickup_date)) { $pickup_date = current_time('Y-m-d'); } if (empty($items)) { wp_send_json_error(array('message' => '資料不完整:缺少品項或格式錯誤')); } try { $order = wc_create_order(); foreach ($items as $item) { $order->add_product(wc_get_product($item['product_id']), $item['quantity']); } // 現場銷售固定屬性 $order->set_address(array( 'first_name' => '現場', 'phone' => '0900000000', ), 'billing'); // 設定付款方式 $order->set_payment_method('cod'); $order->set_payment_method_title($payment_method_title); $order->calculate_totals(); // 寫入 Metadata $utc_timestamp = strtotime($pickup_date . ' UTC'); $order->update_meta_data('_orddd_lite_timestamp', (string) $utc_timestamp); $order->update_meta_data('自取日期', $pickup_date); $order->update_meta_data('_custom_order_source', 'instore_direct'); // 現場銷售狀態直接設為已完成 $order->update_status('completed'); $order->save(); wp_send_json_success(array( 'message' => '結帳成功,已建立 WooCommerce 訂單', 'order_id' => $order->get_id() )); } catch (Exception $e) { wp_send_json_error(array('message' => '寫入 WooCommerce 失敗: ' . $e->getMessage())); } } // 2. 建立 WooCommerce 預購單 add_action('wp_ajax_qc_pos_create_woo_order', 'qc_pos_create_woo_order_handler'); function qc_pos_create_woo_order_handler() { qc_pos_verify_ajax_request(); if (!class_exists('WooCommerce')) { wp_send_json_error(array('message' => 'WooCommerce 未啟用')); } $last_name = sanitize_text_field($_POST['last_name']); $first_name = sanitize_text_field($_POST['first_name']); $phone = sanitize_text_field($_POST['phone']); $pickup_date = sanitize_text_field($_POST['pickup_date']); $order_source = sanitize_text_field($_POST['order_source']); $customer_note = sanitize_textarea_field($_POST['customer_note']); $items = json_decode(stripslashes($_POST['items']), true); if (empty($first_name) || empty($phone) || empty($pickup_date) || empty($order_source) || empty($items)) { wp_send_json_error(array('message' => '必填欄位缺失')); } try { $order = wc_create_order(); foreach ($items as $item) { $order->add_product(wc_get_product($item['product_id']), $item['quantity']); } $order->set_billing_last_name($last_name); $order->set_billing_first_name($first_name); $order->set_billing_phone($phone); if (!empty($customer_note)) { $order->set_customer_note($customer_note); } // 設定付款方式:現場付款 (Cash on delivery) $order->set_payment_method('cod'); $order->set_payment_method_title('現場付款'); $order->calculate_totals(); $order->set_status('processing', 'POS 儀表板建單'); // 寫入 Metadata $utc_timestamp = strtotime($pickup_date . ' UTC'); $order->update_meta_data('_orddd_lite_timestamp', (string) $utc_timestamp); $order->update_meta_data('自取日期', $pickup_date); $order->update_meta_data('_custom_order_source', $order_source); $order->save(); wp_send_json_success(array( 'message' => '訂單建立成功', 'order_id' => $order->get_id() )); } catch (Exception $e) { wp_send_json_error(array('message' => 'WooCommerce 訂單建立失敗: ' . $e->getMessage())); } } // 3. 取得儀表板資料 (透過 WooCommerce API 查詢) add_action('wp_ajax_qc_pos_get_dashboard_data', 'qc_pos_get_dashboard_data_handler'); function qc_pos_get_dashboard_data_handler() { qc_pos_verify_ajax_request(); if (!class_exists('WooCommerce')) { wp_send_json_error(array('message' => 'WooCommerce 未啟用')); } $date = sanitize_text_field($_GET['date']); if (empty($date)) { $date = current_time('Y-m-d'); } $status = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : 'processing'; $orders_data = array(); $target_timestamp = strtotime($date . ' UTC'); $args = array( 'status' => $status, 'limit' => -1, 'meta_key' => '_orddd_lite_timestamp', 'meta_value' => (string) $target_timestamp, 'meta_compare' => '=', 'orderby' => 'ID', 'order' => 'ASC' ); $woo_orders = wc_get_orders($args); foreach ($woo_orders as $order) { $items_formatted = array(); foreach ($order->get_items() as $item_id => $item) { $items_formatted[] = array( 'name' => $item->get_name(), 'quantity' => $item->get_quantity(), 'price' => $item->get_subtotal() / max(1, $item->get_quantity()) ); } $source = $order->get_meta('_custom_order_source'); if (empty($source)) $source = 'website'; $orders_data[] = array( 'id' => $order->get_id(), 'status' => $status, 'customer_name' => trim($order->get_billing_last_name() . ' ' . $order->get_billing_first_name()), 'phone' => $order->get_billing_phone(), 'source' => $source, 'total' => (float)$order->get_total(), 'pickup_date' => $order->get_meta('自取日期') ? $order->get_meta('自取日期') : gmdate('Y-m-d', (int)$order->get_meta('_orddd_lite_timestamp')), 'items' => $items_formatted, 'customer_note' => $order->get_customer_note(), 'payment_method' => $order->get_payment_method() ); } $stats_args = array( 'status' => array('processing', 'completed'), 'limit' => -1, 'meta_key' => '_orddd_lite_timestamp', 'meta_value' => (string) $target_timestamp, 'meta_compare' => '=' ); $stats_orders = wc_get_orders($stats_args); $stats = array('order_count' => count($stats_orders), 'total_revenue' => 0); foreach ($stats_orders as $o) { $stats['total_revenue'] += (float)$o->get_total(); } wp_send_json_success(array( 'data' => $orders_data, 'stats' => $stats )); } // ========================================================= // 4. Dashboard 統計總覽 API(圓餅圖 + 趨勢折線圖資料源) // ========================================================= /** * AJAX Action: qc_pos_get_stats_overview * * 職責: * 接收前端指定的日期範圍,查詢該區間內所有有效訂單(processing + completed), * 彙整兩種統計維度後回傳: * 1. source_breakdown — 各訂單來源的營業額合計(供圓餅圖使用) * 2. daily_trend — 以「自取日期」為鍵的每日營業額(供折線圖使用) * * 由 dashboard-charts.js 的 fetchOverview() 透過 GET 請求呼叫。 * * 查詢參數: * date_from YYYY-MM-DD 查詢起始日(含) * date_to YYYY-MM-DD 查詢結束日(含) * nonce string WordPress nonce,防 CSRF 攻擊 * * 訂單查詢邏輯: * 使用 wc_get_orders() 搭配 meta_key = _orddd_lite_timestamp, * 以 BETWEEN 條件比對 UTC 午夜時間戳記。 * 例:date_from='2026-04-21' → from_ts=1745193600 * date_to ='2026-04-23' → to_ts =1745366400 * 查詢結果包含自取日期落在此區間的所有 processing + completed 訂單。 * * 訂單來源判斷規則(_custom_order_source meta): * fb_msg → 'FB' * ig_msg → 'IG' * website → '官網' * instore_preorder → '現場'(與 instore_direct 合計) * instore_direct → '現場'(與 instore_preorder 合計) * 空值(官網前台下單)→ fallback 視為 'website' → '官網' * 其他未知值 → '其他' * * 成功回傳格式: * { * "success": true, * "data": { * "source_breakdown": { "FB": 1200, "官網": 3400 }, // 已過濾 revenue=0 的來源 * "daily_trend": { "2026-04-21": 800, ... }, // 已 ksort 升冪排列 * "total_revenue": 4600, * "order_count": 15 * } * } * * 對應前端:assets/js/dashboard-charts.js → fetchOverview() */ add_action('wp_ajax_qc_pos_get_stats_overview', 'qc_pos_get_stats_overview_handler'); function qc_pos_get_stats_overview_handler() { qc_pos_verify_ajax_request(); if (!class_exists('WooCommerce')) { wp_send_json_error(array('message' => 'WooCommerce 未啟用')); } $date_from = sanitize_text_field($_GET['date_from'] ?? ''); $date_to = sanitize_text_field($_GET['date_to'] ?? ''); if (empty($date_from) || empty($date_to)) { wp_send_json_error(array('message' => '日期範圍缺失')); } // 將日期字串轉為 UTC 午夜時間戳記 // _orddd_lite_timestamp 儲存的是「自取日期當天 UTC 00:00」的 Unix 時間戳記, // 例如 2026-04-21 → 1745193600(strtotime('2026-04-21 UTC')) $from_ts = strtotime($date_from . ' UTC'); $to_ts = strtotime($date_to . ' UTC'); if ($from_ts > $to_ts) { wp_send_json_error(array('message' => '起始日期不可大於結束日期')); } // 使用 BETWEEN 查詢此時間戳記區間的所有訂單 // meta_type = NUMERIC 確保資料庫以數字型態比較,避免字串排序錯誤 $args = array( 'status' => array('processing', 'completed'), 'limit' => -1, 'meta_key' => '_orddd_lite_timestamp', 'meta_value' => array((string)$from_ts, (string)$to_ts), 'meta_compare' => 'BETWEEN', 'meta_type' => 'NUMERIC', ); $orders = wc_get_orders($args); // 來源代碼到顯示標籤的映射表 // instore_preorder 與 instore_direct 合計為同一個「現場」分類, // 因為從報表角度兩者都屬「門市實體接觸」,業主無需再細分 $source_map = array( 'fb_msg' => 'FB', 'ig_msg' => 'IG', 'website' => '官網', 'instore_preorder' => '現場', 'instore_direct' => '現場', ); // 初始化各來源的營業額累計,保持固定順序(圓餅圖色彩一致) $source_revenue = array( 'FB' => 0, 'IG' => 0, '官網' => 0, '現場' => 0, '其他' => 0, ); $daily_revenue = array(); // 'YYYY-MM-DD' => float,統計每個自取日期的營業額 foreach ($orders as $order) { $total = (float) $order->get_total(); $source_raw = $order->get_meta('_custom_order_source'); // 邊界案例:官網前台下單不會寫入 _custom_order_source meta, // empty($source_raw) 時必須 fallback 為 'website', // 否則會因為空字串在 $source_map 中找不到而落入「其他」 if (empty($source_raw)) $source_raw = 'website'; $pickup_date = $order->get_meta('自取日期'); // 邊界案例:若訂單未寫入「自取日期」字串 meta(舊格式訂單), // 改從 _orddd_lite_timestamp 的 Unix 時間戳記轉換 if (empty($pickup_date)) { $ts = (int) $order->get_meta('_orddd_lite_timestamp'); $pickup_date = $ts ? gmdate('Y-m-d', $ts) : ''; } // 累計來源營業額 $group = $source_map[$source_raw] ?? '其他'; $source_revenue[$group] += $total; // 累計日期趨勢(以自取日期為 key) if (!empty($pickup_date)) { $daily_revenue[$pickup_date] = ($daily_revenue[$pickup_date] ?? 0) + $total; } } // 依自取日期升冪排序,確保折線圖 X 軸時序正確 ksort($daily_revenue); // 過濾掉營業額為 0 的來源,避免圓餅圖出現「幽靈扇形」 $source_revenue_filtered = array_filter($source_revenue, fn($v) => $v > 0); wp_send_json_success(array( 'source_breakdown' => $source_revenue_filtered, 'daily_trend' => $daily_revenue, 'total_revenue' => array_sum($source_revenue_filtered), 'order_count' => count($orders), )); } // 5. 取得最新商品列表 add_action('wp_ajax_qc_pos_get_products', 'qc_pos_get_products_handler'); function qc_pos_get_products_handler() { qc_pos_verify_ajax_request(); if (!class_exists('WooCommerce')) { wp_send_json_error(array('message' => 'WooCommerce 未啟用')); } $products_data = array(); $args = array( 'status' => 'publish', 'limit' => -1, ); $products = wc_get_products($args); foreach ($products as $product) { $image_url = wp_get_attachment_image_url($product->get_image_id(), 'thumbnail'); if (!$image_url) { $image_url = wc_placeholder_img_src('thumbnail'); } $products_data[] = array( 'id' => $product->get_id(), 'name' => $product->get_name(), 'price' => (float) $product->get_price(), 'image' => $image_url, 'stock' => $product->managing_stock() ? $product->get_stock_quantity() : null ); } wp_send_json_success($products_data); } https://quietcuisine.com/post-sitemap.xml 2026-04-07T07:19:10+00:00 https://quietcuisine.com/page-sitemap.xml 2026-04-15T23:13:34+00:00 https://quietcuisine.com/blocks-sitemap.xml 2025-09-05T04:35:05+00:00 https://quietcuisine.com/product-sitemap.xml 2026-04-23T08:14:58+00:00 https://quietcuisine.com/category-sitemap.xml 2026-04-07T07:19:10+00:00 https://quietcuisine.com/product_cat-sitemap.xml 2026-04-23T08:14:58+00:00 https://quietcuisine.com/author-sitemap.xml 2024-09-06T06:43:17+00:00