Files
GSB-Construction/Modules/MaterialLogistics/app/Http/Controllers/MaterialController.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.');
}
}