Files
Ajjj c13f418517
Some checks failed
Tests / PHP 8.3 (push) Has been cancelled
Tests / PHP 8.4 (push) Has been cancelled
Tests / PHP 8.5 (push) Has been cancelled
feat: implement inventory management controllers, dispatch service logic, and frontend inventory tracking dashboard
2026-08-05 16:23:57 +08:00

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