From 61738519208f52181ad387ad5f424fa117db5b6b Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 5 Jun 2026 17:34:55 +0800 Subject: [PATCH 1/4] =?UTF-8?q?[FEAT]=20=E9=87=8D=E6=A7=8B=E5=95=86?= =?UTF-8?q?=E5=93=81=E8=88=87=E6=A9=9F=E5=8F=B0=E5=A0=B1=E8=A1=A8=E7=B5=B1?= =?UTF-8?q?=E8=A8=88=E9=82=8F=E8=BC=AF=EF=BC=8C=E5=B0=8D=E9=BD=8A=E5=AF=A6?= =?UTF-8?q?=E9=9A=9B=E5=87=BA=E8=B2=A8=E6=88=90=E5=8A=9F=E4=BB=B6=E6=95=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Admin/AnalysisController.php | 256 +++++++++++++----- tests/Feature/Admin/AnalysisReportsTest.php | 33 +++ 2 files changed, 220 insertions(+), 69 deletions(-) diff --git a/app/Http/Controllers/Admin/AnalysisController.php b/app/Http/Controllers/Admin/AnalysisController.php index 4357105..3ee00b8 100644 --- a/app/Http/Controllers/Admin/AnalysisController.php +++ b/app/Http/Controllers/Admin/AnalysisController.php @@ -100,55 +100,113 @@ class AnalysisController extends Controller ]; // 6. 執行主報表統計 SQL (按機台分組) - $mainQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') - ->join('orders as o', 'o.id', '=', 'oi.order_id') - ->leftJoin('machines as m', 'm.id', '=', 'o.machine_id') - ->leftJoin('products as p', 'p.id', '=', 'oi.product_id') + // 子查詢 A:營業額 (計算機台實收) + $salesQuery = \Illuminate\Support\Facades\DB::table('orders as o') ->select([ 'o.machine_id', - \Illuminate\Support\Facades\DB::raw('COALESCE(m.serial_no, "UNKNOWN") as serial_no'), - \Illuminate\Support\Facades\DB::raw('COALESCE(m.name, "未知機台") as name'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.quantity) as total_quantity'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) as total_sales'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.quantity * COALESCE(p.cost, 0)) as total_cost'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) - SUM(oi.quantity * COALESCE(p.cost, 0)) as total_profit') + \Illuminate\Support\Facades\DB::raw('SUM(o.pay_amount) as total_sales') ]) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->whereNull('o.deleted_at') ->whereBetween('o.created_at', [$startDate, $endDate]); - // 套用公司篩選 + // 套用篩選條件到營業額子查詢 if ($effectiveCompanyId) { - $mainQuery->where('o.company_id', $effectiveCompanyId); + $salesQuery->where('o.company_id', $effectiveCompanyId); + } + if ($allowedMachineIds !== null) { + if (empty($allowedMachineIds)) { + $salesQuery->whereRaw('1 = 0'); + } else { + $salesQuery->whereIn('o.machine_id', $allowedMachineIds); + } + } + if ($selectedMachineId && $selectedMachineId !== 'ALL') { + $salesQuery->where('o.machine_id', $selectedMachineId); + } + if (!empty($paymentTypes)) { + $salesQuery->whereIn('o.payment_type', $paymentTypes); } - // 套用機台權限限制 + // 如果篩選特定商品,營業額只計算有成功出貨的該商品金額 + if ($selectedProductId && $selectedProductId !== 'ALL') { + $salesQuery->join('order_items as oi', 'oi.order_id', '=', 'o.id') + ->leftJoin('dispense_records as dr', function($join) { + $join->on('dr.order_id', '=', 'oi.order_id') + ->on('dr.product_id', '=', 'oi.product_id') + ->where('dr.dispense_status', '=', 1); + }) + ->where('oi.product_id', $selectedProductId) + ->select([ + 'o.machine_id', + \Illuminate\Support\Facades\DB::raw('SUM(CASE WHEN dr.id IS NOT NULL THEN oi.subtotal ELSE 0 END) as total_sales') + ]); + } + $salesQuery->groupBy('o.machine_id'); + + // 子查詢 B:實際出貨件數與成本 (僅統計出貨成功且在範圍內) + $dispenseQuery = \Illuminate\Support\Facades\DB::table('dispense_records as dr') + ->join('orders as o', 'o.id', '=', 'dr.order_id') + ->leftJoin('products as p', 'p.id', '=', 'dr.product_id') + ->select([ + 'dr.machine_id', + \Illuminate\Support\Facades\DB::raw('COUNT(dr.id) as total_quantity'), + \Illuminate\Support\Facades\DB::raw('SUM(COALESCE(p.cost, 0)) as total_cost') + ]) + ->where('dr.dispense_status', 1) + ->whereNull('o.deleted_at') + ->whereBetween('o.created_at', [$startDate, $endDate]); + + // 套用篩選條件到出貨子查詢 + if ($effectiveCompanyId) { + $dispenseQuery->where('o.company_id', $effectiveCompanyId); + } + if ($allowedMachineIds !== null) { + if (empty($allowedMachineIds)) { + $dispenseQuery->whereRaw('1 = 0'); + } else { + $dispenseQuery->whereIn('o.machine_id', $allowedMachineIds); + } + } + if ($selectedMachineId && $selectedMachineId !== 'ALL') { + $dispenseQuery->where('o.machine_id', $selectedMachineId); + } + if ($selectedProductId && $selectedProductId !== 'ALL') { + $dispenseQuery->where('dr.product_id', $selectedProductId); + } + if (!empty($paymentTypes)) { + $dispenseQuery->whereIn('o.payment_type', $paymentTypes); + } + $dispenseQuery->groupBy('dr.machine_id'); + + // 聯結機台與子查詢,取得最終統計資料 + $mainQuery = \Illuminate\Support\Facades\DB::table('machines as m') + ->joinSub($salesQuery, 'sq', 'sq.machine_id', '=', 'm.id') + ->leftJoinSub($dispenseQuery, 'dq', 'dq.machine_id', '=', 'm.id') + ->select([ + 'm.id as machine_id', + \Illuminate\Support\Facades\DB::raw('COALESCE(m.serial_no, "UNKNOWN") as serial_no'), + \Illuminate\Support\Facades\DB::raw('COALESCE(m.name, "未知機台") as name'), + \Illuminate\Support\Facades\DB::raw('CAST(COALESCE(dq.total_quantity, 0) as SIGNED) as total_quantity'), + \Illuminate\Support\Facades\DB::raw('CAST(COALESCE(sq.total_sales, 0) as DECIMAL(10,2)) as total_sales'), + \Illuminate\Support\Facades\DB::raw('CAST(COALESCE(dq.total_cost, 0) as DECIMAL(10,2)) as total_cost'), + \Illuminate\Support\Facades\DB::raw('CAST((COALESCE(sq.total_sales, 0) - COALESCE(dq.total_cost, 0)) as DECIMAL(10,2)) as total_profit') + ]); + + if ($effectiveCompanyId) { + $mainQuery->where('m.company_id', $effectiveCompanyId); + } if ($allowedMachineIds !== null) { if (empty($allowedMachineIds)) { $mainQuery->whereRaw('1 = 0'); } else { - $mainQuery->whereIn('o.machine_id', $allowedMachineIds); + $mainQuery->whereIn('m.id', $allowedMachineIds); } } - - // 套用特定機台篩選 if ($selectedMachineId && $selectedMachineId !== 'ALL') { - $mainQuery->where('o.machine_id', $selectedMachineId); + $mainQuery->where('m.id', $selectedMachineId); } - // 套用特定商品篩選 - if ($selectedProductId && $selectedProductId !== 'ALL') { - $mainQuery->where('oi.product_id', $selectedProductId); - } - - // 套用支付工具多選篩選 - if (!empty($paymentTypes)) { - $mainQuery->whereIn('o.payment_type', $paymentTypes); - } - - $mainQuery->groupBy('o.machine_id') - ->groupBy(\Illuminate\Support\Facades\DB::raw('COALESCE(m.serial_no, "UNKNOWN")')) - ->groupBy(\Illuminate\Support\Facades\DB::raw('COALESCE(m.name, "未知機台")')); $tableData = $mainQuery->get(); // 7. 計算總計 Summary @@ -176,46 +234,101 @@ class AnalysisController extends Controller 'total_profit' => round($totalProfit, 2), ]; - // 8. 圖表日走勢統計 SQL - $trendQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') - ->join('orders as o', 'o.id', '=', 'oi.order_id') - ->leftJoin('products as p', 'p.id', '=', 'oi.product_id') + // 8. 圖表日走勢統計 + // 每日營業額 + $trendSalesQuery = \Illuminate\Support\Facades\DB::table('orders as o') ->select([ \Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"), - \Illuminate\Support\Facades\DB::raw('SUM(oi.quantity) as total_quantity'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) as total_sales'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.quantity * COALESCE(p.cost, 0)) as total_cost'), + \Illuminate\Support\Facades\DB::raw('SUM(o.pay_amount) as total_sales') ]) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->whereNull('o.deleted_at') ->whereBetween('o.created_at', [$startDate, $endDate]); + // 套用篩選到走勢營業額 if ($effectiveCompanyId) { - $trendQuery->where('o.company_id', $effectiveCompanyId); + $trendSalesQuery->where('o.company_id', $effectiveCompanyId); } - if ($allowedMachineIds !== null) { if (empty($allowedMachineIds)) { - $trendQuery->whereRaw('1 = 0'); + $trendSalesQuery->whereRaw('1 = 0'); } else { - $trendQuery->whereIn('o.machine_id', $allowedMachineIds); + $trendSalesQuery->whereIn('o.machine_id', $allowedMachineIds); } } - if ($selectedMachineId && $selectedMachineId !== 'ALL') { - $trendQuery->where('o.machine_id', $selectedMachineId); + $trendSalesQuery->where('o.machine_id', $selectedMachineId); } - - if ($selectedProductId && $selectedProductId !== 'ALL') { - $trendQuery->where('oi.product_id', $selectedProductId); - } - if (!empty($paymentTypes)) { - $trendQuery->whereIn('o.payment_type', $paymentTypes); + $trendSalesQuery->whereIn('o.payment_type', $paymentTypes); } - $trendQuery->groupBy('date')->orderBy('date', 'asc'); - $trendData = $trendQuery->get(); + // 如果篩選特定商品,營業額只計算有成功出貨的商品價格 + if ($selectedProductId && $selectedProductId !== 'ALL') { + $trendSalesQuery->join('order_items as oi', 'oi.order_id', '=', 'o.id') + ->leftJoin('dispense_records as dr', function($join) { + $join->on('dr.order_id', '=', 'oi.order_id') + ->on('dr.product_id', '=', 'oi.product_id') + ->where('dr.dispense_status', '=', 1); + }) + ->where('oi.product_id', $selectedProductId) + ->select([ + \Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"), + \Illuminate\Support\Facades\DB::raw('SUM(CASE WHEN dr.id IS NOT NULL THEN oi.subtotal ELSE 0 END) as total_sales') + ]); + } + $trendSalesQuery->groupBy('date'); + $trendSalesData = $trendSalesQuery->get(); + + // 每日出貨件數與成本 + $trendDispenseQuery = \Illuminate\Support\Facades\DB::table('dispense_records as dr') + ->join('orders as o', 'o.id', '=', 'dr.order_id') + ->leftJoin('products as p', 'p.id', '=', 'dr.product_id') + ->select([ + \Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"), + \Illuminate\Support\Facades\DB::raw('COUNT(dr.id) as total_quantity'), + \Illuminate\Support\Facades\DB::raw('SUM(COALESCE(p.cost, 0)) as total_cost') + ]) + ->where('dr.dispense_status', 1) + ->whereNull('o.deleted_at') + ->whereBetween('o.created_at', [$startDate, $endDate]); + + // 套用篩選到走勢出貨 + if ($effectiveCompanyId) { + $trendDispenseQuery->where('o.company_id', $effectiveCompanyId); + } + if ($allowedMachineIds !== null) { + if (empty($allowedMachineIds)) { + $trendDispenseQuery->whereRaw('1 = 0'); + } else { + $trendDispenseQuery->whereIn('o.machine_id', $allowedMachineIds); + } + } + if ($selectedMachineId && $selectedMachineId !== 'ALL') { + $trendDispenseQuery->where('o.machine_id', $selectedMachineId); + } + if ($selectedProductId && $selectedProductId !== 'ALL') { + $trendDispenseQuery->where('dr.product_id', $selectedProductId); + } + if (!empty($paymentTypes)) { + $trendDispenseQuery->whereIn('o.payment_type', $paymentTypes); + } + $trendDispenseQuery->groupBy('date'); + $trendDispenseData = $trendDispenseQuery->get(); + + // 將子查詢結果轉換為 PHP Map 對照 + $salesMap = []; + foreach ($trendSalesData as $row) { + $salesMap[$row->date] = $row->total_sales; + } + + $dispenseMap = []; + foreach ($trendDispenseData as $row) { + $dispenseMap[$row->date] = [ + 'quantity' => $row->total_quantity, + 'cost' => $row->total_cost + ]; + } // 補齊日期區間內所有空缺日走勢 $period = new \DatePeriod( @@ -227,21 +340,16 @@ class AnalysisController extends Controller $chartData = []; foreach ($period as $date) { $dateStr = $date->format('Y-m-d'); + $hasSales = isset($salesMap[$dateStr]); + $hasDispense = isset($dispenseMap[$dateStr]); + $chartData[$dateStr] = [ 'date' => $dateStr, - 'quantity' => 0, - 'sales' => 0.0, - 'cost' => 0.0, + 'quantity' => $hasDispense ? (int)$dispenseMap[$dateStr]['quantity'] : 0, + 'sales' => $hasSales ? round((float)$salesMap[$dateStr], 2) : 0.0, + 'cost' => $hasDispense ? round((float)$dispenseMap[$dateStr]['cost'], 2) : 0.0, ]; } - - foreach ($trendData as $row) { - if (isset($chartData[$row->date])) { - $chartData[$row->date]['quantity'] = (int)$row->total_quantity; - $chartData[$row->date]['sales'] = round((float)$row->total_sales, 2); - $chartData[$row->date]['cost'] = round((float)$row->total_cost, 2); - } - } $chartData = array_values($chartData); // 9. 判斷是否為 AJAX 請求,回傳 JSON 格式 @@ -340,15 +448,20 @@ class AnalysisController extends Controller // 計算各商品的銷售數量、銷售額、總成本、利潤 $mainQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') ->join('orders as o', 'o.id', '=', 'oi.order_id') + ->leftJoin('dispense_records as dr', function($join) { + $join->on('dr.order_id', '=', 'oi.order_id') + ->on('dr.product_id', '=', 'oi.product_id') + ->where('dr.dispense_status', '=', 1); + }) ->leftJoin('products as p', 'p.id', '=', 'oi.product_id') ->select([ 'oi.product_id', \Illuminate\Support\Facades\DB::raw('COALESCE(p.barcode, oi.barcode) as barcode'), \Illuminate\Support\Facades\DB::raw('COALESCE(p.name, oi.product_name) as name'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.quantity) as total_quantity'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) as total_sales'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.quantity * COALESCE(p.cost, 0)) as total_cost'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) - SUM(oi.quantity * COALESCE(p.cost, 0)) as total_profit') + \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN 1 ELSE 0 END) as SIGNED) as total_quantity'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) as DECIMAL(10,2)) as total_sales'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END) as DECIMAL(10,2)) as total_cost'), + \Illuminate\Support\Facades\DB::raw('CAST((SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) - SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END)) as DECIMAL(10,2)) as total_profit') ]) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->whereNull('o.deleted_at') @@ -411,12 +524,17 @@ class AnalysisController extends Controller // 7. 圖表日走勢統計 SQL $trendQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') ->join('orders as o', 'o.id', '=', 'oi.order_id') + ->leftJoin('dispense_records as dr', function($join) { + $join->on('dr.order_id', '=', 'oi.order_id') + ->on('dr.product_id', '=', 'oi.product_id') + ->where('dr.dispense_status', '=', 1); + }) ->leftJoin('products as p', 'p.id', '=', 'oi.product_id') ->select([ \Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"), - \Illuminate\Support\Facades\DB::raw('SUM(oi.quantity) as total_quantity'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.subtotal) as total_sales'), - \Illuminate\Support\Facades\DB::raw('SUM(oi.quantity * COALESCE(p.cost, 0)) as total_cost'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN 1 ELSE 0 END) as SIGNED) as total_quantity'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) as DECIMAL(10,2)) as total_sales'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END) as DECIMAL(10,2)) as total_cost') ]) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->whereNull('o.deleted_at') diff --git a/tests/Feature/Admin/AnalysisReportsTest.php b/tests/Feature/Admin/AnalysisReportsTest.php index c623055..b925bea 100644 --- a/tests/Feature/Admin/AnalysisReportsTest.php +++ b/tests/Feature/Admin/AnalysisReportsTest.php @@ -8,6 +8,7 @@ use App\Models\Machine\Machine; use App\Models\Product\Product; use App\Models\Transaction\Order; use App\Models\Transaction\OrderItem; +use App\Models\Transaction\DispenseRecord; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -143,6 +144,27 @@ class AnalysisReportsTest extends TestCase 'subtotal' => 30.00, ]); + DispenseRecord::create([ + 'company_id' => $this->company->id, + 'order_id' => $order1->id, + 'machine_id' => $this->machineA->id, + 'slot_no' => '1', + 'product_id' => $this->productX->id, + 'amount' => 15.00, + 'dispense_status' => 1, + 'machine_time' => now(), + ]); + DispenseRecord::create([ + 'company_id' => $this->company->id, + 'order_id' => $order1->id, + 'machine_id' => $this->machineA->id, + 'slot_no' => '1', + 'product_id' => $this->productX->id, + 'amount' => 15.00, + 'dispense_status' => 1, + 'machine_time' => now(), + ]); + // Order 2: Machine B, Product Y, payment type 2 (E-ticket), price 25, quantity 1. (Sales: 25, Cost: 20, Profit: 5) $order2 = Order::create([ 'company_id' => $this->company->id, @@ -163,6 +185,17 @@ class AnalysisReportsTest extends TestCase 'price' => 25.00, 'subtotal' => 25.00, ]); + + DispenseRecord::create([ + 'company_id' => $this->company->id, + 'order_id' => $order2->id, + 'machine_id' => $this->machineB->id, + 'slot_no' => '1', + 'product_id' => $this->productY->id, + 'amount' => 25.00, + 'dispense_status' => 1, + 'machine_time' => now(), + ]); } /** From 36c07631066a62b88f575e7b15ab8d2ba2fd8d79 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 5 Jun 2026 17:38:13 +0800 Subject: [PATCH 2/4] =?UTF-8?q?[PROMOTE]=20=E5=90=8C=E6=AD=A5=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E5=B7=A5=E4=BD=9C=E6=B5=81=20.gitea/workflows/deploy-?= =?UTF-8?q?prod.yaml?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy-prod.yaml | 31 ++----------------------------- 1 file changed, 2 insertions(+), 29 deletions(-) diff --git a/.gitea/workflows/deploy-prod.yaml b/.gitea/workflows/deploy-prod.yaml index 97aaa36..262200b 100644 --- a/.gitea/workflows/deploy-prod.yaml +++ b/.gitea/workflows/deploy-prod.yaml @@ -31,21 +31,9 @@ jobs: docker run --rm -v /home/twsystem1003/star-cloud:/target busybox mkdir -p /target/docker/emqx cat docker/emqx/acl.conf | docker run -i --rm -v /home/twsystem1003/star-cloud:/target busybox sh -c "cat > /target/docker/emqx/acl.conf" - - name: Step 1 - Ensure Existing Containers Are Running + - name: Step 1 - Build & Deploy Containers run: | - for container in \ - star-cloud-mysql \ - star-cloud-redis \ - star-cloud-emqx - do - if ! docker inspect "${container}" >/dev/null 2>&1; then - echo ">>> Required container is missing: ${container}" - exit 1 - fi - - echo ">>> Ensuring ${container} is running..." - docker start "${container}" >/dev/null - done + WWWGROUP=1001 WWWUSER=1001 docker-compose up -d --build for container in star-cloud-mysql star-cloud-redis star-cloud-emqx; do echo ">>> Waiting for ${container} to become healthy..." @@ -68,21 +56,6 @@ jobs: done done - for container in \ - star-cloud-laravel \ - star-cloud-queue \ - star-cloud-mqtt-worker \ - star-cloud-mqtt-gateway - do - if ! docker inspect "${container}" >/dev/null 2>&1; then - echo ">>> Required container is missing: ${container}" - exit 1 - fi - - echo ">>> Ensuring ${container} is running..." - docker start "${container}" >/dev/null - done - - name: Step 2 - Deploy Code run: | echo ">>> Syncing code to star-cloud-laravel..." From e29a01f61150c82efd880b69378c1ef35be50233 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 5 Jun 2026 21:50:23 +0800 Subject: [PATCH 3/4] =?UTF-8?q?[FIX]=20=E4=BF=AE=E6=AD=A3=E6=A9=9F?= =?UTF-8?q?=E5=8F=B0=E5=A0=B1=E8=A1=A8=E5=9B=A0=20joinSub=20=E6=BC=8F?= =?UTF-8?q?=E6=8E=89=E7=84=A1=E4=BB=98=E8=B2=BB=E7=87=9F=E6=A5=AD=E9=A1=8D?= =?UTF-8?q?=E6=A9=9F=E5=8F=B0=E5=87=BA=E8=B2=A8=E6=95=B8=E9=87=8F=E7=9A=84?= =?UTF-8?q?=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Admin/AnalysisController.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/Admin/AnalysisController.php b/app/Http/Controllers/Admin/AnalysisController.php index 3ee00b8..d4ce8ce 100644 --- a/app/Http/Controllers/Admin/AnalysisController.php +++ b/app/Http/Controllers/Admin/AnalysisController.php @@ -181,7 +181,7 @@ class AnalysisController extends Controller // 聯結機台與子查詢,取得最終統計資料 $mainQuery = \Illuminate\Support\Facades\DB::table('machines as m') - ->joinSub($salesQuery, 'sq', 'sq.machine_id', '=', 'm.id') + ->leftJoinSub($salesQuery, 'sq', 'sq.machine_id', '=', 'm.id') ->leftJoinSub($dispenseQuery, 'dq', 'dq.machine_id', '=', 'm.id') ->select([ 'm.id as machine_id', @@ -191,7 +191,11 @@ class AnalysisController extends Controller \Illuminate\Support\Facades\DB::raw('CAST(COALESCE(sq.total_sales, 0) as DECIMAL(10,2)) as total_sales'), \Illuminate\Support\Facades\DB::raw('CAST(COALESCE(dq.total_cost, 0) as DECIMAL(10,2)) as total_cost'), \Illuminate\Support\Facades\DB::raw('CAST((COALESCE(sq.total_sales, 0) - COALESCE(dq.total_cost, 0)) as DECIMAL(10,2)) as total_profit') - ]); + ]) + ->where(function($query) { + $query->whereNotNull('sq.total_sales') + ->orWhereNotNull('dq.total_quantity'); + }); if ($effectiveCompanyId) { $mainQuery->where('m.company_id', $effectiveCompanyId); From b6c71426773ab05ba8975f4335d07775d3b62bd2 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 5 Jun 2026 21:56:41 +0800 Subject: [PATCH 4/4] =?UTF-8?q?[FIX]=20=E4=BF=AE=E6=AD=A3=E5=95=86?= =?UTF-8?q?=E5=93=81=E8=88=87=E6=A9=9F=E5=8F=B0=E5=A0=B1=E8=A1=A8=E5=9B=A0?= =?UTF-8?q?=20order=5Fitems=20=E8=88=87=20dispense=5Frecords=20=E8=81=AF?= =?UTF-8?q?=E7=B5=90=E7=94=A2=E7=94=9F=E7=AC=9B=E5=8D=A1=E7=88=BE=E7=A9=8D?= =?UTF-8?q?=E5=B0=8E=E8=87=B4=E4=BB=B6=E6=95=B8=E9=87=8D=E8=A4=87=E5=8A=A0?= =?UTF-8?q?=E7=B8=BD=E7=9A=84=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Admin/AnalysisController.php | 102 +++++++++++------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/app/Http/Controllers/Admin/AnalysisController.php b/app/Http/Controllers/Admin/AnalysisController.php index d4ce8ce..9bd1ee0 100644 --- a/app/Http/Controllers/Admin/AnalysisController.php +++ b/app/Http/Controllers/Admin/AnalysisController.php @@ -99,6 +99,15 @@ class AnalysisController extends Controller 42 => __('Pickup Voucher'), ]; + // 子查詢:取得每個 order_id + product_id 的銷售單價 + $itemPriceQuery = \Illuminate\Support\Facades\DB::table('order_items') + ->select([ + 'order_id', + 'product_id', + \Illuminate\Support\Facades\DB::raw('MIN(price) as unit_price') + ]) + ->groupBy('order_id', 'product_id'); + // 6. 執行主報表統計 SQL (按機台分組) // 子查詢 A:營業額 (計算機台實收) $salesQuery = \Illuminate\Support\Facades\DB::table('orders as o') @@ -130,16 +139,18 @@ class AnalysisController extends Controller // 如果篩選特定商品,營業額只計算有成功出貨的該商品金額 if ($selectedProductId && $selectedProductId !== 'ALL') { - $salesQuery->join('order_items as oi', 'oi.order_id', '=', 'o.id') - ->leftJoin('dispense_records as dr', function($join) { - $join->on('dr.order_id', '=', 'oi.order_id') - ->on('dr.product_id', '=', 'oi.product_id') + $salesQuery->join('dispense_records as dr', function($join) { + $join->on('dr.order_id', '=', 'o.id') ->where('dr.dispense_status', '=', 1); }) - ->where('oi.product_id', $selectedProductId) + ->leftJoinSub($itemPriceQuery, 'oi', function($join) { + $join->on('oi.order_id', '=', 'dr.order_id') + ->on('oi.product_id', '=', 'dr.product_id'); + }) + ->where('dr.product_id', $selectedProductId) ->select([ 'o.machine_id', - \Illuminate\Support\Facades\DB::raw('SUM(CASE WHEN dr.id IS NOT NULL THEN oi.subtotal ELSE 0 END) as total_sales') + \Illuminate\Support\Facades\DB::raw('SUM(COALESCE(oi.unit_price, 0)) as total_sales') ]); } $salesQuery->groupBy('o.machine_id'); @@ -269,16 +280,18 @@ class AnalysisController extends Controller // 如果篩選特定商品,營業額只計算有成功出貨的商品價格 if ($selectedProductId && $selectedProductId !== 'ALL') { - $trendSalesQuery->join('order_items as oi', 'oi.order_id', '=', 'o.id') - ->leftJoin('dispense_records as dr', function($join) { - $join->on('dr.order_id', '=', 'oi.order_id') - ->on('dr.product_id', '=', 'oi.product_id') + $trendSalesQuery->join('dispense_records as dr', function($join) { + $join->on('dr.order_id', '=', 'o.id') ->where('dr.dispense_status', '=', 1); }) - ->where('oi.product_id', $selectedProductId) + ->leftJoinSub($itemPriceQuery, 'oi', function($join) { + $join->on('oi.order_id', '=', 'dr.order_id') + ->on('oi.product_id', '=', 'dr.product_id'); + }) + ->where('dr.product_id', $selectedProductId) ->select([ \Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"), - \Illuminate\Support\Facades\DB::raw('SUM(CASE WHEN dr.id IS NOT NULL THEN oi.subtotal ELSE 0 END) as total_sales') + \Illuminate\Support\Facades\DB::raw('SUM(COALESCE(oi.unit_price, 0)) as total_sales') ]); } $trendSalesQuery->groupBy('date'); @@ -449,24 +462,35 @@ class AnalysisController extends Controller $products = $productsQuery->active()->get(['id', 'name', 'barcode']); // 5. 執行主報表統計 SQL - // 計算各商品的銷售數量、銷售額、總成本、利潤 - $mainQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') - ->join('orders as o', 'o.id', '=', 'oi.order_id') - ->leftJoin('dispense_records as dr', function($join) { - $join->on('dr.order_id', '=', 'oi.order_id') - ->on('dr.product_id', '=', 'oi.product_id') - ->where('dr.dispense_status', '=', 1); - }) - ->leftJoin('products as p', 'p.id', '=', 'oi.product_id') + // 子查詢:取得每個 order_id + product_id 的銷售單價與品名 + $itemPriceQuery = \Illuminate\Support\Facades\DB::table('order_items') ->select([ - 'oi.product_id', + 'order_id', + 'product_id', + \Illuminate\Support\Facades\DB::raw('MIN(price) as unit_price'), + \Illuminate\Support\Facades\DB::raw('MIN(barcode) as barcode'), + \Illuminate\Support\Facades\DB::raw('MIN(product_name) as product_name') + ]) + ->groupBy('order_id', 'product_id'); + + // 計算各商品的銷售數量、銷售額、總成本、利潤 (以出貨成功為主體) + $mainQuery = \Illuminate\Support\Facades\DB::table('dispense_records as dr') + ->join('orders as o', 'o.id', '=', 'dr.order_id') + ->leftJoinSub($itemPriceQuery, 'oi', function($join) { + $join->on('oi.order_id', '=', 'dr.order_id') + ->on('oi.product_id', '=', 'dr.product_id'); + }) + ->leftJoin('products as p', 'p.id', '=', 'dr.product_id') + ->select([ + 'dr.product_id', \Illuminate\Support\Facades\DB::raw('COALESCE(p.barcode, oi.barcode) as barcode'), \Illuminate\Support\Facades\DB::raw('COALESCE(p.name, oi.product_name) as name'), - \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN 1 ELSE 0 END) as SIGNED) as total_quantity'), - \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) as DECIMAL(10,2)) as total_sales'), - \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END) as DECIMAL(10,2)) as total_cost'), - \Illuminate\Support\Facades\DB::raw('CAST((SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) - SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END)) as DECIMAL(10,2)) as total_profit') + \Illuminate\Support\Facades\DB::raw('CAST(COUNT(dr.id) as SIGNED) as total_quantity'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(COALESCE(oi.unit_price, 0)) as DECIMAL(10,2)) as total_sales'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(COALESCE(p.cost, 0)) as DECIMAL(10,2)) as total_cost'), + \Illuminate\Support\Facades\DB::raw('CAST((SUM(COALESCE(oi.unit_price, 0)) - SUM(COALESCE(p.cost, 0))) as DECIMAL(10,2)) as total_profit') ]) + ->where('dr.dispense_status', 1) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->whereNull('o.deleted_at') ->whereBetween('o.created_at', [$startDate, $endDate]); @@ -492,10 +516,10 @@ class AnalysisController extends Controller // 套用特定商品篩選 if ($selectedProductId && $selectedProductId !== 'ALL') { - $mainQuery->where('oi.product_id', $selectedProductId); + $mainQuery->where('dr.product_id', $selectedProductId); } - $mainQuery->groupBy('oi.product_id') + $mainQuery->groupBy('dr.product_id') ->groupBy(\Illuminate\Support\Facades\DB::raw('COALESCE(p.barcode, oi.barcode)')) ->groupBy(\Illuminate\Support\Facades\DB::raw('COALESCE(p.name, oi.product_name)')); $tableData = $mainQuery->get(); @@ -526,20 +550,20 @@ class AnalysisController extends Controller ]; // 7. 圖表日走勢統計 SQL - $trendQuery = \Illuminate\Support\Facades\DB::table('order_items as oi') - ->join('orders as o', 'o.id', '=', 'oi.order_id') - ->leftJoin('dispense_records as dr', function($join) { - $join->on('dr.order_id', '=', 'oi.order_id') - ->on('dr.product_id', '=', 'oi.product_id') - ->where('dr.dispense_status', '=', 1); + $trendQuery = \Illuminate\Support\Facades\DB::table('dispense_records as dr') + ->join('orders as o', 'o.id', '=', 'dr.order_id') + ->leftJoinSub($itemPriceQuery, 'oi', function($join) { + $join->on('oi.order_id', '=', 'dr.order_id') + ->on('oi.product_id', '=', 'dr.product_id'); }) - ->leftJoin('products as p', 'p.id', '=', 'oi.product_id') + ->leftJoin('products as p', 'p.id', '=', 'dr.product_id') ->select([ \Illuminate\Support\Facades\DB::raw("DATE_FORMAT(o.created_at, '%Y-%m-%d') as date"), - \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN 1 ELSE 0 END) as SIGNED) as total_quantity'), - \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN oi.price ELSE 0 END) as DECIMAL(10,2)) as total_sales'), - \Illuminate\Support\Facades\DB::raw('CAST(SUM(CASE WHEN dr.id IS NOT NULL THEN COALESCE(p.cost, 0) ELSE 0 END) as DECIMAL(10,2)) as total_cost') + \Illuminate\Support\Facades\DB::raw('CAST(COUNT(dr.id) as SIGNED) as total_quantity'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(COALESCE(oi.unit_price, 0)) as DECIMAL(10,2)) as total_sales'), + \Illuminate\Support\Facades\DB::raw('CAST(SUM(COALESCE(p.cost, 0)) as DECIMAL(10,2)) as total_cost') ]) + ->where('dr.dispense_status', 1) ->where('o.payment_status', \App\Models\Transaction\Order::PAYMENT_STATUS_SUCCESS) ->whereNull('o.deleted_at') ->whereBetween('o.created_at', [$startDate, $endDate]); @@ -561,7 +585,7 @@ class AnalysisController extends Controller } if ($selectedProductId && $selectedProductId !== 'ALL') { - $trendQuery->where('oi.product_id', $selectedProductId); + $trendQuery->where('dr.product_id', $selectedProductId); } $trendQuery->groupBy('date')->orderBy('date', 'asc');