user('web'); abort_unless($actor instanceof User && $actor->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403); return $actor; } private function access(Request $request, int $id): Requisition { abort_unless($this->actor($request)->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403); return Requisition::query()->visibleTo($this->actor($request))->findOrFail($id); } public function show(Request $request, Requisition $requisition, ProposalOpeningWorkflow $opening): Response { $this->access($request, $requisition->id); $filters = $request->validate(['version' => 'nullable|integer|min:1']); $sets = QualificationSet::query()->where('requisition_id', $requisition->id)->latest('revision')->get(['id', 'revision', 'reason', 'created_at']); $latest = $sets->first() ? QualificationSet::query()->findOrFail($sets->first()->id) : null; $selected = isset($filters['version']) ? QualificationSet::query()->where('requisition_id', $requisition->id)->findOrFail((int) $filters['version']) : $latest; $focusInput = $request->validate(['quotation_id' => ['nullable', 'integer', 'min:1', Rule::exists('quotations', 'id')->where('tenant_id', $requisition->tenant_id)->where('requisition_id', $requisition->id), Rule::in(array_column($selected->snapshot['proposals'] ?? [], 'id'))]]); $focus = isset($focusInput['quotation_id']) ? (int) $focusInput['quotation_id'] : null; $proposals = $opening->proposals($requisition); $basis = $opening->basis($proposals); $workflow = app(DocumentaryQualificationWorkflow::class); $current = $selected !== null && $workflow->currentSet($requisition, $selected); $closed = $requisition->status !== 'approved' || PurchaseAward::query()->where('requisition_id', $requisition->id)->exists(); $query = QualificationAssessment::query()->where('qualification_set_id', $selected->id ?? 0); $recordIds = (clone $query)->selectRaw('MAX(id)')->groupBy('quotation_id', 'analyst_id'); $records = QualificationAssessment::query()->whereIn('id', $recordIds)->orderBy('quotation_id')->orderBy('analyst_id')->get(); Inertia::encryptHistory(); $response = Inertia::render('purchases/qualification', [ 'requisition' => $requisition->only(['id', 'number', 'justification']), 'sets' => $sets, 'selected' => $selected, 'latestId' => $latest->id ?? 0, 'latestCriteria' => $latest->criteria ?? [], 'basisHash' => $basis, 'isCurrent' => (bool) $current, 'proposals' => $proposals, 'closedForChanges' => $closed, 'records' => $records, 'history' => $query->latest('id')->paginate(20)->withQueryString(), 'actorId' => $this->actor($request)->id, 'focusQuotationId' => $focus, 'canDefine' => $workflow->canDefine($this->actor($request), $requisition), 'canAssess' => $selected !== null && $workflow->canAssess($this->actor($request), $requisition, $selected), ])->toResponse($request); $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); return $response; } public function define(Request $request, Requisition $requisition, DocumentaryQualificationWorkflow $workflow): RedirectResponse { $this->access($request, $requisition->id); $data = $request->validate([ 'previous_id' => 'required|integer|min:0', 'basis_hash' => 'required|string|size:64', 'reason' => 'required|string|min:10|max:4000', 'criteria' => 'required|array|min:1|max:30', 'criteria.*' => 'required|array:label,category,required', 'criteria.*.label' => 'required|string|min:3|max:500', 'criteria.*.category' => 'required|in:legal,financial,fiscal,technical,administrative', 'criteria.*.required' => 'required|boolean', 'snapshot' => 'prohibited', 'tenant_id' => 'prohibited', 'created_by' => 'prohibited', ]); $workflow->define($this->actor($request), $requisition, $data); return to_route('qualifications.show', $requisition); } public function assess(Request $request, QualificationSet $set, DocumentaryQualificationWorkflow $workflow): RedirectResponse { $this->access($request, $set->requisition_id); $data = $request->validate([ 'previous_id' => 'required|integer|min:0', 'quotation_id' => 'required|integer', 'reason' => 'required|string|min:10|max:4000', 'answers' => 'required|array|min:1|max:30', 'answers.*' => 'required|array:criterion_id,result,evidence,reason', 'answers.*.criterion_id' => 'required|integer|distinct', 'answers.*.result' => 'required|in:pending,met,not_met,not_applicable', 'answers.*.evidence' => 'nullable|string|max:1000', 'answers.*.reason' => 'nullable|string|max:4000', 'snapshot' => 'prohibited', 'analyst_id' => 'prohibited', 'tenant_id' => 'prohibited', 'revision' => 'prohibited', ]); $data['answers'] = array_map(fn ($answer) => [...$answer, 'criterion_id' => (int) $answer['criterion_id']], $data['answers']); $workflow->assess($this->actor($request), $set, $data); return to_route('qualifications.show', $set->requisition_id); } }