2026-01-06 16:17:12 +08:00
|
|
|
<?php
|
|
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
namespace App\Modules\Core\Controllers;
|
2026-01-06 16:17:12 +08:00
|
|
|
|
2026-01-26 10:37:47 +08:00
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
|
2026-01-27 08:59:45 +08:00
|
|
|
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
|
|
|
|
use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
|
2026-01-06 16:17:12 +08:00
|
|
|
use Inertia\Inertia;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
|
|
|
|
|
class DashboardController extends Controller
|
|
|
|
|
{
|
2026-01-27 08:59:45 +08:00
|
|
|
protected $inventoryService;
|
|
|
|
|
protected $procurementService;
|
|
|
|
|
|
|
|
|
|
public function __construct(
|
|
|
|
|
InventoryServiceInterface $inventoryService,
|
|
|
|
|
ProcurementServiceInterface $procurementService
|
|
|
|
|
) {
|
|
|
|
|
$this->inventoryService = $inventoryService;
|
|
|
|
|
$this->procurementService = $procurementService;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-06 16:17:12 +08:00
|
|
|
public function index()
|
|
|
|
|
{
|
2026-01-15 13:56:11 +08:00
|
|
|
$centralDomains = config('tenancy.central_domains', []);
|
|
|
|
|
|
2026-01-16 09:28:29 +08:00
|
|
|
$demoPort = config('tenancy.demo_tenant_port');
|
|
|
|
|
if ((!$demoPort || request()->getPort() != $demoPort) && in_array(request()->getHost(), $centralDomains)) {
|
2026-01-15 13:56:11 +08:00
|
|
|
return redirect()->route('landlord.dashboard');
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 08:59:45 +08:00
|
|
|
$invStats = $this->inventoryService->getDashboardStats();
|
2026-02-13 14:27:43 +08:00
|
|
|
$procStats = $this->procurementService->getDashboardStats();
|
|
|
|
|
|
|
|
|
|
// 銷售統計 (本月營收)
|
|
|
|
|
$thisMonthRevenue = \App\Modules\Sales\Models\SalesImportItem::whereMonth('transaction_at', now()->month)
|
|
|
|
|
->whereYear('transaction_at', now()->year)
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
// 生產統計 (待核准工單)
|
|
|
|
|
$pendingProductionCount = \App\Modules\Production\Models\ProductionOrder::where('status', 'pending')->count();
|
|
|
|
|
|
|
|
|
|
// 生產狀態分佈
|
|
|
|
|
// 近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,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 本月熱銷商品 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,
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 庫存積壓排行 (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,
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 熱銷數量排行 (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,
|
|
|
|
|
];
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 即將過期商品 (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,
|
|
|
|
|
];
|
|
|
|
|
});
|
2026-01-06 16:17:12 +08:00
|
|
|
|
|
|
|
|
return Inertia::render('Dashboard', [
|
2026-02-10 10:47:31 +08:00
|
|
|
'stats' => [
|
|
|
|
|
'totalItems' => $invStats['productsCount'],
|
|
|
|
|
'lowStockCount' => $invStats['lowStockCount'],
|
|
|
|
|
'negativeCount' => $invStats['negativeCount'] ?? 0,
|
|
|
|
|
'expiringCount' => $invStats['expiringCount'] ?? 0,
|
2026-02-13 14:27:43 +08:00
|
|
|
'totalInventoryValue' => $invStats['totalInventoryValue'] ?? 0,
|
|
|
|
|
'thisMonthRevenue' => $thisMonthRevenue,
|
|
|
|
|
'pendingOrdersCount' => $procStats['pendingOrdersCount'] ?? 0,
|
|
|
|
|
'pendingTransferCount' => $invStats['pendingTransferCount'] ?? 0,
|
|
|
|
|
'pendingProductionCount' => $pendingProductionCount,
|
|
|
|
|
'todoCount' => ($procStats['pendingOrdersCount'] ?? 0) + ($invStats['pendingTransferCount'] ?? 0) + $pendingProductionCount,
|
|
|
|
|
'salesTrend' => $salesTrend,
|
|
|
|
|
'topSellingProducts' => $topSellingProducts,
|
|
|
|
|
'topInventoryValue' => $topInventoryValue,
|
|
|
|
|
'topSellingByQuantity' => $topSellingByQuantity,
|
|
|
|
|
'expiringSoon' => $expiringSoon,
|
2026-02-10 10:47:31 +08:00
|
|
|
],
|
2026-01-06 16:17:12 +08:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 10:47:31 +08:00
|
|
|
|