feat(offers): complete phase five operational controls in QA

This commit is contained in:
2026-11-14 18:08:46 +02:00
parent e431de8b42
commit 528a2597ed
33 changed files with 2383 additions and 19 deletions
@@ -8,7 +8,7 @@ use App\Services\OpportunityManagement;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\StreamedResponse;
final class OfferPublicCatalogueController extends Controller
@@ -17,30 +17,30 @@ final class OfferPublicCatalogueController extends Controller
{
$filters = $catalogue->filters($request);
return Inertia::render('offers/catalogue', [...$catalogue->page($request, $filters), 'filters' => $filters,
return $this->uncached(Inertia::render('offers/catalogue', [...$catalogue->page($request, $filters), 'filters' => $filters,
'mapData' => in_array($filters['view'] ?? '', ['map', 'combined'], true) ? $catalogue->map($filters) : null,
'territorySelection' => app(InvestmentGeography::class)->selection($filters),
'references' => ['categories' => OpportunityManagement::CATEGORIES, 'options' => [], 'clusters' => [], 'corridors' => [], 'projects' => [], 'locations' => [], 'sectors' => []]]);
'references' => ['categories' => OpportunityManagement::CATEGORIES, 'options' => [], 'clusters' => [], 'corridors' => [], 'projects' => [], 'locations' => [], 'sectors' => []]])->toResponse($request));
}
public function show(int $id, OfferPublicCatalogue $catalogue): Response
public function show(Request $request, int $id, OfferPublicCatalogue $catalogue): Response
{
return Inertia::render('offers/public-detail', ['offer' => $catalogue->detail($id)]);
return $this->uncached(Inertia::render('offers/public-detail', ['offer' => $catalogue->detail($id)])->toResponse($request));
}
public function search(Request $request, OfferPublicCatalogue $catalogue): JsonResponse
{
return response()->json($catalogue->page($request, $catalogue->filters($request)));
return response()->json($catalogue->page($request, $catalogue->filters($request)))->header('Cache-Control', 'private, no-store');
}
public function detail(int $id, OfferPublicCatalogue $catalogue): JsonResponse
{
return response()->json(['data' => $catalogue->detail($id)]);
return response()->json(['data' => $catalogue->detail($id)])->header('Cache-Control', 'private, no-store');
}
public function map(Request $request, OfferPublicCatalogue $catalogue): JsonResponse
{
return response()->json($catalogue->map($catalogue->filters($request)));
return response()->json($catalogue->map($catalogue->filters($request)))->header('Cache-Control', 'private, no-store');
}
public function export(Request $request, OfferPublicCatalogue $catalogue): StreamedResponse
@@ -60,6 +60,13 @@ final class OfferPublicCatalogueController extends Controller
fputcsv($stream, array_map(fn ($value) => preg_match('/^[\s]*[=+@-]/u', (string) $value) ? "'".$value : $value, $fields), ';', '"', '');
}
fclose($stream);
}, 'ofertas.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'X-Export-Limit' => '1000', 'X-Results-Truncated' => $result['truncated'] ? 'true' : 'false']);
}, 'ofertas.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'Cache-Control' => 'private, no-store', 'X-Export-Limit' => '1000', 'X-Results-Truncated' => $result['truncated'] ? 'true' : 'false']);
}
private function uncached(Response $response): Response
{
$response->headers->set('Cache-Control', 'private, no-store');
return $response;
}
}
@@ -107,6 +107,7 @@ final class PortalEditorialWorkflowController extends Controller
$destinations = app(ContentManagement::class)->access($actor);
$data = ['items' => $items, 'filters' => $filters, 'notices' => $workflow->notices($actor), 'resourceAccess' => $access->full($actor) || $access->editorial($actor), 'opportunitiesUrl' => $destinations['opportunities']['index'] ?? null,
'offersUrl' => isset($destinations['opportunities']['index']) && app(PortalProfileAccess::class)->available() ? '/minhas-oportunidades/ofertas' : null,
'offerOperationsUrl' => ! app(PortalProfileAccess::class)->has($actor, 'auditor') && (app(PortalProfileAccess::class)->has($actor, 'validator') || app(PortalProfileAccess::class)->has($actor, 'publisher')) ? '/minhas-oportunidades/ofertas?operational=1' : null,
'opportunityPending' => $workflow->opportunityPending($actor), 'clustersUrl' => $destinations['clusters']['index'] ?? null,
'portfolioUrl' => $destinations['portfolio']['index'] ?? null, 'portfolioPending' => $workflow->portfolioPending($actor), 'demographyUrl' => $destinations['demography']['index'] ?? null, 'demographyPending' => $workflow->demographyPending($actor),
'territorialUrl' => app(TerritorialRegistry::class)->editorialAvailable() && $actor->can('manage-portal') ? '/management/territorial/editorial' : null,
@@ -0,0 +1,51 @@
<?php
namespace App\Http\Controllers;
use App\Models\Opportunity;
use App\Services\PrivateOfferOperations;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Symfony\Component\HttpFoundation\Response;
final class PrivateOfferOperationsController extends Controller
{
public function show(Request $request, int $id, PrivateOfferOperations $operations): Response
{
return response()->json($operations->presentation($request->user(), $id));
}
public function owner(Request $request, int $id, PrivateOfferOperations $operations): Response
{
$operations->ownerAction($request->user(), $id, $request->all());
return response()->json($operations->presentation($request->user(), $id));
}
public function reportForm(Request $request, int $id, PrivateOfferOperations $operations): Response
{
$record = Opportunity::findOrFail($id);
abort_unless($record->isPrivateOfferPreparation() && $operations->canReport($request->user(), $record), 404);
return Inertia::render('offers/report', ['offerId' => $id])->toResponse($request);
}
public function report(Request $request, int $id, PrivateOfferOperations $operations): Response
{
return response()->json($operations->report($request->user(), $id, $request->all()), 201);
}
public function handleReport(Request $request, int $id, string $reportId, PrivateOfferOperations $operations): Response
{
$operations->handleReport($request->user(), $id, $reportId, $request->all());
return response()->json($operations->presentation($request->user(), $id));
}
public function highlight(Request $request, int $id, PrivateOfferOperations $operations): Response
{
$operations->highlight($request->user(), $id, $request->all());
return response()->json($operations->presentation($request->user(), $id));
}
}
@@ -3,12 +3,14 @@
namespace App\Http\Controllers;
use App\Models\Opportunity;
use App\Services\OfferAvailability;
use App\Services\OpportunityCanonicalContract;
use App\Services\OpportunityManagement;
use App\Services\OpportunityProfileWorkflow;
use App\Services\OpportunityResources;
use App\Services\PortalProfileAccess;
use App\Services\PrivateOfferConfidentiality;
use App\Services\PrivateOfferOperations;
use App\Services\PrivateOfferPreparation;
use App\Services\VerifiedDocumentBinary;
use Illuminate\Http\JsonResponse;
@@ -86,14 +88,20 @@ final class PrivateOfferPreparationController extends Controller
public function index(Request $request, PortalProfileAccess $access): Response
{
$filters = $request->validate(['state' => 'nullable|in:draft,submitted,in_review,needs_information,rejected,approved']);
$records = $access->scope(Opportunity::query(), $request->user())->whereNotNull('workflow_context->private_offer_preparation')
$filters = $request->validate(['state' => 'nullable|in:draft,submitted,in_review,needs_information,rejected,approved', 'operational' => 'nullable|boolean', 'pending_reports' => 'nullable|boolean']);
$query = ($filters['operational'] ?? false) ? app(PrivateOfferOperations::class)->staffQuery($request->user()) : $access->scope(Opportunity::query(), $request->user());
if ($filters['pending_reports'] ?? false) {
$query = app(PrivateOfferOperations::class)->staffQuery($request->user());
$query->whereRaw("jsonb_path_exists(COALESCE(workflow_context->'offer_reports','{}'::jsonb), '$.* ? (@.status == \"open\" || @.status == \"in_review\")')");
}
$records = $query->whereNotNull('workflow_context->private_offer_preparation')
->when($filters['state'] ?? null, fn ($q, $state) => $q->where('workflow_context->private_offer_preparation->state', $state))
->orderByDesc('id')->paginate(20)->withQueryString();
return Inertia::render('offers/preparation-index', ['records' => $records->through(fn ($r) => [
'id' => $r->id, 'title' => app(OpportunityManagement::class)->values($r)['title'] ?? null, 'reference' => $r->reference,
'state' => $access->state($r),
'availability' => app(OfferAvailability::class)->record($r)['state'],
]), 'filters' => $filters, 'canCreate' => count($access->navigation($request->user())['contexts']) > 0]);
}