refactor(modular): 完成第二階段儀表板解耦與模型清理
All checks were successful
Koori-ERP-Deploy-System / deploy-demo (push) Successful in 1m1s
Koori-ERP-Deploy-System / deploy-production (push) Has been skipped

This commit is contained in:
2026-01-27 08:59:45 +08:00
parent ac6a81b3d2
commit 0e51992cb4
13 changed files with 140 additions and 52 deletions

View File

@@ -28,4 +28,11 @@ interface CoreServiceInterface
* @return Collection * @return Collection
*/ */
public function getAllUsers(): Collection; public function getAllUsers(): Collection;
/**
* Get the system user or create one if not exists.
*
* @return object
*/
public function ensureSystemUserExists();
} }

View File

@@ -4,18 +4,24 @@ namespace App\Modules\Core\Controllers;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Modules\Inventory\Models\Product; use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use App\Modules\Procurement\Models\Vendor; use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
use App\Modules\Procurement\Models\PurchaseOrder;
use App\Modules\Inventory\Models\Warehouse;
use App\Modules\Inventory\Models\Inventory;
use App\Modules\Inventory\Models\WarehouseProductSafetyStock;
use Inertia\Inertia; use Inertia\Inertia;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class DashboardController extends Controller class DashboardController extends Controller
{ {
protected $inventoryService;
protected $procurementService;
public function __construct(
InventoryServiceInterface $inventoryService,
ProcurementServiceInterface $procurementService
) {
$this->inventoryService = $inventoryService;
$this->procurementService = $procurementService;
}
public function index() public function index()
{ {
$centralDomains = config('tenancy.central_domains', []); $centralDomains = config('tenancy.central_domains', []);
@@ -25,25 +31,17 @@ class DashboardController extends Controller
return redirect()->route('landlord.dashboard'); return redirect()->route('landlord.dashboard');
} }
// 計算低庫存數量:各商品在各倉庫的總量 < 安全庫存 $invStats = $this->inventoryService->getDashboardStats();
$lowStockCount = DB::table('warehouse_product_safety_stocks as ss') $procStats = $this->procurementService->getDashboardStats();
->join(DB::raw('(SELECT warehouse_id, product_id, SUM(quantity) as total_qty FROM inventories WHERE deleted_at IS NULL GROUP BY warehouse_id, product_id) as inv'),
function ($join) {
$join->on('ss.warehouse_id', '=', 'inv.warehouse_id')
->on('ss.product_id', '=', 'inv.product_id');
})
->whereRaw('inv.total_qty <= ss.safety_stock')
->count();
$stats = [ $stats = [
'productsCount' => Product::count(), 'productsCount' => $invStats['productsCount'],
'vendorsCount' => Vendor::count(), 'vendorsCount' => $procStats['vendorsCount'],
'purchaseOrdersCount' => PurchaseOrder::count(), 'purchaseOrdersCount' => $procStats['purchaseOrdersCount'],
'warehousesCount' => Warehouse::count(), 'warehousesCount' => $invStats['warehousesCount'],
'totalInventoryValue' => Inventory::join('products', 'inventories.product_id', '=', 'products.id') 'totalInventoryValue' => $invStats['totalInventoryQuantity'], // 原本前端命名是 totalInventoryValue 但實作是 Quantity暫且保留欄位名以不破壞前端
->sum('inventories.quantity'), 'pendingOrdersCount' => $procStats['pendingOrdersCount'],
'pendingOrdersCount' => PurchaseOrder::where('status', 'pending')->count(), 'lowStockCount' => $invStats['lowStockCount'],
'lowStockCount' => $lowStockCount,
]; ];
return Inertia::render('Dashboard', [ return Inertia::render('Dashboard', [

View File

@@ -39,4 +39,17 @@ class CoreService implements CoreServiceInterface
{ {
return User::all(); return User::all();
} }
public function ensureSystemUserExists()
{
$user = User::first();
if (!$user) {
$user = User::create([
'name' => '系統管理員',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
]);
}
return $user;
}
} }

View File

@@ -40,6 +40,14 @@ interface InventoryServiceInterface
*/ */
public function getProductsByIds(array $ids); public function getProductsByIds(array $ids);
/**
* Search products by name.
*
* @param string $name
* @return \Illuminate\Support\Collection
*/
public function getProductsByName(string $name);
/** /**
* Get a specific product by ID. * Get a specific product by ID.
* *
@@ -97,4 +105,11 @@ interface InventoryServiceInterface
* @return void * @return void
*/ */
public function decreaseInventoryQuantity(int $inventoryId, float $quantity, ?string $reason = null, ?string $referenceType = null, $referenceId = null); public function decreaseInventoryQuantity(int $inventoryId, float $quantity, ?string $reason = null, ?string $referenceType = null, $referenceId = null);
/**
* Get statistics for the dashboard.
*
* @return array
*/
public function getDashboardStats(): array;
} }

View File

@@ -40,6 +40,11 @@ class InventoryService implements InventoryServiceInterface
return Product::whereIn('id', $ids)->get(); return Product::whereIn('id', $ids)->get();
} }
public function getProductsByName(string $name)
{
return Product::where('name', 'like', "%{$name}%")->get();
}
public function getWarehouse(int $id) public function getWarehouse(int $id)
{ {
return Warehouse::find($id); return Warehouse::find($id);
@@ -182,4 +187,24 @@ class InventoryService implements InventoryServiceInterface
]); ]);
}); });
} }
public function getDashboardStats(): array
{
// 庫存總表 join 安全庫存表,計算低庫存
$lowStockCount = DB::table('warehouse_product_safety_stocks as ss')
->join(DB::raw('(SELECT warehouse_id, product_id, SUM(quantity) as total_qty FROM inventories WHERE deleted_at IS NULL GROUP BY warehouse_id, product_id) as inv'),
function ($join) {
$join->on('ss.warehouse_id', '=', 'inv.warehouse_id')
->on('ss.product_id', '=', 'inv.product_id');
})
->whereRaw('inv.total_qty <= ss.safety_stock')
->count();
return [
'productsCount' => Product::count(),
'warehousesCount' => Warehouse::count(),
'lowStockCount' => $lowStockCount,
'totalInventoryQuantity' => Inventory::sum('quantity'),
];
}
} }

View File

@@ -24,4 +24,11 @@ interface ProcurementServiceInterface
* @return Collection * @return Collection
*/ */
public function getPurchaseOrdersByIds(array $ids, array $with = []): Collection; public function getPurchaseOrdersByIds(array $ids, array $with = []): Collection;
/**
* Get statistics for the dashboard.
*
* @return array
*/
public function getDashboardStats(): array;
} }

View File

@@ -215,15 +215,7 @@ class PurchaseOrderController extends Controller
// 確保有一個有效的使用者 ID // 確保有一個有效的使用者 ID
$userId = auth()->id(); $userId = auth()->id();
if (!$userId) { if (!$userId) {
$user = \App\Modules\Core\Models\User::first(); $user = $this->coreService->ensureSystemUserExists(); $userId = $user->id;
if (!$user) {
$user = \App\Modules\Core\Models\User::create([
'name' => '系統管理員',
'email' => 'admin@example.com',
'password' => bcrypt('password'),
]);
}
$userId = $user->id;
} }
$order = PurchaseOrder::create([ $order = PurchaseOrder::create([

View File

@@ -78,8 +78,34 @@ class VendorController extends Controller
*/ */
public function show(Vendor $vendor): Response public function show(Vendor $vendor): Response
{ {
$vendor->load(['products.baseUnit', 'products.largeUnit']); // $vendor->load(['products.baseUnit', 'products.largeUnit']); // REMOVED: Cross-module relation
// 1. 獲取關聯的 Product IDs 與 Pivot Data
$pivots = \Illuminate\Support\Facades\DB::table('product_vendor')
->where('vendor_id', $vendor->id)
->get();
$productIds = $pivots->pluck('product_id')->toArray();
// 2. 透過 Service 獲取 Products
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
$supplyProducts = $pivots->map(function ($pivot) use ($products) {
$product = $products->get($pivot->product_id);
if (!$product) return null;
return (object) [
'id' => (string) $pivot->id,
'productId' => (string) $product->id,
'productName' => $product->name,
'unit' => $product->baseUnit?->name ?? 'N/A',
'baseUnit' => $product->baseUnit?->name,
'largeUnit' => $product->largeUnit?->name,
'conversionRate' => (float) $product->conversion_rate,
'lastPrice' => (float) $pivot->last_price,
];
})->filter()->values();
$formattedVendor = (object) [ $formattedVendor = (object) [
'id' => (string) $vendor->id, 'id' => (string) $vendor->id,
'code' => $vendor->code, 'code' => $vendor->code,
@@ -93,16 +119,7 @@ class VendorController extends Controller
'email' => $vendor->email, 'email' => $vendor->email,
'address' => $vendor->address, 'address' => $vendor->address,
'remark' => $vendor->remark, 'remark' => $vendor->remark,
'supplyProducts' => $vendor->products->map(fn($p) => (object) [ 'supplyProducts' => $supplyProducts,
'id' => (string) $p->pivot->id,
'productId' => (string) $p->id,
'productName' => $p->name,
'unit' => $p->baseUnit?->name ?? 'N/A',
'baseUnit' => $p->baseUnit?->name,
'largeUnit' => $p->largeUnit?->name,
'conversionRate' => (float) $p->conversion_rate,
'lastPrice' => (float) $p->pivot->last_price,
]),
]; ];
return Inertia::render('Vendor/Show', [ return Inertia::render('Vendor/Show', [

View File

@@ -4,7 +4,7 @@ namespace App\Modules\Procurement\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Modules\Inventory\Models\Product;
class PurchaseOrderItem extends Model class PurchaseOrderItem extends Model
{ {

View File

@@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Spatie\Activitylog\Traits\LogsActivity; use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Activitylog\LogOptions; use Spatie\Activitylog\LogOptions;
use App\Modules\Inventory\Models\Product;
class Vendor extends Model class Vendor extends Model
{ {

View File

@@ -20,4 +20,13 @@ class ProcurementService implements ProcurementServiceInterface
{ {
return PurchaseOrder::whereIn('id', $ids)->with($with)->get(); return PurchaseOrder::whereIn('id', $ids)->with($with)->get();
} }
public function getDashboardStats(): array
{
return [
'vendorsCount' => \App\Modules\Procurement\Models\Vendor::count(),
'purchaseOrdersCount' => PurchaseOrder::count(),
'pendingOrdersCount' => PurchaseOrder::where('status', 'pending')->count(),
];
}
} }

View File

@@ -7,7 +7,7 @@ use App\Http\Controllers\Controller;
use App\Modules\Production\Models\ProductionOrder; use App\Modules\Production\Models\ProductionOrder;
use App\Modules\Production\Models\ProductionOrderItem; use App\Modules\Production\Models\ProductionOrderItem;
use App\Modules\Inventory\Contracts\InventoryServiceInterface; use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use App\Modules\Core\Models\User; use App\Modules\Core\Contracts\CoreServiceInterface;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Inertia\Inertia; use Inertia\Inertia;
@@ -16,10 +16,12 @@ use Inertia\Response;
class ProductionOrderController extends Controller class ProductionOrderController extends Controller
{ {
protected $inventoryService; protected $inventoryService;
protected $coreService;
public function __construct(InventoryServiceInterface $inventoryService) public function __construct(InventoryServiceInterface $inventoryService, CoreServiceInterface $coreService)
{ {
$this->inventoryService = $inventoryService; $this->inventoryService = $inventoryService;
$this->coreService = $coreService;
} }
/** /**
@@ -38,7 +40,10 @@ class ProductionOrderController extends Controller
$q->where('code', 'like', "%{$search}%") $q->where('code', 'like', "%{$search}%")
->orWhere('output_batch_number', 'like', "%{$search}%"); ->orWhere('output_batch_number', 'like', "%{$search}%");
// 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs // 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs
$productIds = \App\Modules\Inventory\Models\Product::where('name', 'like', "%{$search}%")->pluck('id'); $q->where('code', 'like', "%{$search}%")
->orWhere('output_batch_number', 'like', "%{$search}%");
// 若要搜尋產品名稱,現在需先從 Inventory 查出 IDs
$productIds = $this->inventoryService->getProductsByName($search)->pluck('id');
$q->orWhereIn('product_id', $productIds); $q->orWhereIn('product_id', $productIds);
}); });
} }
@@ -62,7 +67,7 @@ class ProductionOrderController extends Controller
$products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id'); $products = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
$warehouses = $this->inventoryService->getAllWarehouses()->whereIn('id', $warehouseIds)->keyBy('id'); $warehouses = $this->inventoryService->getAllWarehouses()->whereIn('id', $warehouseIds)->keyBy('id');
$users = User::whereIn('id', $userIds)->get()->keyBy('id'); // Core 模組暫由 Model 直接獲取 $users = $this->coreService->getUsersByIds($userIds)->keyBy('id');
$productionOrders->getCollection()->transform(function ($order) use ($products, $warehouses, $users) { $productionOrders->getCollection()->transform(function ($order) use ($products, $warehouses, $users) {
$order->product = $products->get($order->product_id); $order->product = $products->get($order->product_id);
@@ -195,7 +200,7 @@ class ProductionOrderController extends Controller
$productionOrder->product->base_unit = $this->inventoryService->getUnits()->where('id', $productionOrder->product->base_unit_id)->first(); $productionOrder->product->base_unit = $this->inventoryService->getUnits()->where('id', $productionOrder->product->base_unit_id)->first();
} }
$productionOrder->warehouse = $this->inventoryService->getWarehouse($productionOrder->warehouse_id); $productionOrder->warehouse = $this->inventoryService->getWarehouse($productionOrder->warehouse_id);
$productionOrder->user = User::find($productionOrder->user_id); $productionOrder->user = $this->coreService->getUser($productionOrder->user_id);
// 手動水和明細資料 // 手動水和明細資料
$items = $productionOrder->items; $items = $productionOrder->items;

View File

@@ -4,7 +4,7 @@ namespace App\Modules\Production\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use App\Modules\Inventory\Models\Product;
class ProductionOrderItem extends Model class ProductionOrderItem extends Model
{ {