visibleWarehouseIdsQuery()); if ($search = $request->search) { $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('code', 'like', "%{$search}%"); }); } $warehouses = $query->withCount('stock') ->latest() ->paginate(15) ->withQueryString() ->through(fn ($w) => $w->append(['total_value', 'item_count'])); return Inertia::render('MaterialLogistics::Warehouses/Index', [ 'warehouses' => $warehouses, 'filters' => $request->only(['search']), ]); } public function create() { return Inertia::render('MaterialLogistics::Warehouses/Form'); } public function show(Request $request, Warehouse $warehouse) { abort_unless($this->visibleWarehouseIdsQuery()->where('warehouses.id', $warehouse->id)->exists(), 403); // Paginated stock with search & category filter $stockQuery = $warehouse->stock()->with([ 'material:id,ulid,name,sku,unit,category,type', 'material.components.component:id,name,unit' ]); if ($search = $request->search) { $stockQuery->whereHas('material', fn ($q) => $q->where('name', 'like', "%{$search}%") ->orWhere('sku', 'like', "%{$search}%") ); } if ($category = $request->category) { $stockQuery->whereHas('material', fn ($q) => $q->where('category', $category)); } $stock = $stockQuery->paginate(25)->withQueryString(); // Append is_low_stock to each stock item $stock->getCollection()->each(fn ($s) => $s->append('is_low_stock')); // Summary stats from full warehouse stock (not filtered) $allStock = $warehouse->stock()->with('material:id,category')->get(); $summary = [ 'total_items' => $allStock->count(), 'total_value' => $allStock->sum(fn ($s) => (float) $s->quantity * (float) $s->unit_cost), 'categories' => $allStock->pluck('material.category')->filter()->unique()->count(), 'low_stock_count' => $allStock->filter(fn ($s) => $s->is_low_stock)->count(), ]; // Distinct categories for filter dropdown $categories = $allStock->pluck('material.category') ->filter() ->unique() ->sort() ->values(); // Recent movements for this warehouse $movements = InventoryMovement::forWarehouse($warehouse->id) ->with(['material:id,ulid,name,sku,unit', 'performer:id,name']) ->latest() ->paginate(15, ['*'], 'movements_page') ->withQueryString(); return Inertia::render('MaterialLogistics::Warehouses/Show', [ 'warehouse' => $warehouse, 'stock' => $stock, 'summary' => $summary, 'categories' => $categories, 'movements' => $movements, 'filters' => $request->only(['search', 'category']), ]); } private function visibleWarehouseIdsQuery() { return Warehouse::query() ->whereIn('project_id', Project::query()->select('projects.id')) ->select('warehouses.id'); } public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'code' => 'required|string|max:20|unique:warehouses', 'address' => 'nullable|string', 'type' => 'required|in:main,yard,staging', ]); Warehouse::create($validated); return redirect()->route('warehouses.index')->with('success', 'Warehouse created.'); } public function edit(Warehouse $warehouse) { return Inertia::render('MaterialLogistics::Warehouses/Form', [ 'warehouse' => $warehouse, ]); } public function update(Request $request, Warehouse $warehouse) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'code' => "required|string|max:20|unique:warehouses,code,{$warehouse->id}", 'address' => 'nullable|string', 'type' => 'required|in:main,yard,staging', 'status' => 'nullable|in:active,inactive', ]); $warehouse->update($validated); return redirect()->route('warehouses.index')->with('success', 'Warehouse updated.'); } /** * JSON API: warehouse stock for selects and dispatching. */ public function stockSummary(Request $request) { $request->validate([ 'warehouse_ulid' => 'required|string', ]); $warehouse = Warehouse::where('ulid', $request->warehouse_ulid)->firstOrFail(); $stock = $warehouse->stock() ->with([ 'material:id,ulid,name,sku,unit,category,type', 'material.components.component:id,name,unit' ]) ->where('quantity', '>', 0) ->get() ->map(fn ($s) => [ 'material' => $s->material, 'quantity' => $s->quantity, 'reserved_qty' => $s->reserved_qty, 'available_qty' => $s->available_qty, 'unit_cost' => $s->unit_cost, ]); return response()->json($stock); } }