diff --git a/app/Http/Controllers/Admin/AnalysisController.php b/app/Http/Controllers/Admin/AnalysisController.php index 2e7f099..3b913ab 100644 --- a/app/Http/Controllers/Admin/AnalysisController.php +++ b/app/Http/Controllers/Admin/AnalysisController.php @@ -26,11 +26,186 @@ class AnalysisController extends Controller } // 商品報表分析 - public function productReports() + public function productReports(Request $request) { - return view('admin.placeholder', [ + $user = auth()->user(); + $companyId = $user->company_id; + + // 1. 取得篩選參數 + $selectedCompanyId = $request->input('company_id'); + $selectedProductId = $request->input('product_id', 'ALL'); + $dateRange = $request->input('date_range'); + + // 2. 解析日期區間 + if ($dateRange) { + $dates = explode(' to ', $dateRange); + if (count($dates) === 2) { + $startDate = \Illuminate\Support\Carbon::parse($dates[0])->startOfDay(); + $endDate = \Illuminate\Support\Carbon::parse($dates[1])->endOfDay(); + } else { + $startDate = \Illuminate\Support\Carbon::parse($dates[0])->startOfDay(); + $endDate = \Illuminate\Support\Carbon::parse($dates[0])->endOfDay(); + } + } else { + // 預設為最近 7 天 (含今日) + $startDate = now()->subDays(6)->startOfDay(); + $endDate = now()->endOfDay(); + $dateRange = $startDate->format('Y-m-d') . ' to ' . $endDate->format('Y-m-d'); + } + + // 3. 租戶隔離:非系統管理員強制限制為其所屬公司 + if (!$user->isSystemAdmin()) { + $effectiveCompanyId = $companyId; + } else { + $effectiveCompanyId = $selectedCompanyId; + } + + // 4. 加載下拉選單選項 + // 公司選單 (僅限 Super Admin) + $companies = []; + if ($user->isSystemAdmin()) { + $companies = \App\Models\System\Company::active()->get(['id', 'name']); + } + + // 商品選單 + $productsQuery = \App\Models\Product\Product::query(); + if ($effectiveCompanyId) { + $productsQuery->where('company_id', $effectiveCompanyId); + } + $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('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') + ]) + ->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); + } + + // 套用特定商品篩選 + if ($selectedProductId && $selectedProductId !== 'ALL') { + $mainQuery->where('oi.product_id', $selectedProductId); + } + + $mainQuery->groupBy('oi.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(); + + // 6. 計算總計 Summary + $totalQuantity = 0; + $totalSales = 0.0; + $totalCost = 0.0; + $totalProfit = 0.0; + + foreach ($tableData as $row) { + $row->total_quantity = (int)$row->total_quantity; + $row->total_sales = (float)$row->total_sales; + $row->total_cost = (float)$row->total_cost; + $row->total_profit = (float)$row->total_profit; + + $totalQuantity += $row->total_quantity; + $totalSales += $row->total_sales; + $totalCost += $row->total_cost; + $totalProfit += $row->total_profit; + } + + $summary = [ + 'total_quantity' => $totalQuantity, + 'total_sales' => round($totalSales, 2), + 'total_cost' => round($totalCost, 2), + 'total_profit' => round($totalProfit, 2), + ]; + + // 7. 圖表日走勢統計 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') + ->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'), + ]) + ->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); + } + + if ($selectedProductId && $selectedProductId !== 'ALL') { + $trendQuery->where('oi.product_id', $selectedProductId); + } + + $trendQuery->groupBy('date')->orderBy('date', 'asc'); + $trendData = $trendQuery->get(); + + // 補齊日期區間內所有空缺日走勢 + $period = new \DatePeriod( + $startDate->copy()->startOfDay(), + new \DateInterval('P1D'), + $endDate->copy()->endOfDay() + ); + + $chartData = []; + foreach ($period as $date) { + $dateStr = $date->format('Y-m-d'); + $chartData[$dateStr] = [ + 'date' => $dateStr, + 'quantity' => 0, + 'sales' => 0.0, + 'cost' => 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); + + // 8. 判斷是否為 AJAX 請求,回傳 JSON 格式 + if ($request->ajax()) { + return response()->json([ + 'success' => true, + 'summary' => $summary, + 'chart_data' => $chartData, + 'table_data' => $tableData, + ]); + } + + // 9. 一般 GET 載入視圖 + return view('admin.analysis.product-reports', [ 'title' => '商品報表分析', 'description' => '商品銷售數據分析', + 'companies' => $companies, + 'products' => $products, + 'selectedCompanyId' => $selectedCompanyId, + 'selectedProductId' => $selectedProductId, + 'dateRange' => $dateRange, + 'summary' => $summary, + 'chartData' => $chartData, + 'tableData' => $tableData, ]); } diff --git a/lang/en.json b/lang/en.json index 834e5c8..48bd1b6 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1984,5 +1984,47 @@ "Please use our standard template to ensure data compatibility.": "Please use our standard template to ensure data compatibility.", ":count staff cards imported successfully": ":count staff cards imported successfully", "Import failed": "Import failed", - "System restarting": "System restarting" + "System restarting": "System restarting", + "Product Report Analysis": "Product Report Analysis", + "Smart vending machine product sales, cost and profit analysis report": "Smart vending machine product sales, cost and profit analysis report", + "Real-time operational data synchronized": "Real-time operational data synchronized", + "Total Quantity Sold": "Total Quantity Sold", + "Product physical shipment count": "Product physical shipment count", + "Total Sales Amount": "Total Sales Amount", + "Sales Amount": "Sales Amount", + "Total transaction amount of successful payments": "Total transaction amount of successful payments", + "Total Product Cost": "Total Product Cost", + "Product Cost": "Product Cost", + "Sales Quantity": "Sales Quantity", + "Accumulated dynamic product purchase cost": "Accumulated dynamic product purchase cost", + "Total Sales Profit": "Total Sales Profit", + "Sales revenue minus product cost net value": "Sales revenue minus product cost net value", + "Current data indicates a loss": "Current data indicates a loss", + "Quick Filter": "Quick Filter", + "Last 7 Days": "Last 7 Days", + "Today": "Today", + "This Week": "This Week", + "Last Week": "Last Week", + "This Month": "This Month", + "Last Month": "Last Month", + "Select Product for Analysis": "Select Product for Analysis", + "All Products (ALL)": "All Products (ALL)", + "Tenant Company": "Tenant Company", + "Cross-Company / Headquarter Global Stats": "Cross-Company / Headquarter Global Stats", + "Product Sales & Profit Trend": "Product Sales & Profit Trend", + "Daily operational variance curve for selected timeframe": "Daily operational variance curve for selected timeframe", + "Detailed Analysis Data": "Detailed Analysis Data", + "Search product name, barcode...": "Search product name, barcode...", + "Export Report": "Export Report", + "Copy to Clipboard": "Copy to Clipboard", + "Export to CSV": "Export to CSV", + "Export to Excel": "Export to Excel", + "Print & PDF": "Print & PDF", + "Product Barcode": "Product Barcode", + "Quantity Sold": "Quantity Sold", + "Total Sales": "Total Sales", + "Total Net Profit": "Total Net Profit", + "Gross Margin": "Gross Margin", + "No product data matching search criteria": "No product data matching search criteria", + "Analyze Solely": "Analyze Solely" } \ No newline at end of file diff --git a/lang/ja.json b/lang/ja.json index 61dd5ba..e4af4c0 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1984,5 +1984,47 @@ "Please use our standard template to ensure data compatibility.": "データの互換性を確保するために、標準テンプレートを使用してください。", ":count staff cards imported successfully": ":count 枚のスタッフカードが正常にインポートされました", "Import failed": "インポート失敗", - "System restarting": "再起動中" + "System restarting": "再起動中", + "Product Report Analysis": "商品レポート分析", + "Smart vending machine product sales, cost and profit analysis report": "スマート自動販売機商品販売・コスト利益分析レポート", + "Real-time operational data synchronized": "リアルタイム運営データ同期完了", + "Total Quantity Sold": "販売総数", + "Product physical shipment count": "商品の実出荷数", + "Total Sales Amount": "販売総額", + "Sales Amount": "売上金額", + "Total transaction amount of successful payments": "支払い成功取引総額", + "Total Product Cost": "商品総コスト", + "Product Cost": "商品コスト", + "Sales Quantity": "販売数量", + "Accumulated dynamic product purchase cost": "リアルタイム仕入コスト累計", + "Total Sales Profit": "販売総利益", + "Sales revenue minus product cost net value": "販売額から商品コストを差し引いた純利益", + "Current data indicates a loss": "現在データは赤字を示しています", + "Quick Filter": "クイックフィルター", + "Last 7 Days": "過去7日間", + "Today": "今日", + "This Week": "今週", + "Last Week": "先週", + "This Month": "今月", + "Last Month": "先月", + "Select Product for Analysis": "分析対象商品の選択", + "All Products (ALL)": "全商品 (ALL)", + "Tenant Company": "所属テナント会社", + "Cross-Company / Headquarter Global Stats": "複数会社間 / 本部グローバル統計", + "Product Sales & Profit Trend": "商品販売・利益推移", + "Daily operational variance curve for selected timeframe": "選択期間における日次運営変動曲線", + "Detailed Analysis Data": "明細分析データ", + "Search product name, barcode...": "商品名、バーコードで検索...", + "Export Report": "レポートのエクスポート", + "Copy to Clipboard": "クリップボードにコピー", + "Export to CSV": "CSVエクスポート", + "Export to Excel": "Excelエクスポート", + "Print & PDF": "印刷 & PDF", + "Product Barcode": "商品バーコード", + "Quantity Sold": "販売数", + "Total Sales": "販売総額", + "Total Net Profit": "純利益総額", + "Gross Margin": "粗利益率", + "No product data matching search criteria": "検索条件に一致する商品データはありません", + "Analyze Solely": "個別分析" } \ No newline at end of file diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 9dadedc..6509e85 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -2033,5 +2033,47 @@ "Please use our standard template to ensure data compatibility.": "請使用標準範例檔以確保資料相容性。", ":count staff cards imported successfully": "成功匯入 :count 張員工識別卡", "Import failed": "匯入失敗", - "System restarting": "重啟中" + "System restarting": "重啟中", + "Product Report Analysis": "商品報表分析", + "Smart vending machine product sales, cost and profit analysis report": "智慧型販賣機商品銷售與成本利潤分析報表", + "Real-time operational data synchronized": "即時營運數據已同步", + "Total Quantity Sold": "銷售總數量", + "Product physical shipment count": "商品實體出貨件數", + "Total Sales Amount": "銷售總金額", + "Sales Amount": "銷售金額", + "Total transaction amount of successful payments": "已付款成功之交易總額", + "Total Product Cost": "商品總成本", + "Product Cost": "商品成本", + "Sales Quantity": "銷售數量", + "Accumulated dynamic product purchase cost": "即時商品進貨成本累計", + "Total Sales Profit": "銷售總利潤", + "Sales revenue minus product cost net value": "銷售額扣除商品成本淨值", + "Current data indicates a loss": "當前數據出現虧損", + "Quick Filter": "快捷篩選", + "Last 7 Days": "近 7 日", + "Today": "今日", + "This Week": "本週", + "Last Week": "上週", + "This Month": "本月", + "Last Month": "上月", + "Select Product for Analysis": "選擇分析商品", + "All Products (ALL)": "全部商品 (ALL)", + "Tenant Company": "所屬租戶公司", + "Cross-Company / Headquarter Global Stats": "跨公司 / 總部全域統計", + "Product Sales & Profit Trend": "商品銷售利潤走勢", + "Daily operational variance curve for selected timeframe": "所選統計區間之每日營運變動曲線", + "Detailed Analysis Data": "明細分析數據", + "Search product name, barcode...": "搜尋商品名稱、條碼...", + "Export Report": "匯出報表", + "Copy to Clipboard": "複製至剪貼簿", + "Export to CSV": "匯出成 CSV", + "Export to Excel": "匯出成 Excel", + "Print & PDF": "列印與 PDF", + "Product Barcode": "商品條碼", + "Quantity Sold": "銷售數量", + "Total Sales": "銷售總額", + "Total Net Profit": "總淨利潤", + "Gross Margin": "毛利率", + "No product data matching search criteria": "沒有符合搜尋條件的商品資料", + "Analyze Solely": "單獨分析" } \ No newline at end of file diff --git a/resources/views/admin/analysis/product-reports.blade.php b/resources/views/admin/analysis/product-reports.blade.php new file mode 100644 index 0000000..c28ba0b --- /dev/null +++ b/resources/views/admin/analysis/product-reports.blade.php @@ -0,0 +1,1073 @@ +@extends('layouts.admin') + +@section('scripts') + +@endsection + +@section('content') +
{{ __('Sales Quantity') }}
+{{ __('Total Sales Amount') }}
+{{ __('Total Product Cost') }}
+{{ __('Total Sales Profit') }}
+{{ __('Daily operational variance curve for selected timeframe') }}
+|
+
+ {{ __('Product Barcode') }}
+
+
+
+
+ |
+
+
+ {{ __('Product Name') }}
+
+
+
+
+ |
+
+
+ {{ __('Quantity Sold') }}
+
+
+
+
+ |
+
+
+ {{ __('Total Sales') }}
+
+
+
+
+ |
+
+
+ {{ __('Total Product Cost') }}
+
+
+
+
+ |
+
+
+ {{ __('Total Net Profit') }}
+
+
+
+
+ |
+
+
+ {{ __('Gross Margin') }}
+
+
+
+
+ |
+
|---|---|---|---|---|---|---|
| + | + | + | + | + | + | + |
|
+
+
+
+ {{ __('No product data matching search criteria') }} + |
+ ||||||
{{ __('Sales Quantity') }}
+ +{{ __('Total Sales') }}
+ +{{ __('Product Cost') }}
+ +{{ __('Total Net Profit') }}
+ +{{ __('No product data matching search criteria') }}
++ {{ __('Showing') }} + + {{ __('to') }} + + {{ __('of') }} + / + + {{ __('items') }} +
+