refactor(modular): 完成第二階段儀表板解耦與模型清理
This commit is contained in:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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', [
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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'),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -78,7 +78,33 @@ 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,
|
||||||
@@ -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', [
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user