Files
compras/app/Http/Controllers/RequisitionController.php
T
nala c6373365bf
tests / ci (push) Canceled after 0s
commit9
2026-10-30 20:02:30 +02:00

178 lines
8.6 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreRequisitionRequest;
use App\Models\AuditLog;
use App\Models\CostCenter;
use App\Models\Equipment;
use App\Models\EquipmentSparePart;
use App\Models\Requisition;
use App\Models\SparePart;
use App\Models\StockItem;
use App\Models\TechnicalReview;
use App\Models\Tenant;
use App\Services\ComparisonPolicy;
use App\Services\ConflictGuard;
use App\Services\EquipmentWorkflow;
use App\Services\OrganizationAccess;
use App\Services\RequisitionCatalogue;
use App\Services\RequisitionWorkflow;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
class RequisitionController extends Controller
{
public function index(Request $request): Response
{
$filters = $request->validate(['search' => 'nullable|string|max:100', 'status' => ['nullable', Rule::in(['draft', 'pending', 'approved', 'rejected', 'cancelled'])], 'view' => ['nullable', Rule::in(['all', 'mine', 'approvals', 'decided'])]]);
$query = Requisition::query()->visibleTo($request->user())->with(['requester:id,name', 'department:id,name', 'steps']);
if (($filters['view'] ?? 'all') === 'mine') {
$query->where('requester_id', $request->user()->id);
} elseif (($filters['view'] ?? 'all') === 'approvals') {
$eligible = (clone $query)->where('status', 'pending')->get()
->filter(fn (Requisition $requisition) => $requisition->canDecide($request->user()))->modelKeys();
$query->whereIn('id', $eligible);
} elseif (($filters['view'] ?? 'all') === 'decided') {
$query->whereHas('steps', fn ($steps) => $steps->where('approver_id', $request->user()->id)->whereIn('decision', ['approved', 'rejected']));
}
if (! empty($filters['search'])) {
$query->where('number', 'like', '%'.$filters['search'].'%');
}
if (! empty($filters['status'])) {
$query->where('status', $filters['status']);
}
return Inertia::render('requisitions/index', [
'requisitions' => $query->latest()->paginate(15)->withQueryString(),
'filters' => $filters,
]);
}
public function create(Request $request): Response
{
abort_unless(app(OrganizationAccess::class)->canRequest($request->user()), 403);
return Inertia::render('requisitions/form', [
'costCenters' => CostCenter::query()->where('department_id', $request->user()->department_id)->orderBy('code')->get(),
'requisition' => null,
'defaultTaxRate' => (string) (Tenant::query()->findOrFail($request->user()->tenant_id)->default_tax_rate ?? '16.00'),
...$this->equipmentOptions($request),
'catalogue' => $this->catalogueOptions($request),
]);
}
public function store(StoreRequisitionRequest $request, RequisitionWorkflow $workflow): RedirectResponse
{
$requisition = $workflow->save($request->user(), $request->validated());
return to_route('requisitions.show', $requisition);
}
public function show(Request $request, Requisition $requisition): Response
{
Requisition::query()->visibleTo($request->user())->findOrFail($requisition->id);
$requisition->load(['items', 'steps', 'requester:id,name', 'department:id,name', 'costCenter']);
return Inertia::render('requisitions/show', [
'requisition' => $requisition,
'comparisonPolicy' => app(ComparisonPolicy::class)->forRequest($requisition),
'canEdit' => app(OrganizationAccess::class)->canRequest($request->user()) && $requisition->requester_id === $request->user()->id && $requisition->status === 'draft',
'canDecide' => $requisition->canDecide($request->user()),
'conflictBlocked' => app(ConflictGuard::class)->participationBlocked($request->user(), $requisition),
'audit' => AuditLog::query()->where('requisition_id', $requisition->id)->orderBy('id')->get(['id', 'action', 'user_id', 'created_at']),
'technical' => [
'canReview' => app(EquipmentWorkflow::class)->canReview($request->user(), $requisition),
'basis_hash' => app(EquipmentWorkflow::class)->basis($requisition),
'reviews' => TechnicalReview::query()->where('requisition_id', $requisition->id)->orderBy('id')->get(),
],
]);
}
public function edit(Request $request, Requisition $requisition): Response
{
abort_unless(app(OrganizationAccess::class)->canRequest($request->user()), 403);
abort_unless($requisition->requester_id === $request->user()->id && $requisition->status === 'draft', 403);
return Inertia::render('requisitions/form', [
'requisition' => $requisition->load('items'),
'defaultTaxRate' => (string) (Tenant::query()->findOrFail($request->user()->tenant_id)->default_tax_rate ?? '16.00'),
'costCenters' => CostCenter::query()->where('department_id', $request->user()->department_id)->orderBy('code')->get(),
...$this->equipmentOptions($request),
'catalogue' => $this->catalogueOptions($request),
]);
}
/** @return array<string,mixed> */
private function equipmentOptions(Request $request): array
{
$equipment = Equipment::query()->where('department_id', $request->user()->department_id)->orderBy('code')->get(['id', 'code', 'description']);
$parts = SparePart::query()->whereIn('id', EquipmentSparePart::query()->whereIn('equipment_id', $equipment->pluck('id'))->select('spare_part_id'))->get(['id', 'stock_item_id']);
return ['equipment' => $equipment, 'spareParts' => $parts, 'stockItems' => StockItem::query()->whereIn('id', $parts->pluck('stock_item_id'))->get(['id', 'code', 'name', 'unit'])];
}
/** @return array<string,mixed> */
private function catalogueOptions(Request $request): array
{
$search = $request->validate(['catalogo' => 'nullable|string|max:100'])['catalogo'] ?? '';
return app(RequisitionCatalogue::class)->options($request->user(), $search);
}
public function technical(Request $request, Requisition $requisition): RedirectResponse
{
$data = $request->validate(['basis_hash' => 'required|string|size:64', 'decision' => ['required', Rule::in(['approved', 'rejected'])], 'reason' => 'required|string|max:4000']);
app(EquipmentWorkflow::class)->review($request->user(), $requisition, $data['basis_hash'], $data['decision'], $data['reason']);
return back();
}
public function update(StoreRequisitionRequest $request, Requisition $requisition, RequisitionWorkflow $workflow): RedirectResponse
{
$workflow->save($request->user(), $request->validated(), $requisition);
return to_route('requisitions.show', $requisition);
}
public function submit(Request $request, Requisition $requisition, RequisitionWorkflow $workflow): RedirectResponse
{
$workflow->submit($request->user(), $requisition);
return back();
}
public function decide(Request $request, Requisition $requisition, RequisitionWorkflow $workflow): RedirectResponse
{
$data = $request->validate([
'decision' => ['required', Rule::in(['approved', 'rejected'])],
'comment' => ['nullable', 'required_if:decision,rejected', 'string', 'max:2000'],
]);
$workflow->decide($request->user(), $requisition, $data['decision'], $data['comment'] ?? null);
return back();
}
public function dashboard(Request $request): Response
{
$visible = Requisition::query()->visibleTo($request->user());
$pending = (clone $visible)->where('status', 'pending')->with('steps')->get();
return Inertia::render('dashboard', [
'company' => $request->user()->tenant_id ? Tenant::query()->find($request->user()->tenant_id) : null,
'stats' => [
'total' => (clone $visible)->count(),
'pending' => $pending->count(),
'approved' => (clone $visible)->where('status', 'approved')->count(),
'approvedMinor' => (clone $visible)->where('status', 'approved')->sum('total_minor'),
'awaitingMe' => $pending->filter(fn (Requisition $r) => $r->canDecide($request->user()))->count(),
],
'recent' => (clone $visible)->with(['requester:id,name', 'department:id,name'])->latest()->limit(6)->get(),
'approvals' => $pending->filter(fn (Requisition $r) => $r->canDecide($request->user()))->take(5)->values(),
]);
}
}