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

@@ -0,0 +1,11 @@
<?php
namespace App\Modules\Sales\Contracts;
interface SalesServiceInterface
{
public function getThisMonthRevenue(): float;
public function getSalesTrend(int $days = 30): array;
public function getTopSellingProducts(int $limit = 5): \Illuminate\Support\Collection;
public function getTopSellingByQuantity(int $limit = 5): \Illuminate\Support\Collection;
}

View File

@@ -5,7 +5,7 @@ namespace App\Modules\Sales\Controllers;
use App\Http\Controllers\Controller;
use App\Modules\Sales\Models\SalesImportBatch;
use App\Modules\Sales\Imports\SalesImport;
use App\Modules\Inventory\Services\InventoryService; // Assuming this exists or we need to use ProductService
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Maatwebsite\Excel\Facades\Excel;
@@ -66,17 +66,32 @@ class SalesImportController extends Controller
$import->load(['items', 'importer']);
$perPage = $request->input('per_page', 10);
$paginatedItems = $import->items()->paginate($perPage)->withQueryString();
// Manual Hydration for Products and Warehouses
$inventoryService = app(InventoryServiceInterface::class);
$productIds = collect($paginatedItems->items())->pluck('product_id')->filter()->unique()->toArray();
$machineCodes = collect($paginatedItems->items())->pluck('machine_id')->filter()->unique()->toArray();
$products = $inventoryService->getProductsByIds($productIds)->keyBy('id');
$warehouses = $inventoryService->getWarehousesByCodes($machineCodes)->keyBy('code');
$paginatedItems->getCollection()->transform(function ($item) use ($products, $warehouses) {
$item->product = $products->get($item->product_id);
$item->warehouse = $warehouses->get($item->machine_id);
return $item;
});
return Inertia::render('Sales/Import/Show', [
'import' => $import,
'items' => $import->items()->with(['product', 'warehouse'])->paginate($perPage)->withQueryString(),
'items' => $paginatedItems,
'filters' => [
'per_page' => $perPage,
],
]);
}
public function confirm(SalesImportBatch $import, InventoryService $inventoryService)
public function confirm(SalesImportBatch $import, InventoryServiceInterface $inventoryService)
{
if ($import->status !== 'pending') {
return back()->with('error', '此批次無法確認。');
@@ -87,8 +102,8 @@ class SalesImportController extends Controller
$aggregatedDeductions = []; // Key: "warehouse_id:product_id:slot"
// Pre-load necessary warehouses for matching
$machineIds = $import->items->pluck('machine_id')->filter()->unique();
$warehouses = \App\Modules\Inventory\Models\Warehouse::whereIn('code', $machineIds)->get()->keyBy('code');
$machineIds = $import->items->pluck('machine_id')->filter()->unique()->toArray();
$warehouses = $inventoryService->getWarehousesByCodes($machineIds)->keyBy('code');
foreach ($import->items as $item) {
// Only process shipped items with a valid product

View File

@@ -4,7 +4,7 @@ namespace App\Modules\Sales\Imports;
use App\Modules\Sales\Models\SalesImportBatch;
use App\Modules\Sales\Models\SalesImportItem;
use App\Modules\Inventory\Models\Product;
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithStartRow;
@@ -19,7 +19,8 @@ class SalesImportSheet implements ToCollection, WithStartRow
{
$this->batch = $batch;
// Pre-load all products to minimize queries (keyed by code)
$this->products = Product::pluck('id', 'code'); // assumes code is unique
$inventoryService = app(InventoryServiceInterface::class);
$this->products = $inventoryService->getAllProducts()->pluck('id', 'code')->toArray(); // assumes code is unique
}
public function startRow(): int

View File

@@ -2,8 +2,7 @@
namespace App\Modules\Sales\Models;
use App\Modules\Inventory\Models\Product;
use App\Modules\Inventory\Models\Warehouse;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -38,14 +37,4 @@ class SalesImportItem extends Model
{
return $this->belongsTo(SalesImportBatch::class, 'batch_id');
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class, 'product_id');
}
public function warehouse(): BelongsTo
{
return $this->belongsTo(Warehouse::class, 'machine_id', 'code');
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Modules\Sales;
use Illuminate\Support\ServiceProvider;
use App\Modules\Sales\Contracts\SalesServiceInterface;
use App\Modules\Sales\Services\SalesService;
class SalesServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(SalesServiceInterface::class, SalesService::class);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Modules\Sales\Services;
use App\Modules\Sales\Contracts\SalesServiceInterface;
use App\Modules\Sales\Models\SalesImportItem;
use Illuminate\Support\Facades\DB;
class SalesService implements SalesServiceInterface
{
public function getThisMonthRevenue(): float
{
return (float) SalesImportItem::whereMonth('transaction_at', now()->month)
->whereYear('transaction_at', now()->year)
->sum('amount');
}
public function getSalesTrend(int $days = 30): array
{
$startDate = now()->subDays($days - 1)->startOfDay();
$salesData = 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 < $days; $i++) {
$date = $startDate->copy()->addDays($i)->format('Y-m-d');
$salesTrend[] = [
'date' => $startDate->copy()->addDays($i)->format('m/d'),
'amount' => $salesData[$date] ?? 0,
];
}
return $salesTrend;
}
public function getTopSellingProducts(int $limit = 5): \Illuminate\Support\Collection
{
return SalesImportItem::whereMonth('transaction_at', now()->month)
->whereYear('transaction_at', now()->year)
->select('product_code', 'product_id', DB::raw('SUM(amount) as total_amount'))
->groupBy('product_code', 'product_id')
->orderByDesc('total_amount')
->limit($limit)
->get();
}
public function getTopSellingByQuantity(int $limit = 5): \Illuminate\Support\Collection
{
return SalesImportItem::whereMonth('transaction_at', now()->month)
->whereYear('transaction_at', now()->year)
->select('product_code', 'product_id', DB::raw('SUM(quantity) as total_quantity'))
->groupBy('product_code', 'product_id')
->orderByDesc('total_quantity')
->limit($limit)
->get();
}
}