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(), + ]); } /**