chore: update document approval workflow and bug fixes
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\MaterialLogistics\Http\Controllers;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
use Modules\MasterData\Models\Material;
|
||||
use Modules\MaterialLogistics\Models\ProjectInventory;
|
||||
use Modules\MaterialLogistics\Models\Warehouse;
|
||||
use Modules\MaterialLogistics\Models\WarehouseStock;
|
||||
use Modules\MaterialLogistics\Services\WarehouseService;
|
||||
use Modules\ProjectManagement\Models\Project;
|
||||
use Modules\ProjectManagement\Models\Task;
|
||||
use Modules\MaterialLogistics\Models\InventoryBatch;
|
||||
|
||||
class MaterialController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private WarehouseService $warehouseService,
|
||||
) {}
|
||||
|
||||
// --- Inventory Dashboard ---
|
||||
public function inventory(Request $request)
|
||||
{
|
||||
$warehouses = Warehouse::active()->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.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user