refactor: 重構模組通訊與調整儀表板功能
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 57s
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:
@@ -6,6 +6,8 @@ use App\Http\Controllers\Controller;
|
||||
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
|
||||
use App\Modules\Sales\Contracts\SalesServiceInterface;
|
||||
use App\Modules\Production\Contracts\ProductionServiceInterface;
|
||||
use Inertia\Inertia;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -13,13 +15,19 @@ class DashboardController extends Controller
|
||||
{
|
||||
protected $inventoryService;
|
||||
protected $procurementService;
|
||||
protected $salesService;
|
||||
protected $productionService;
|
||||
|
||||
public function __construct(
|
||||
InventoryServiceInterface $inventoryService,
|
||||
ProcurementServiceInterface $procurementService
|
||||
ProcurementServiceInterface $procurementService,
|
||||
SalesServiceInterface $salesService,
|
||||
ProductionServiceInterface $productionService
|
||||
) {
|
||||
$this->inventoryService = $inventoryService;
|
||||
$this->procurementService = $procurementService;
|
||||
$this->salesService = $salesService;
|
||||
$this->productionService = $productionService;
|
||||
}
|
||||
|
||||
public function index()
|
||||
@@ -35,99 +43,70 @@ class DashboardController extends Controller
|
||||
$procStats = $this->procurementService->getDashboardStats();
|
||||
|
||||
// 銷售統計 (本月營收)
|
||||
$thisMonthRevenue = \App\Modules\Sales\Models\SalesImportItem::whereMonth('transaction_at', now()->month)
|
||||
->whereYear('transaction_at', now()->year)
|
||||
->sum('amount');
|
||||
$thisMonthRevenue = $this->salesService->getThisMonthRevenue();
|
||||
|
||||
// 生產統計 (待核准工單)
|
||||
$pendingProductionCount = \App\Modules\Production\Models\ProductionOrder::where('status', 'pending')->count();
|
||||
$pendingProductionCount = $this->productionService->getPendingProductionCount();
|
||||
|
||||
// 生產狀態分佈
|
||||
// 近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,
|
||||
];
|
||||
}
|
||||
$salesTrend = $this->salesService->getSalesTrend();
|
||||
|
||||
// 本月熱銷商品 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,
|
||||
];
|
||||
});
|
||||
$topSellingItems = $this->salesService->getTopSellingProducts();
|
||||
$productIds = $topSellingItems->pluck('product_id')->filter()->unique()->toArray();
|
||||
$productsMap = $this->inventoryService->getProductsByIds($productIds)->keyBy('id');
|
||||
|
||||
$topSellingProducts = $topSellingItems->map(function ($item) use ($productsMap) {
|
||||
$product = $productsMap->get($item->product_id);
|
||||
return [
|
||||
'name' => $product ? $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,
|
||||
];
|
||||
});
|
||||
$topInventoryValueItems = $this->inventoryService->getTopInventoryValue();
|
||||
$invProductIds = $topInventoryValueItems->pluck('product_id')->filter()->unique()->toArray();
|
||||
$invProductsMap = $this->inventoryService->getProductsByIds($invProductIds)->keyBy('id');
|
||||
|
||||
$topInventoryValue = $topInventoryValueItems->map(function ($item) use ($invProductsMap) {
|
||||
$product = $invProductsMap->get($item->product_id);
|
||||
return [
|
||||
'name' => $product ? $product->name : 'Unknown Product',
|
||||
'code' => $product ? $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,
|
||||
];
|
||||
});
|
||||
$topSellingQtyItems = $this->salesService->getTopSellingByQuantity();
|
||||
$qtyProductIds = $topSellingQtyItems->pluck('product_id')->filter()->unique()->toArray();
|
||||
$qtyProductsMap = $this->inventoryService->getProductsByIds($qtyProductIds)->keyBy('id');
|
||||
|
||||
$topSellingByQuantity = $topSellingQtyItems->map(function ($item) use ($qtyProductsMap) {
|
||||
$product = $qtyProductsMap->get($item->product_id);
|
||||
return [
|
||||
'name' => $product ? $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,
|
||||
];
|
||||
});
|
||||
$expiringItems = $this->inventoryService->getExpiringSoon();
|
||||
$expiringProductIds = $expiringItems->pluck('product_id')->filter()->unique()->toArray();
|
||||
$expiringProductsMap = $this->inventoryService->getProductsByIds($expiringProductIds)->keyBy('id');
|
||||
|
||||
$expiringSoon = $expiringItems->map(function ($item) use ($expiringProductsMap) {
|
||||
$product = $expiringProductsMap->get($item->product_id);
|
||||
return [
|
||||
'name' => $product ? $product->name : 'Unknown Product',
|
||||
'batch_number' => $item->batch_number,
|
||||
'expiry_date' => $item->expiry_date->format('Y-m-d'),
|
||||
'quantity' => (int)$item->quantity,
|
||||
];
|
||||
});
|
||||
|
||||
return Inertia::render('Dashboard', [
|
||||
'stats' => [
|
||||
|
||||
@@ -7,23 +7,41 @@ use App\Modules\Finance\Models\AccountPayable;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
||||
use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
|
||||
use App\Modules\Inventory\Contracts\InventoryServiceInterface;
|
||||
|
||||
class AccountPayableController extends Controller
|
||||
{
|
||||
protected $procurementService;
|
||||
protected $inventoryService;
|
||||
|
||||
public function __construct(
|
||||
ProcurementServiceInterface $procurementService,
|
||||
InventoryServiceInterface $inventoryService
|
||||
) {
|
||||
$this->procurementService = $procurementService;
|
||||
$this->inventoryService = $inventoryService;
|
||||
}
|
||||
/**
|
||||
* Display a listing of the resource.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = AccountPayable::with(['vendor', 'creator']);
|
||||
$query = AccountPayable::with(['creator']);
|
||||
|
||||
// 關鍵字搜尋 (單號、供應商名稱)
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('document_number', 'like', "%{$search}%")
|
||||
->orWhereHas('vendor', function ($q) use ($search) {
|
||||
$q->where('name', 'like', "%{$search}%");
|
||||
});
|
||||
|
||||
// 透過 ProcurementService 查詢符合關鍵字的 Vendor IDs
|
||||
$matchedVendors = $this->procurementService->searchVendors($search);
|
||||
$vendorIds = $matchedVendors->pluck('id')->toArray();
|
||||
|
||||
$query->where(function ($q) use ($search, $vendorIds) {
|
||||
$q->where('document_number', 'like', "%{$search}%");
|
||||
if (!empty($vendorIds)) {
|
||||
$q->orWhereIn('vendor_id', $vendorIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +66,16 @@ class AccountPayableController extends Controller
|
||||
$perPage = $request->input('per_page', 10);
|
||||
$payables = $query->latest()->paginate($perPage)->withQueryString();
|
||||
|
||||
$vendors = \App\Modules\Procurement\Models\Vendor::select('id', 'name')->get();
|
||||
// Manual Hydration for Vendors
|
||||
$allVendorIds = collect($payables->items())->pluck('vendor_id')->unique()->filter()->toArray();
|
||||
$vendorsMap = $this->procurementService->getVendorsByIds($allVendorIds)->keyBy('id');
|
||||
|
||||
$payables->getCollection()->transform(function ($item) use ($vendorsMap) {
|
||||
$item->vendor = $vendorsMap->get($item->vendor_id);
|
||||
return $item;
|
||||
});
|
||||
|
||||
$vendors = $this->procurementService->getAllVendors();
|
||||
|
||||
return Inertia::render('AccountPayable/Index', [
|
||||
'payables' => $payables,
|
||||
@@ -62,14 +89,19 @@ class AccountPayableController extends Controller
|
||||
*/
|
||||
public function show(AccountPayable $accountPayable)
|
||||
{
|
||||
$accountPayable->load(['vendor', 'creator']);
|
||||
$accountPayable->load(['creator']);
|
||||
|
||||
if ($accountPayable->vendor_id) {
|
||||
$accountPayable->vendor = $this->procurementService->getVendorsByIds([$accountPayable->vendor_id])->first();
|
||||
}
|
||||
|
||||
// 嘗試加載來源單據資訊 (目前支援 goods_receipt)
|
||||
$sourceDocumentCode = null;
|
||||
if ($accountPayable->source_document_type === 'goods_receipt') {
|
||||
$receipt = \App\Modules\Inventory\Models\GoodsReceipt::find($accountPayable->source_document_id);
|
||||
if ($receipt) {
|
||||
$sourceDocumentCode = $receipt->code;
|
||||
$receiptData = app(\App\Modules\Inventory\Contracts\GoodsReceiptServiceInterface::class)
|
||||
->getGoodsReceiptData($accountPayable->source_document_id);
|
||||
if ($receiptData) {
|
||||
$sourceDocumentCode = $receiptData['code'] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,14 +41,7 @@ class AccountPayable extends Model
|
||||
'paid_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* 關聯:供應商
|
||||
* @return BelongsTo
|
||||
*/
|
||||
public function vendor(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(\App\Modules\Procurement\Models\Vendor::class, 'vendor_id');
|
||||
}
|
||||
// vendor 關聯移至 service (跨模組)
|
||||
|
||||
/**
|
||||
* 關聯:建立者
|
||||
|
||||
@@ -40,6 +40,14 @@ interface InventoryServiceInterface
|
||||
*/
|
||||
public function getProductsByIds(array $ids);
|
||||
|
||||
/**
|
||||
* Get multiple warehouses by their codes.
|
||||
*
|
||||
* @param array $codes
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
public function getWarehousesByCodes(array $codes);
|
||||
|
||||
/**
|
||||
* Search products by name.
|
||||
*
|
||||
@@ -139,4 +147,14 @@ interface InventoryServiceInterface
|
||||
* @return object
|
||||
*/
|
||||
public function findOrCreateWarehouseByName(string $warehouseName);
|
||||
|
||||
/**
|
||||
* Get top inventory value for dashboard.
|
||||
*/
|
||||
public function getTopInventoryValue(int $limit = 5): \Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Get items expiring soon for dashboard.
|
||||
*/
|
||||
public function getExpiringSoon(int $limit = 5): \Illuminate\Support\Collection;
|
||||
}
|
||||
@@ -7,11 +7,10 @@ use App\Modules\Inventory\Services\GoodsReceiptService;
|
||||
use App\Modules\Inventory\Services\InventoryService;
|
||||
use App\Modules\Procurement\Contracts\ProcurementServiceInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Modules\Procurement\Models\Vendor;
|
||||
use App\Modules\Inventory\Services\DuplicateCheckService;
|
||||
use Inertia\Inertia;
|
||||
use App\Modules\Inventory\Models\GoodsReceipt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Modules\Inventory\Services\DuplicateCheckService;
|
||||
|
||||
class GoodsReceiptController extends Controller
|
||||
{
|
||||
|
||||
@@ -16,6 +16,26 @@ class InventoryService implements InventoryServiceInterface
|
||||
return Warehouse::all();
|
||||
}
|
||||
|
||||
public function getTopInventoryValue(int $limit = 5): \Illuminate\Support\Collection
|
||||
{
|
||||
return Inventory::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($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getExpiringSoon(int $limit = 5): \Illuminate\Support\Collection
|
||||
{
|
||||
return Inventory::where('quantity', '>', 0)
|
||||
->whereNotNull('expiry_date')
|
||||
->where('expiry_date', '>=', now()) // 只顯示未過期但即將過期的
|
||||
->orderBy('expiry_date', 'asc')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
public function getAllProducts()
|
||||
{
|
||||
return Product::with(['baseUnit', 'largeUnit'])->get();
|
||||
@@ -41,6 +61,11 @@ class InventoryService implements InventoryServiceInterface
|
||||
return Product::whereIn('id', $ids)->with(['baseUnit', 'largeUnit'])->get();
|
||||
}
|
||||
|
||||
public function getWarehousesByCodes(array $codes)
|
||||
{
|
||||
return Warehouse::whereIn('code', $codes)->get();
|
||||
}
|
||||
|
||||
public function getProductsByName(string $name)
|
||||
{
|
||||
return Product::where('name', 'like', "%{$name}%")->with(['baseUnit', 'largeUnit'])->get();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Production\Contracts;
|
||||
|
||||
interface ProductionServiceInterface
|
||||
{
|
||||
public function getPendingProductionCount(): int;
|
||||
}
|
||||
@@ -27,13 +27,5 @@ class RecipeItem extends Model
|
||||
return $this->belongsTo(Recipe::class);
|
||||
}
|
||||
|
||||
public function product()
|
||||
{
|
||||
return $this->belongsTo(\App\Modules\Inventory\Models\Product::class);
|
||||
}
|
||||
|
||||
public function unit()
|
||||
{
|
||||
return $this->belongsTo(\App\Modules\Inventory\Models\Unit::class);
|
||||
}
|
||||
// product 和 unit 關聯移至 service (跨模組)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@ class ProductionServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
$this->app->bind(
|
||||
\App\Modules\Production\Contracts\ProductionServiceInterface::class,
|
||||
\App\Modules\Production\Services\ProductionService::class
|
||||
);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
|
||||
14
app/Modules/Production/Services/ProductionService.php
Normal file
14
app/Modules/Production/Services/ProductionService.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Production\Services;
|
||||
|
||||
use App\Modules\Production\Contracts\ProductionServiceInterface;
|
||||
use App\Modules\Production\Models\ProductionOrder;
|
||||
|
||||
class ProductionService implements ProductionServiceInterface
|
||||
{
|
||||
public function getPendingProductionCount(): int
|
||||
{
|
||||
return ProductionOrder::where('status', 'pending')->count();
|
||||
}
|
||||
}
|
||||
11
app/Modules/Sales/Contracts/SalesServiceInterface.php
Normal file
11
app/Modules/Sales/Contracts/SalesServiceInterface.php
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
15
app/Modules/Sales/SalesServiceProvider.php
Normal file
15
app/Modules/Sales/SalesServiceProvider.php
Normal 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);
|
||||
}
|
||||
}
|
||||
63
app/Modules/Sales/Services/SalesService.php
Normal file
63
app/Modules/Sales/Services/SalesService.php
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user