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.'); } }