refactor: 重構模組通訊與調整儀表板功能
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 57s

- 依循跨模組通訊規範,將 Sales 與 Production 模組中對 Inventory 的直接模型關聯改為透過 InventoryServiceInterface 取得
- 於 InventoryService 實作獲取最高庫存價值、即將過期商品等方法,供儀表板使用
- 確保所有跨模組調用皆採用手動水和(Manual Hydration)方式組合資料
- 移除本地已歸檔的 .agent 規範檔案
This commit is contained in:
2026-02-25 11:48:52 +08:00
parent ad91b08dbc
commit deef3baacc
22 changed files with 826 additions and 1396 deletions

View File

@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
use App\Modules\Sales\Contracts\SalesServiceInterface;
use App\Modules\Production\Contracts\ProductionServiceInterface;
use Inertia\Inertia;
use Illuminate\Http\Request;
@@ -13,13 +15,19 @@ class DashboardController extends Controller
{
protected $inventoryService;
protected $procurementService;
protected $salesService;
protected $productionService;
public function __construct(
InventoryServiceInterface $inventoryService,
ProcurementServiceInterface $procurementService
ProcurementServiceInterface $procurementService,
SalesServiceInterface $salesService,
ProductionServiceInterface $productionService
) {
$this->inventoryService = $inventoryService;
$this->procurementService = $procurementService;
$this->salesService = $salesService;
$this->productionService = $productionService;
}
public function index()
@@ -35,99 +43,70 @@ class DashboardController extends Controller
$procStats = $this->procurementService->getDashboardStats();
// 銷售統計 (本月營收)
$thisMonthRevenue = \App\Modules\Sales\Models\SalesImportItem::whereMonth('transaction_at', now()->month)
->whereYear('transaction_at', now()->year)
->sum('amount');
$thisMonthRevenue = $this->salesService->getThisMonthRevenue();
// 生產統計 (待核准工單)
$pendingProductionCount = \App\Modules\Production\Models\ProductionOrder::where('status', 'pending')->count();
$pendingProductionCount = $this->productionService->getPendingProductionCount();
// 生產狀態分佈
// 近30日銷售趨勢 (Area Chart)
$startDate = now()->subDays(29)->startOfDay();
$salesData = \App\Modules\Sales\Models\SalesImportItem::where('transaction_at', '>=', $startDate)
->selectRaw('DATE(transaction_at) as date, SUM(amount) as total')
->groupBy('date')
->orderBy('date')
->get()
->mapWithKeys(function ($item) {
return [$item->date => (int)$item->total];
});
$salesTrend = [];
for ($i = 0; $i < 30; $i++) {
$date = $startDate->copy()->addDays($i)->format('Y-m-d');
$salesTrend[] = [
'date' => $startDate->copy()->addDays($i)->format('m/d'),
'amount' => $salesData[$date] ?? 0,
];
}
$salesTrend = $this->salesService->getSalesTrend();
// 本月熱銷商品 Top 5 (Bar Chart)
$topSellingProducts = \App\Modules\Sales\Models\SalesImportItem::with('product')
->whereMonth('transaction_at', now()->month)
->whereYear('transaction_at', now()->year)
->select('product_code', 'product_id', \Illuminate\Support\Facades\DB::raw('SUM(amount) as total_amount'))
->groupBy('product_code', 'product_id')
->orderByDesc('total_amount')
->limit(5)
->get()
->map(function ($item) {
return [
'name' => $item->product ? $item->product->name : $item->product_code,
'amount' => (int)$item->total_amount,
];
});
$topSellingItems = $this->salesService->getTopSellingProducts();
$productIds = $topSellingItems->pluck('product_id')->filter()->unique()->toArray();
$productsMap = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
$topSellingProducts = $topSellingItems->map(function ($item) use ($productsMap) {
$product = $productsMap->get($item->product_id);
return [
'name' => $product ? $product->name : $item->product_code,
'amount' => (int)$item->total_amount,
];
});
// 庫存積壓排行 (Top Inventory Value)
$topInventoryValue = \App\Modules\Inventory\Models\Inventory::with('product')
->select('product_id', \Illuminate\Support\Facades\DB::raw('SUM(quantity * unit_cost) as total_value'))
->where('quantity', '>', 0)
->groupBy('product_id')
->orderByDesc('total_value')
->limit(5)
->get()
->map(function ($item) {
return [
'name' => $item->product ? $item->product->name : 'Unknown Product',
'code' => $item->product ? $item->product->code : '',
'value' => (int)$item->total_value,
];
});
$topInventoryValueItems = $this->inventoryService->getTopInventoryValue();
$invProductIds = $topInventoryValueItems->pluck('product_id')->filter()->unique()->toArray();
$invProductsMap = $this->inventoryService->getProductsByIds($invProductIds)->keyBy('id');
$topInventoryValue = $topInventoryValueItems->map(function ($item) use ($invProductsMap) {
$product = $invProductsMap->get($item->product_id);
return [
'name' => $product ? $product->name : 'Unknown Product',
'code' => $product ? $product->code : '',
'value' => (int)$item->total_value,
];
});
// 熱銷數量排行 (Top Selling by Quantity)
$topSellingByQuantity = \App\Modules\Sales\Models\SalesImportItem::with('product')
->whereMonth('transaction_at', now()->month)
->whereYear('transaction_at', now()->year)
->select('product_code', 'product_id', \Illuminate\Support\Facades\DB::raw('SUM(quantity) as total_quantity'))
->groupBy('product_code', 'product_id')
->orderByDesc('total_quantity')
->limit(5)
->get()
->map(function ($item) {
return [
'name' => $item->product ? $item->product->name : $item->product_code,
'code' => $item->product_code,
'value' => (int)$item->total_quantity,
];
});
$topSellingQtyItems = $this->salesService->getTopSellingByQuantity();
$qtyProductIds = $topSellingQtyItems->pluck('product_id')->filter()->unique()->toArray();
$qtyProductsMap = $this->inventoryService->getProductsByIds($qtyProductIds)->keyBy('id');
$topSellingByQuantity = $topSellingQtyItems->map(function ($item) use ($qtyProductsMap) {
$product = $qtyProductsMap->get($item->product_id);
return [
'name' => $product ? $product->name : $item->product_code,
'code' => $item->product_code,
'value' => (int)$item->total_quantity,
];
});
// 即將過期商品 (Expiring Soon)
$expiringSoon = \App\Modules\Inventory\Models\Inventory::with('product')
->where('quantity', '>', 0)
->whereNotNull('expiry_date')
->where('expiry_date', '>=', now()) // 只顯示未過期但即將過期的
->orderBy('expiry_date', 'asc')
->limit(5)
->get()
->map(function ($item) {
return [
'name' => $item->product ? $item->product->name : 'Unknown Product',
'batch_number' => $item->batch_number,
'expiry_date' => $item->expiry_date->format('Y-m-d'),
'quantity' => (int)$item->quantity,
];
});
$expiringItems = $this->inventoryService->getExpiringSoon();
$expiringProductIds = $expiringItems->pluck('product_id')->filter()->unique()->toArray();
$expiringProductsMap = $this->inventoryService->getProductsByIds($expiringProductIds)->keyBy('id');
$expiringSoon = $expiringItems->map(function ($item) use ($expiringProductsMap) {
$product = $expiringProductsMap->get($item->product_id);
return [
'name' => $product ? $product->name : 'Unknown Product',
'batch_number' => $item->batch_number,
'expiry_date' => $item->expiry_date->format('Y-m-d'),
'quantity' => (int)$item->quantity,
];
});
return Inertia::render('Dashboard', [
'stats' => [