subMonths(6)->format('Y-m-d'); $dateTo = $filters['date_to'] ?? Carbon::now()->format('Y-m-d'); $vendorId = $filters['vendor_id'] ?? null; $warehouseId = $filters['warehouse_id'] ?? null; // 採購單基礎查詢(排除草稿與已取消) $poQuery = PurchaseOrder::whereNotIn('status', ['draft', 'cancelled']) ->whereBetween('order_date', [$dateFrom, $dateTo]); if ($vendorId) $poQuery->where('vendor_id', $vendorId); if ($warehouseId) $poQuery->where('warehouse_id', $warehouseId); $totalAmount = (clone $poQuery)->sum('grand_total') ?: (clone $poQuery)->sum('total_amount'); $totalOrders = (clone $poQuery)->count(); // 進貨單查詢 $grQuery = DB::table('goods_receipts') ->where('status', 'completed') ->whereBetween('received_date', [$dateFrom, $dateTo]) ->whereNull('deleted_at'); if ($vendorId) $grQuery->where('vendor_id', $vendorId); if ($warehouseId) $grQuery->where('warehouse_id', $warehouseId); $totalReceipts = (clone $grQuery)->count(); // 平均交期天數(採購單→進貨單) $deliveryStats = DB::table('purchase_orders as po') ->join('goods_receipts as gr', 'gr.purchase_order_id', '=', 'po.id') ->whereNotIn('po.status', ['draft', 'cancelled']) ->where('gr.status', 'completed') ->whereNull('gr.deleted_at') ->whereBetween('po.order_date', [$dateFrom, $dateTo]); if ($vendorId) $deliveryStats->where('po.vendor_id', $vendorId); if ($warehouseId) $deliveryStats->where('po.warehouse_id', $warehouseId); $deliveryStats = $deliveryStats->selectRaw(' AVG(DATEDIFF(gr.received_date, po.order_date)) as avg_days, COUNT(*) as total_linked, SUM(CASE WHEN gr.received_date <= po.expected_delivery_date THEN 1 ELSE 0 END) as on_time_count ')->first(); $avgDays = round($deliveryStats->avg_days ?? 0, 1); $onTimeRate = $deliveryStats->total_linked > 0 ? round(($deliveryStats->on_time_count / $deliveryStats->total_linked) * 100, 1) : 0; return [ 'total_amount' => (float) $totalAmount, 'total_orders' => $totalOrders, 'total_receipts' => $totalReceipts, 'avg_delivery_days' => $avgDays, 'on_time_rate' => $onTimeRate, ]; } /** * 廠商供貨時效分析 */ public function getDeliveryAnalysis(array $filters): array { $dateFrom = $filters['date_from'] ?? Carbon::now()->subMonths(6)->format('Y-m-d'); $dateTo = $filters['date_to'] ?? Carbon::now()->format('Y-m-d'); $vendorId = $filters['vendor_id'] ?? null; $warehouseId = $filters['warehouse_id'] ?? null; $query = DB::table('purchase_orders as po') ->join('goods_receipts as gr', 'gr.purchase_order_id', '=', 'po.id') ->whereNotIn('po.status', ['draft', 'cancelled']) ->where('gr.status', 'completed') ->whereNull('gr.deleted_at') ->whereBetween('po.order_date', [$dateFrom, $dateTo]); if ($vendorId) $query->where('po.vendor_id', $vendorId); if ($warehouseId) $query->where('po.warehouse_id', $warehouseId); // 按廠商分組的交期統計 $vendorDelivery = (clone $query) ->join('vendors', 'vendors.id', '=', 'po.vendor_id') ->groupBy('po.vendor_id', 'vendors.name') ->selectRaw(' po.vendor_id, vendors.name as vendor_name, COUNT(*) as total_count, ROUND(AVG(DATEDIFF(gr.received_date, po.order_date)), 1) as avg_days, MIN(DATEDIFF(gr.received_date, po.order_date)) as min_days, MAX(DATEDIFF(gr.received_date, po.order_date)) as max_days, SUM(CASE WHEN gr.received_date <= po.expected_delivery_date THEN 1 ELSE 0 END) as on_time_count ') ->orderBy('avg_days', 'asc') ->get() ->map(function ($row) { $row->on_time_rate = $row->total_count > 0 ? round(($row->on_time_count / $row->total_count) * 100, 1) : 0; return $row; }); // 延遲分佈統計 $delayDistribution = (clone $query) ->selectRaw(" CASE WHEN DATEDIFF(gr.received_date, po.order_date) <= 0 THEN '提前到貨' WHEN DATEDIFF(gr.received_date, po.order_date) BETWEEN 1 AND 3 THEN '1-3天' WHEN DATEDIFF(gr.received_date, po.order_date) BETWEEN 4 AND 7 THEN '4-7天' WHEN DATEDIFF(gr.received_date, po.order_date) BETWEEN 8 AND 14 THEN '8-14天' ELSE '超過14天' END as category, COUNT(*) as count ") ->groupByRaw(" CASE WHEN DATEDIFF(gr.received_date, po.order_date) <= 0 THEN '提前到貨' WHEN DATEDIFF(gr.received_date, po.order_date) BETWEEN 1 AND 3 THEN '1-3天' WHEN DATEDIFF(gr.received_date, po.order_date) BETWEEN 4 AND 7 THEN '4-7天' WHEN DATEDIFF(gr.received_date, po.order_date) BETWEEN 8 AND 14 THEN '8-14天' ELSE '超過14天' END ") ->get(); return [ 'vendor_delivery' => $vendorDelivery, 'delay_distribution' => $delayDistribution, ]; } /** * 進貨數量分析 */ public function getQuantityAnalysis(array $filters): array { $dateFrom = $filters['date_from'] ?? Carbon::now()->subMonths(6)->format('Y-m-d'); $dateTo = $filters['date_to'] ?? Carbon::now()->format('Y-m-d'); $vendorId = $filters['vendor_id'] ?? null; $warehouseId = $filters['warehouse_id'] ?? null; $baseQuery = DB::table('goods_receipts as gr') ->join('goods_receipt_items as gri', 'gri.goods_receipt_id', '=', 'gr.id') ->where('gr.status', 'completed') ->whereNull('gr.deleted_at') ->whereBetween('gr.received_date', [$dateFrom, $dateTo]); if ($vendorId) $baseQuery->where('gr.vendor_id', $vendorId); if ($warehouseId) $baseQuery->where('gr.warehouse_id', $warehouseId); // 月度進貨量趨勢 $monthlyTrend = (clone $baseQuery) ->selectRaw(" DATE_FORMAT(gr.received_date, '%Y-%m') as month, ROUND(SUM(gri.quantity_received), 2) as total_quantity, ROUND(SUM(gri.total_amount), 2) as total_amount, COUNT(DISTINCT gr.id) as receipt_count ") ->groupByRaw("DATE_FORMAT(gr.received_date, '%Y-%m')") ->orderBy('month', 'asc') ->get(); // 廠商佔比(按進貨金額) $vendorShare = (clone $baseQuery) ->join('vendors', 'vendors.id', '=', 'gr.vendor_id') ->selectRaw(' gr.vendor_id, vendors.name as vendor_name, ROUND(SUM(gri.total_amount), 2) as total_amount, ROUND(SUM(gri.quantity_received), 2) as total_quantity ') ->groupBy('gr.vendor_id', 'vendors.name') ->orderByDesc('total_amount') ->limit(10) ->get(); // 商品進貨排行 Top 10 $productRanking = (clone $baseQuery) ->join('products', 'products.id', '=', 'gri.product_id') ->selectRaw(' gri.product_id, products.name as product_name, products.code as product_code, ROUND(SUM(gri.quantity_received), 2) as total_quantity, ROUND(SUM(gri.total_amount), 2) as total_amount ') ->groupBy('gri.product_id', 'products.name', 'products.code') ->orderByDesc('total_amount') ->limit(10) ->get(); return [ 'monthly_trend' => $monthlyTrend, 'vendor_share' => $vendorShare, 'product_ranking' => $productRanking, ]; } /** * 單價趨勢分析 */ public function getPriceTrendAnalysis(array $filters): array { $dateFrom = $filters['date_from'] ?? Carbon::now()->subMonths(6)->format('Y-m-d'); $dateTo = $filters['date_to'] ?? Carbon::now()->format('Y-m-d'); $vendorId = $filters['vendor_id'] ?? null; $warehouseId = $filters['warehouse_id'] ?? null; $baseQuery = DB::table('goods_receipts as gr') ->join('goods_receipt_items as gri', 'gri.goods_receipt_id', '=', 'gr.id') ->join('products', 'products.id', '=', 'gri.product_id') ->where('gr.status', 'completed') ->whereNull('gr.deleted_at') ->whereBetween('gr.received_date', [$dateFrom, $dateTo]); if ($vendorId) $baseQuery->where('gr.vendor_id', $vendorId); if ($warehouseId) $baseQuery->where('gr.warehouse_id', $warehouseId); // 商品月平均單價趨勢(取進貨金額 Top 10 的商品) $topProductIds = (clone $baseQuery) ->selectRaw('gri.product_id, SUM(gri.total_amount) as total') ->groupBy('gri.product_id') ->orderByDesc('total') ->limit(10) ->pluck('product_id'); $priceTrend = []; if ($topProductIds->isNotEmpty()) { $priceTrend = (clone $baseQuery) ->whereIn('gri.product_id', $topProductIds->toArray()) ->selectRaw(" gri.product_id, products.name as product_name, DATE_FORMAT(gr.received_date, '%Y-%m') as month, ROUND(AVG(gri.unit_price), 2) as avg_price, ROUND(MIN(gri.unit_price), 2) as min_price, ROUND(MAX(gri.unit_price), 2) as max_price ") ->groupBy('gri.product_id', 'products.name', DB::raw("DATE_FORMAT(gr.received_date, '%Y-%m')")) ->orderBy('month', 'asc') ->get(); } // 跨廠商比價(同一商品不同廠商的最近價格) $vendorComparison = (clone $baseQuery) ->join('vendors', 'vendors.id', '=', 'gr.vendor_id') ->whereIn('gri.product_id', $topProductIds->toArray()) ->selectRaw(' gri.product_id, products.name as product_name, gr.vendor_id, vendors.name as vendor_name, ROUND(AVG(gri.unit_price), 2) as avg_price, ROUND(MIN(gri.unit_price), 2) as min_price, ROUND(MAX(gri.unit_price), 2) as max_price, COUNT(*) as purchase_count ') ->groupBy('gri.product_id', 'products.name', 'gr.vendor_id', 'vendors.name') ->orderBy('products.name') ->orderBy('avg_price') ->get(); return [ 'price_trend' => $priceTrend, 'vendor_comparison' => $vendorComparison, ]; } }