1. 【後端優化】重構 AnalysisController.php,以高效單次 SQL 統計計算銷量、銷額、總成本與淨利,套用 company_id 進行多租戶隔離,支援超級管理員跨租戶查詢,並解決 MySQL only_full_group_by 嚴格模式的 group by 語法相容問題。 2. 【前端介面】全新實作 product-reports.blade.php 極簡奢華風視圖,整合 Flatpickr 雙向日期區間選擇與「近 7 日、今日、本週、上週、本月、上月」快速切換快捷按鈕。 3. 【圖表自適應】整合 ApexCharts 銷售走勢曲線與圓餅圖,設計 PHP 端走勢數據日補零機制,並導入 ResizeObserver 進行圖表寬度百分之百響應式重繪。 4. 【尊榮數據導出】提供複製 (tsv)、CSV、Excel 與 PDF/列印的多維度導出引擎控制項。 5. 【高級前端分頁】對齊銷售紀錄規格,實作 Alpine.js 純前端高級分頁與 Show 10/25/50/100 筆數選擇器,並修復 select 動態 option 渲染時 ES6 template string 逸出問題。 6. 【細節齊平與遮擋修正】將日期 Input 寬度由 sm:w-56 拓寬至 sm:w-72,將日曆 Icon 左邊距從 pl-3.5 改進為 pl-5,Input 文字內左距調整為 pl-13,達到與 searchable-select 的 100% 齊平對齊,並配置跨卡片遞降 z-index 層級(z-40 -> z-30 -> z-20)徹底解決下拉選單在電腦版被下方走勢圖 Card 遮擋/截斷的 Bug。 7. 【多語系支援】於 zh_TW.json, en.json, ja.json 補強指標中英日翻譯。 8. 【選單整合】於 sidebar-menu.blade.php 的數據分析模組下正式啟用「商品報表」選單項目。
221 lines
8.4 KiB
PHP
221 lines
8.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
|
|
class AnalysisController extends Controller
|
|
{
|
|
// 零錢庫存分析
|
|
public function changeStock()
|
|
{
|
|
return view('admin.placeholder', [
|
|
'title' => '零錢庫存分析',
|
|
'description' => '機台零錢數量監測與分析',
|
|
]);
|
|
}
|
|
|
|
// 機台報表分析
|
|
public function machineReports()
|
|
{
|
|
return view('admin.placeholder', [
|
|
'title' => '機台報表分析',
|
|
'description' => '機台運營數據分析報表',
|
|
]);
|
|
}
|
|
|
|
// 商品報表分析
|
|
public function productReports(Request $request)
|
|
{
|
|
$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,
|
|
]);
|
|
}
|
|
|
|
// 互動問卷分析
|
|
public function surveyAnalysis()
|
|
{
|
|
return view('admin.placeholder', [
|
|
'title' => '互動問卷分析',
|
|
'description' => '問卷結果統計與分析',
|
|
]);
|
|
}
|
|
}
|