Implementa qualificacao documental com requisitos e analises por versao

This commit is contained in:
2026-10-28 19:28:05 +02:00
parent b7a52e9e00
commit 9138379432
21 changed files with 1677 additions and 11 deletions
@@ -0,0 +1,87 @@
<?php
namespace App\Http\Controllers;
use App\Enums\PurchasingRole;
use App\Models\PurchaseAward;
use App\Models\QualificationAssessment;
use App\Models\QualificationSet;
use App\Models\Requisition;
use App\Services\ConflictGuard;
use App\Services\DocumentaryQualificationWorkflow;
use App\Services\ProposalComparison;
use App\Services\ProposalOpeningWorkflow;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
class DocumentaryQualificationController extends Controller
{
private function access(Request $request, int $id): Requisition
{
abort_unless(in_array($request->user()->purchasing_role, [PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403);
return Requisition::query()->visibleTo($request->user())->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;
$proposals = $opening->proposals($requisition);
$basis = $opening->basis($proposals);
$current = $selected && $selected->id === $latest?->id && $selected->basis_hash === $basis;
$closed = $requisition->status !== 'approved' || PurchaseAward::query()->where('requisition_id', $requisition->id)->exists();
$open = ! $closed && ! app(ConflictGuard::class)->participationBlocked($request->user(), $requisition);
$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' => $request->user()->id,
'canDefine' => $open && $proposals !== [] && $request->user()->purchasing_role === PurchasingRole::Procurement,
'canAssess' => $open && $current && $request->user()->id !== $requisition->requester_id && app(ProposalComparison::class)->eligibleRole($request->user(), $requisition),
])->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($request->user(), $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($request->user(), $set, $data);
return to_route('qualifications.show', $set->requisition_id);
}
}