user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403); } public function award(Request $request, Requisition $requisition, ProposalComparison $comparison, PurchaseWorkflow $workflow): Response { $this->access($request); Requisition::query()->visibleTo($request->user())->findOrFail($requisition->id); $award = PurchaseAward::query()->where('requisition_id', $requisition->id)->with('approvals')->first(); return Inertia::render('purchases/award', [ 'requisition' => $requisition, 'comparison' => $comparison->inspect($requisition), 'award' => $award, 'canSelect' => $request->user()->purchasing_role === PurchasingRole::Procurement && $award === null && $request->user()->id !== $requisition->requester_id, 'canDecide' => $award !== null && $workflow->canDecide($request->user(), $award, $requisition), 'canIssue' => $award?->status === 'approved' && $request->user()->purchasing_role === PurchasingRole::Procurement, 'order' => PurchaseOrder::query()->where('requisition_id', $requisition->id)->first(['id', 'number']), ]); } public function select(Request $request, Requisition $requisition, PurchaseWorkflow $workflow): RedirectResponse { $this->access($request); $data = $request->validate([ 'quotation_id' => 'required|integer', 'justification' => 'required|string|min:10|max:5000', 'delivery_date' => 'required|date_format:Y-m-d|after_or_equal:today', 'delivery_location' => 'required|string|min:3|max:500', 'status' => 'prohibited', 'total_minor' => 'prohibited', 'subtotal_minor' => 'prohibited', 'tax_minor' => 'prohibited', 'approval_snapshot' => 'prohibited', 'decision_matrix' => 'prohibited', 'created_by' => 'prohibited', 'tenant_id' => 'prohibited', 'override' => 'prohibited', 'exception' => 'prohibited', ]); $workflow->select($request->user(), $requisition, $data); return to_route('purchases.award', $requisition); } public function decide(Request $request, PurchaseAward $award, PurchaseWorkflow $workflow): RedirectResponse { $this->access($request); $data = $request->validate(['decision' => ['required', Rule::in(['approved', 'rejected'])], 'comment' => 'nullable|required_if:decision,rejected|string|max:2000']); $workflow->decide($request->user(), $award, $data['decision'], $data['comment'] ?? null); return to_route('purchases.award', $award->requisition_id); } public function issue(Request $request, PurchaseAward $award, PurchaseWorkflow $workflow): RedirectResponse { $this->access($request); $request->validate([ 'status' => 'prohibited', 'snapshot' => 'prohibited', 'total_minor' => 'prohibited', 'subtotal_minor' => 'prohibited', 'tax_minor' => 'prohibited', 'issued_by' => 'prohibited', 'tenant_id' => 'prohibited', 'quotation_id' => 'prohibited', 'override' => 'prohibited', 'exception' => 'prohibited', ]); $order = $workflow->issue($request->user(), $award); return to_route('orders.show', $order); } public function index(Request $request): Response { $this->access($request); return Inertia::render('purchases/index', [ 'orders' => PurchaseOrder::query()->visibleTo($request->user())->latest('id')->paginate(15), 'awards' => PurchaseAward::query()->whereIn('requisition_id', Requisition::query()->visibleTo($request->user())->select('id'))->latest()->paginate(15, ['*'], 'awards_page'), ]); } public function show(Request $request, PurchaseOrder $order): Response { $this->access($request); PurchaseOrder::query()->visibleTo($request->user())->findOrFail($order->id); return Inertia::render('purchases/show', [ 'order' => $order->load('items'), 'history' => AuditLog::query()->where('requisition_id', $order->requisition_id)->whereIn('action', ['award.created', 'award.approved', 'award.rejected', 'purchase_order.issued'])->orderBy('id')->get(['id', 'user_id', 'action', 'created_at']), ]); } public function print(Request $request, PurchaseOrder $order): View { $this->access($request); PurchaseOrder::query()->visibleTo($request->user())->findOrFail($order->id); return view('purchases.print', ['order' => $order->load('items'), 'snapshot' => $order->snapshot]); } }