From 0e51992cb4d6414cacedd3ea4ad8387c417fdaae Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 27 Jan 2026 08:59:45 +0800 Subject: [PATCH] =?UTF-8?q?refactor(modular):=20=E5=AE=8C=E6=88=90?= =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E9=9A=8E=E6=AE=B5=E5=84=80=E8=A1=A8=E6=9D=BF?= =?UTF-8?q?=E8=A7=A3=E8=80=A6=E8=88=87=E6=A8=A1=E5=9E=8B=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Core/Contracts/CoreServiceInterface.php | 7 +++ .../Core/Controllers/DashboardController.php | 46 +++++++++---------- app/Modules/Core/Services/CoreService.php | 13 ++++++ .../Contracts/InventoryServiceInterface.php | 15 ++++++ .../Inventory/Services/InventoryService.php | 25 ++++++++++ .../Contracts/ProcurementServiceInterface.php | 7 +++ .../Controllers/PurchaseOrderController.php | 10 +--- .../Controllers/VendorController.php | 39 +++++++++++----- .../Procurement/Models/PurchaseOrderItem.php | 2 +- app/Modules/Procurement/Models/Vendor.php | 2 +- .../Services/ProcurementService.php | 9 ++++ .../Controllers/ProductionOrderController.php | 15 ++++-- .../Production/Models/ProductionOrderItem.php | 2 +- 13 files changed, 140 insertions(+), 52 deletions(-) diff --git a/app/Modules/Core/Contracts/CoreServiceInterface.php b/app/Modules/Core/Contracts/CoreServiceInterface.php index d57b3aa..18783ab 100644 --- a/app/Modules/Core/Contracts/CoreServiceInterface.php +++ b/app/Modules/Core/Contracts/CoreServiceInterface.php @@ -28,4 +28,11 @@ interface CoreServiceInterface * @return Collection */ public function getAllUsers(): Collection; + + /** + * Get the system user or create one if not exists. + * + * @return object + */ + public function ensureSystemUserExists(); } diff --git a/app/Modules/Core/Controllers/DashboardController.php b/app/Modules/Core/Controllers/DashboardController.php index ec95777..65febf2 100644 --- a/app/Modules/Core/Controllers/DashboardController.php +++ b/app/Modules/Core/Controllers/DashboardController.php @@ -4,18 +4,24 @@ namespace App\Modules\Core\Controllers; use App\Http\Controllers\Controller; -use App\Modules\Inventory\Models\Product; -use App\Modules\Procurement\Models\Vendor; -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 App\Modules\Inventory\Contracts\InventoryServiceInterface; +use App\Modules\Procurement\Contracts\ProcurementServiceInterface; use Inertia\Inertia; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; class DashboardController extends Controller { + protected $inventoryService; + protected $procurementService; + + public function __construct( + InventoryServiceInterface $inventoryService, + ProcurementServiceInterface $procurementService + ) { + $this->inventoryService = $inventoryService; + $this->procurementService = $procurementService; + } + public function index() { $centralDomains = config('tenancy.central_domains', []); @@ -25,25 +31,17 @@ class DashboardController extends Controller return redirect()->route('landlord.dashboard'); } - // 計算低庫存數量:各商品在各倉庫的總量 < 安全庫存 - $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(); + $invStats = $this->inventoryService->getDashboardStats(); + $procStats = $this->procurementService->getDashboardStats(); $stats = [ - 'productsCount' => Product::count(), - 'vendorsCount' => Vendor::count(), - 'purchaseOrdersCount' => PurchaseOrder::count(), - 'warehousesCount' => Warehouse::count(), - 'totalInventoryValue' => Inventory::join('products', 'inventories.product_id', '=', 'products.id') - ->sum('inventories.quantity'), - 'pendingOrdersCount' => PurchaseOrder::where('status', 'pending')->count(), - 'lowStockCount' => $lowStockCount, + 'productsCount' => $invStats['productsCount'], + 'vendorsCount' => $procStats['vendorsCount'], + 'purchaseOrdersCount' => $procStats['purchaseOrdersCount'], + 'warehousesCount' => $invStats['warehousesCount'], + 'totalInventoryValue' => $invStats['totalInventoryQuantity'], // 原本前端命名是 totalInventoryValue 但實作是 Quantity,暫且保留欄位名以不破壞前端 + 'pendingOrdersCount' => $procStats['pendingOrdersCount'], + 'lowStockCount' => $invStats['lowStockCount'], ]; return Inertia::render('Dashboard', [ diff --git a/app/Modules/Core/Services/CoreService.php b/app/Modules/Core/Services/CoreService.php index 2c59c77..7eaa72f 100644 --- a/app/Modules/Core/Services/CoreService.php +++ b/app/Modules/Core/Services/CoreService.php @@ -39,4 +39,17 @@ class CoreService implements CoreServiceInterface { return User::all(); } + + public function ensureSystemUserExists() + { + $user = User::first(); + if (!$user) { + $user = User::create([ + 'name' => '系統管理員', + 'email' => 'admin@example.com', + 'password' => bcrypt('password'), + ]); + } + return $user; + } } diff --git a/app/Modules/Inventory/Contracts/InventoryServiceInterface.php b/app/Modules/Inventory/Contracts/InventoryServiceInterface.php index 7f057c3..d72538a 100644 --- a/app/Modules/Inventory/Contracts/InventoryServiceInterface.php +++ b/app/Modules/Inventory/Contracts/InventoryServiceInterface.php @@ -40,6 +40,14 @@ interface InventoryServiceInterface */ 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. * @@ -97,4 +105,11 @@ interface InventoryServiceInterface * @return void */ 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; } \ No newline at end of file diff --git a/app/Modules/Inventory/Services/InventoryService.php b/app/Modules/Inventory/Services/InventoryService.php index 2c0f129..d1fcd4c 100644 --- a/app/Modules/Inventory/Services/InventoryService.php +++ b/app/Modules/Inventory/Services/InventoryService.php @@ -40,6 +40,11 @@ class InventoryService implements InventoryServiceInterface return Product::whereIn('id', $ids)->get(); } + public function getProductsByName(string $name) + { + return Product::where('name', 'like', "%{$name}%")->get(); + } + public function getWarehouse(int $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'), + ]; + } } diff --git a/app/Modules/Procurement/Contracts/ProcurementServiceInterface.php b/app/Modules/Procurement/Contracts/ProcurementServiceInterface.php index df4f4a1..9621c44 100644 --- a/app/Modules/Procurement/Contracts/ProcurementServiceInterface.php +++ b/app/Modules/Procurement/Contracts/ProcurementServiceInterface.php @@ -24,4 +24,11 @@ interface ProcurementServiceInterface * @return Collection */ public function getPurchaseOrdersByIds(array $ids, array $with = []): Collection; + + /** + * Get statistics for the dashboard. + * + * @return array + */ + public function getDashboardStats(): array; } diff --git a/app/Modules/Procurement/Controllers/PurchaseOrderController.php b/app/Modules/Procurement/Controllers/PurchaseOrderController.php index 91f8a01..d5774a4 100644 --- a/app/Modules/Procurement/Controllers/PurchaseOrderController.php +++ b/app/Modules/Procurement/Controllers/PurchaseOrderController.php @@ -215,15 +215,7 @@ class PurchaseOrderController extends Controller // 確保有一個有效的使用者 ID $userId = auth()->id(); if (!$userId) { - $user = \App\Modules\Core\Models\User::first(); - if (!$user) { - $user = \App\Modules\Core\Models\User::create([ - 'name' => '系統管理員', - 'email' => 'admin@example.com', - 'password' => bcrypt('password'), - ]); - } - $userId = $user->id; + $user = $this->coreService->ensureSystemUserExists(); $userId = $user->id; } $order = PurchaseOrder::create([ diff --git a/app/Modules/Procurement/Controllers/VendorController.php b/app/Modules/Procurement/Controllers/VendorController.php index 9dc4cf2..a5f5dc7 100644 --- a/app/Modules/Procurement/Controllers/VendorController.php +++ b/app/Modules/Procurement/Controllers/VendorController.php @@ -78,8 +78,34 @@ class VendorController extends Controller */ 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) [ 'id' => (string) $vendor->id, 'code' => $vendor->code, @@ -93,16 +119,7 @@ class VendorController extends Controller 'email' => $vendor->email, 'address' => $vendor->address, 'remark' => $vendor->remark, - 'supplyProducts' => $vendor->products->map(fn($p) => (object) [ - '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, - ]), + 'supplyProducts' => $supplyProducts, ]; return Inertia::render('Vendor/Show', [ diff --git a/app/Modules/Procurement/Models/PurchaseOrderItem.php b/app/Modules/Procurement/Models/PurchaseOrderItem.php index 92aee66..d8ab795 100644 --- a/app/Modules/Procurement/Models/PurchaseOrderItem.php +++ b/app/Modules/Procurement/Models/PurchaseOrderItem.php @@ -4,7 +4,7 @@ namespace App\Modules\Procurement\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use App\Modules\Inventory\Models\Product; + class PurchaseOrderItem extends Model { diff --git a/app/Modules/Procurement/Models/Vendor.php b/app/Modules/Procurement/Models/Vendor.php index 06c6375..3f6f5e2 100644 --- a/app/Modules/Procurement/Models/Vendor.php +++ b/app/Modules/Procurement/Models/Vendor.php @@ -6,7 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Spatie\Activitylog\Traits\LogsActivity; use Spatie\Activitylog\LogOptions; -use App\Modules\Inventory\Models\Product; + class Vendor extends Model { diff --git a/app/Modules/Procurement/Services/ProcurementService.php b/app/Modules/Procurement/Services/ProcurementService.php index 44014cf..a751f8a 100644 --- a/app/Modules/Procurement/Services/ProcurementService.php +++ b/app/Modules/Procurement/Services/ProcurementService.php @@ -20,4 +20,13 @@ class ProcurementService implements ProcurementServiceInterface { 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(), + ]; + } } diff --git a/app/Modules/Production/Controllers/ProductionOrderController.php b/app/Modules/Production/Controllers/ProductionOrderController.php index 0e29555..862523a 100644 --- a/app/Modules/Production/Controllers/ProductionOrderController.php +++ b/app/Modules/Production/Controllers/ProductionOrderController.php @@ -7,7 +7,7 @@ use App\Http\Controllers\Controller; use App\Modules\Production\Models\ProductionOrder; use App\Modules\Production\Models\ProductionOrderItem; use App\Modules\Inventory\Contracts\InventoryServiceInterface; -use App\Modules\Core\Models\User; +use App\Modules\Core\Contracts\CoreServiceInterface; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Inertia\Inertia; @@ -16,10 +16,12 @@ use Inertia\Response; class ProductionOrderController extends Controller { protected $inventoryService; + protected $coreService; - public function __construct(InventoryServiceInterface $inventoryService) + public function __construct(InventoryServiceInterface $inventoryService, CoreServiceInterface $coreService) { $this->inventoryService = $inventoryService; + $this->coreService = $coreService; } /** @@ -38,7 +40,10 @@ class ProductionOrderController extends Controller $q->where('code', 'like', "%{$search}%") ->orWhere('output_batch_number', 'like', "%{$search}%"); // 若要搜尋產品名稱,現在需先從 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); }); } @@ -62,7 +67,7 @@ class ProductionOrderController extends Controller $products = $this->inventoryService->getProductsByIds($productIds)->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) { $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->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; diff --git a/app/Modules/Production/Models/ProductionOrderItem.php b/app/Modules/Production/Models/ProductionOrderItem.php index e0cbc91..84b21fa 100644 --- a/app/Modules/Production/Models/ProductionOrderItem.php +++ b/app/Modules/Production/Models/ProductionOrderItem.php @@ -4,7 +4,7 @@ namespace App\Modules\Production\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use App\Modules\Inventory\Models\Product; + class ProductionOrderItem extends Model {