307 lines
11 KiB
PHP
307 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Modules\MaterialLogistics\Http\Controllers;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Inertia\Inertia;
|
|
use Modules\MasterData\Models\Material;
|
|
use Modules\MaterialLogistics\Models\ProjectInventory;
|
|
use Modules\MaterialLogistics\Models\Warehouse;
|
|
use Modules\MaterialLogistics\Models\WarehouseStock;
|
|
use Modules\MaterialLogistics\Services\WarehouseService;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
use Modules\ProjectManagement\Models\Task;
|
|
use Modules\MaterialLogistics\Models\InventoryBatch;
|
|
|
|
class MaterialController extends Controller
|
|
{
|
|
public function __construct(
|
|
private WarehouseService $warehouseService,
|
|
) {}
|
|
|
|
// --- Inventory Dashboard ---
|
|
public function inventory(Request $request)
|
|
{
|
|
$warehouses = Warehouse::active()->select('id', 'ulid', 'name', 'code', 'type')
|
|
->with(['stock' => function ($query) {
|
|
$query->where('quantity', '>', 0);
|
|
}])
|
|
->get()
|
|
->map(function ($w) {
|
|
return [
|
|
'id' => $w->id,
|
|
'ulid' => $w->ulid,
|
|
'name' => $w->name,
|
|
'code' => $w->code,
|
|
'type' => $w->type,
|
|
'item_count' => $w->stock->count(),
|
|
'total_value' => $w->stock->sum(fn ($s) => (float) $s->quantity * (float) $s->unit_cost),
|
|
];
|
|
});
|
|
|
|
$projects = Project::select('id', 'ulid', 'name', 'code', 'status')
|
|
->with(['inventories' => function ($query) {
|
|
$query->where('on_hand_qty', '>', 0);
|
|
}])
|
|
->latest()
|
|
->get()
|
|
->map(function ($p) {
|
|
return [
|
|
'id' => $p->id,
|
|
'ulid' => $p->ulid,
|
|
'name' => $p->name,
|
|
'code' => $p->code,
|
|
'status' => $p->status,
|
|
'item_count' => $p->inventories->count(),
|
|
'total_value' => $p->inventories->sum(fn ($s) => (float) $s->on_hand_qty * (float) $s->unit_cost),
|
|
];
|
|
});
|
|
|
|
// Warehouse stock
|
|
$warehouseStockQuery = WarehouseStock::with([
|
|
'warehouse:id,ulid,name,code',
|
|
'material' => function ($query) {
|
|
$query->select('id', 'ulid', 'name', 'sku', 'unit', 'category', 'type')
|
|
->with('components.component:id,name,unit');
|
|
},
|
|
]);
|
|
|
|
if ($warehouseId = $request->warehouse_id) {
|
|
$warehouseStockQuery->where('warehouse_id', $warehouseId);
|
|
}
|
|
|
|
$warehouseStock = $warehouseStockQuery->where('quantity', '>', 0)
|
|
->paginate(20, ['*'], 'wh_page')
|
|
->withQueryString();
|
|
|
|
// Project site stock
|
|
$siteStockQuery = ProjectInventory::with([
|
|
'project:id,ulid,name,code',
|
|
'material' => function ($query) {
|
|
$query->select('id', 'ulid', 'name', 'sku', 'unit', 'category', 'type')
|
|
->with('components.component:id,name,unit');
|
|
},
|
|
]);
|
|
|
|
if ($projectId = $request->project_id) {
|
|
$siteStockQuery->where('project_id', $projectId);
|
|
}
|
|
|
|
$siteStock = $siteStockQuery->paginate(20, ['*'], 'site_page')
|
|
->withQueryString();
|
|
|
|
// Summary stats
|
|
$summary = [
|
|
'total_warehouse' => WarehouseStock::sum('quantity'),
|
|
|
|
'total_onsite' => ProjectInventory::sum('on_hand_qty'),
|
|
'total_reserved' => ProjectInventory::sum('allocated_qty'),
|
|
'total_consumed' => ProjectInventory::sum('consumed_qty'),
|
|
];
|
|
|
|
return Inertia::render('MaterialLogistics::Inventory/Index', [
|
|
'warehouseStock' => $warehouseStock,
|
|
'siteStock' => $siteStock,
|
|
'warehouses' => $warehouses,
|
|
'projects' => $projects,
|
|
'summary' => $summary,
|
|
'filters' => $request->only(['warehouse_id', 'project_id']),
|
|
]);
|
|
}
|
|
|
|
// --- Inventory Operations Form Page ---
|
|
public function operations(Request $request, string $type)
|
|
{
|
|
$warehouses = Warehouse::active()->select('id', 'ulid', 'name', 'code')->get();
|
|
$projects = Project::select('id', 'ulid', 'name', 'code')->latest()->get();
|
|
|
|
// Materials with their warehouse/site stocks to enforce limits
|
|
$materials = Material::select('id', 'ulid', 'name', 'sku', 'unit', 'unit_cost')
|
|
->with(['warehouseStocks', 'projectInventories'])
|
|
->get();
|
|
|
|
$projectManagers = \App\Models\User::role('project_manager')->select('id', 'name')->get();
|
|
|
|
return Inertia::render('MaterialLogistics::Inventory/OperationsForm', [
|
|
'warehouses' => $warehouses,
|
|
'projects' => $projects,
|
|
'materials' => $materials,
|
|
'projectManagers' => $projectManagers,
|
|
'defaultType' => $type,
|
|
]);
|
|
}
|
|
|
|
// --- Dispatch from Warehouse to Project ---
|
|
public function dispatchFromWarehouse(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'warehouse_ulid' => 'required|string',
|
|
'project_ulid' => 'required|string',
|
|
'handler_id' => 'required|integer|exists:users,id',
|
|
'notes' => 'nullable|string|max:500',
|
|
'items' => 'required|array|min:1',
|
|
'items.*.inventory_batch_ulid' => 'nullable|string',
|
|
'items.*.material_ulid' => 'required|string',
|
|
'items.*.quantity' => 'required|numeric|min:0.01',
|
|
]);
|
|
|
|
$warehouse = Warehouse::where('ulid', $validated['warehouse_ulid'])->firstOrFail();
|
|
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
|
|
|
|
DB::beginTransaction();
|
|
try {
|
|
foreach ($validated['items'] as $item) {
|
|
$material = Material::where('ulid', $item['material_ulid'])->firstOrFail();
|
|
$batchId = null;
|
|
|
|
if (!empty($item['inventory_batch_ulid'])) {
|
|
$batchId = InventoryBatch::where('ulid', $item['inventory_batch_ulid'])->firstOrFail()->id;
|
|
}
|
|
|
|
$this->warehouseService->dispatchToProject(
|
|
warehouse: $warehouse,
|
|
project: $project,
|
|
material: $material,
|
|
quantity: $item['quantity'],
|
|
inventoryBatchId: $batchId,
|
|
performedBy: auth()->id(),
|
|
dispatchedBy: $validated['handler_id'],
|
|
notes: $validated['notes'] ?? null,
|
|
);
|
|
}
|
|
DB::commit();
|
|
} catch (\Exception $e) {
|
|
DB::rollBack();
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', "Materials dispatched to {$project->name}.");
|
|
}
|
|
|
|
|
|
|
|
// --- Reserve for Task ---
|
|
public function reserveForTask(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'project_ulid' => 'required|string',
|
|
'task_ulid' => 'required|string',
|
|
'material_ulid' => 'required|string',
|
|
'quantity' => 'required|numeric|min:0.01',
|
|
]);
|
|
|
|
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
|
|
$task = Task::where('ulid', $validated['task_ulid'])->firstOrFail();
|
|
$material = Material::where('ulid', $validated['material_ulid'])->firstOrFail();
|
|
|
|
try {
|
|
$this->warehouseService->reserveForTask(
|
|
$project, $task, $material,
|
|
$validated['quantity'],
|
|
auth()->id(),
|
|
);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Materials reserved for task.');
|
|
}
|
|
|
|
// --- Consume at Task ---
|
|
public function consumeAtTask(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'project_ulid' => 'required|string',
|
|
'task_ulid' => 'required|string',
|
|
'material_ulid' => 'required|string',
|
|
'quantity' => 'required|numeric|min:0.01',
|
|
'notes' => 'nullable|string|max:500',
|
|
]);
|
|
|
|
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
|
|
$task = Task::where('ulid', $validated['task_ulid'])->firstOrFail();
|
|
$material = Material::where('ulid', $validated['material_ulid'])->firstOrFail();
|
|
|
|
try {
|
|
$this->warehouseService->consumeAtTask(
|
|
$project, $task, $material,
|
|
$validated['quantity'],
|
|
auth()->id(),
|
|
$validated['notes'] ?? null,
|
|
);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Materials consumed.');
|
|
}
|
|
|
|
// --- Return to Warehouse ---
|
|
public function returnToWarehouse(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'project_ulid' => 'required|string',
|
|
'warehouse_ulid' => 'required|string',
|
|
'material_ulid' => 'required|string',
|
|
'quantity' => 'required|numeric|min:0.01',
|
|
'notes' => 'nullable|string|max:500',
|
|
]);
|
|
|
|
$project = Project::where('ulid', $validated['project_ulid'])->firstOrFail();
|
|
$warehouse = Warehouse::where('ulid', $validated['warehouse_ulid'])->firstOrFail();
|
|
$material = Material::where('ulid', $validated['material_ulid'])->firstOrFail();
|
|
|
|
try {
|
|
$this->warehouseService->returnToWarehouse(
|
|
$project, $warehouse, $material,
|
|
$validated['quantity'],
|
|
auth()->id(),
|
|
$validated['notes'] ?? null,
|
|
);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Materials returned to warehouse.');
|
|
}
|
|
|
|
// --- Adjust Stock ---
|
|
public function adjustStock(Request $request)
|
|
{
|
|
$validated = $request->validate([
|
|
'location_type' => 'required|in:warehouse,project',
|
|
'location_ulid' => 'required|string',
|
|
'material_ulid' => 'required|string',
|
|
'quantity' => 'required|numeric|min:0.01',
|
|
'direction' => 'required|in:in,out',
|
|
'reason' => 'required|string|max:500',
|
|
]);
|
|
|
|
$material = Material::where('ulid', $validated['material_ulid'])->firstOrFail();
|
|
|
|
if ($validated['location_type'] === 'warehouse') {
|
|
$location = Warehouse::where('ulid', $validated['location_ulid'])->firstOrFail();
|
|
$locationId = $location->id;
|
|
} else {
|
|
$location = Project::where('ulid', $validated['location_ulid'])->firstOrFail();
|
|
$locationId = $location->id;
|
|
}
|
|
|
|
try {
|
|
$this->warehouseService->adjustStock(
|
|
$validated['location_type'], $locationId, $material,
|
|
$validated['quantity'],
|
|
$validated['direction'],
|
|
$validated['reason'],
|
|
auth()->id(),
|
|
);
|
|
} catch (\InvalidArgumentException $e) {
|
|
return back()->with('error', $e->getMessage());
|
|
}
|
|
|
|
return back()->with('success', 'Stock adjusted.');
|
|
}
|
|
}
|