user()->hasFunctions([PurchasingRole::Admin, PurchasingRole::Procurement, PurchasingRole::Finance, PurchasingRole::DepartmentHead]), 403); return Requisition::query()->visibleTo($request->user())->findOrFail($requisitionId); } public function show(Request $request, Requisition $requisition, ProposalOpeningWorkflow $workflow): Response { $this->access($request, $requisition->id); $proposals = $workflow->proposals($requisition); $query = ProposalOpening::query()->where('requisition_id', $requisition->id)->latest('revision'); $latest = (clone $query)->first(); $openings = $query->paginate(10)->withQueryString(); Inertia::encryptHistory(); $response = Inertia::render('purchases/opening', [ 'requisition' => $requisition->only(['id', 'number', 'justification']), 'proposals' => $proposals, 'basisHash' => $workflow->basis($proposals), 'openings' => $openings, 'sourceChanged' => $latest && $workflow->basis($proposals) !== $latest->basis_hash, 'latestId' => $latest->id ?? 0, 'canRecord' => $request->user()->hasFunctions([PurchasingRole::Procurement]) && $requisition->status === 'approved' && ! PurchaseAward::query()->where('requisition_id', $requisition->id)->exists() && ! app(ConflictGuard::class)->participationBlocked($request->user(), $requisition), ])->toResponse($request); $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); return $response; } public function store(Request $request, Requisition $requisition, ProposalOpeningWorkflow $workflow): RedirectResponse { $this->access($request, $requisition->id); $data = $request->validate([ 'previous_id' => 'required|integer|min:0', 'basis_hash' => 'required|string|size:64', 'opened_at' => 'required|date_format:Y-m-d\TH:i', 'timezone' => 'required|in:Africa/Maputo,Africa/Johannesburg,UTC', 'submission_deadline' => 'nullable|date_format:Y-m-d\TH:i|before_or_equal:opened_at', 'reference' => 'required|string|min:3|max:150', 'location' => 'required|string|min:3|max:300', 'reason' => 'required|string|min:10|max:4000', 'notes' => 'nullable|string|max:6000', 'quotation_ids' => 'required|array|min:1|max:100', 'quotation_ids.*' => 'required|integer|distinct', 'participants' => 'required|array|min:1|max:50', 'participants.*.name' => 'required|string|min:2|max:150', 'participants.*' => 'required|array:name,capacity,group', 'participants.*.capacity' => 'required|string|min:2|max:150', 'participants.*.group' => 'required|in:jury,participant', 'annexes' => 'nullable|array|max:30', 'annexes.*' => 'required|string|min:3|max:300', 'snapshot' => 'prohibited', 'tenant_id' => 'prohibited', 'created_by' => 'prohibited', 'confirmed' => 'accepted', ]); $opened = CarbonImmutable::createFromFormat('!Y-m-d\TH:i', $data['opened_at'], $data['timezone']); if (! $opened || $opened->isFuture()) { throw ValidationException::withMessages(['opened_at' => 'A sessão não pode estar no futuro no fuso horário indicado.']); } $data['opened_at'] = $opened->utc()->format('Y-m-d H:i:s'); $data['submission_deadline'] = empty($data['submission_deadline']) ? null : CarbonImmutable::createFromFormat('!Y-m-d\TH:i', $data['submission_deadline'], $data['timezone'])->utc()->format('Y-m-d H:i:s'); $data['quotation_ids'] = array_map(intval(...), $data['quotation_ids']); $workflow->record($request->user(), $requisition, $data); return back(); } public function print(Request $request, ProposalOpening $opening): Response { $this->access($request, $opening->requisition_id); $response = response()->view('purchases.opening', ['opening' => $opening, 'snapshot' => $opening->snapshot]); $response->headers->set('Cache-Control', 'private, no-store, max-age=0'); return $response; } }