172 lines
5.8 KiB
PHP
172 lines
5.8 KiB
PHP
<?php
|
|
|
|
namespace Modules\MaterialLogistics\Http\Controllers;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use Inertia\Inertia;
|
|
use Modules\MaterialLogistics\Models\InventoryMovement;
|
|
use Modules\MaterialLogistics\Models\Warehouse;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
|
|
class WarehouseController extends Controller
|
|
{
|
|
public function index(Request $request)
|
|
{
|
|
$query = Warehouse::whereIn('id', $this->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);
|
|
}
|
|
}
|