whereIn('id', $this->visibleWarehouseIdsQuery()) ->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::whereIn('warehouse_id', $this->visibleWarehouseIdsQuery()) ->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::whereIn('project_id', $this->visibleProjectIdsQuery()) ->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::whereIn('warehouse_id', $this->visibleWarehouseIdsQuery())->sum('quantity'), 'total_onsite' => ProjectInventory::whereIn('project_id', $this->visibleProjectIdsQuery())->sum('on_hand_qty'), 'total_reserved' => ProjectInventory::whereIn('project_id', $this->visibleProjectIdsQuery())->sum('allocated_qty'), 'total_consumed' => ProjectInventory::whereIn('project_id', $this->visibleProjectIdsQuery())->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() ->whereIn('id', $this->visibleWarehouseIdsQuery()) ->select('id', 'ulid', 'name', 'code')->get(); $projects = Project::select('id', 'ulid', 'name', 'code') ->with(['personnel' => function ($query) { $query->wherePivot('role', 'pm')->select('users.id', 'users.name'); }]) ->latest() ->get() ->map(function ($p) { $pm = $p->personnel->first(); return [ 'id' => $p->id, 'ulid' => $p->ulid, 'name' => $p->name, 'code' => $p->code, 'pm' => $pm ? [ 'id' => $pm->id, 'name' => $pm->name, ] : null, ]; }); // Materials with their warehouse/site stocks to enforce limits $materials = Material::select('id', 'ulid', 'name', 'sku', 'unit', 'unit_cost') ->with(['warehouseStocks', 'projectInventories']) ->get(); $projectManagers = User::whereIn('user_type', ['admin', 'employee', 'contractor']) ->where(function ($q) { $q->whereIn('user_type', ['admin']) ->orWhereHas('roles', function ($rq) { $rq->whereIn('name', ['Project Manager', 'Main Contractor Admin', 'Super Admin', 'admin', 'Construction Supervisor', 'Site Technical']); }); }) ->select('id', 'name', 'email') ->get(); return Inertia::render('MaterialLogistics::Inventory/OperationsForm', [ 'warehouses' => $warehouses, 'projects' => $projects, 'materials' => $materials, 'projectManagers' => $projectManagers, 'defaultType' => $type, ]); } // --- Fetch materials filtered by operation parameters (e.g. PO for dispatch) --- public function getMaterialsForOperation(Request $request) { $request->validate([ 'purchase_order_ulid' => 'nullable|string', 'warehouse_ulid' => 'nullable|string', 'project_ulid' => 'nullable|string', 'type' => 'required|in:dispatch,return,adjust', ]); $query = Material::select('id', 'ulid', 'name', 'sku', 'unit', 'unit_cost') ->with(['warehouseStocks', 'projectInventories']); if ($request->type === 'dispatch') { if ($request->filled('purchase_order_ulid')) { $po = PurchaseOrder::where('ulid', $request->purchase_order_ulid)->first(); if ($po) { $materialIds = $po->items()->pluck('material_id'); $query->whereIn('id', $materialIds); } else { $query->whereRaw('1 = 0'); } } else { $query->whereRaw('1 = 0'); } } $materials = $query->get(); return response()->json($materials); } // --- Dispatch from Warehouse to Project --- public function dispatchFromWarehouse(Request $request) { $validated = $request->validate([ 'warehouse_ulid' => 'required|string', 'project_ulid' => 'required|string', 'purchase_order_ulid' => 'required|string|exists:purchase_orders,ulid', '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(); $po = PurchaseOrder::where('ulid', $validated['purchase_order_ulid'])->firstOrFail(); if ($po->project_id !== $project->id || $po->target_warehouse_id !== $warehouse->id) { return back()->with('error', 'The selected Purchase Order is not associated with this project and warehouse.'); } $poMaterialIds = $po->items()->pluck('material_id')->toArray(); DB::beginTransaction(); try { foreach ($validated['items'] as $item) { $material = Material::where('ulid', $item['material_ulid'])->firstOrFail(); if (!in_array($material->id, $poMaterialIds)) { throw new \InvalidArgumentException("Material {$material->name} is not listed in the selected Purchase Order."); } $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.'); } private function visibleProjectIdsQuery() { return Project::query()->select('projects.id'); } private function visibleWarehouseIdsQuery() { return Warehouse::query() ->whereIn('project_id', $this->visibleProjectIdsQuery()) ->select('warehouses.id'); } }