fix: complete InvesteMoz validation and error corrections
This commit is contained in:
@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -21,7 +21,7 @@ class HomeController extends Controller
|
||||
$locale = $validated['locale'] ?? 'pt';
|
||||
|
||||
$data = Cache::remember("portal_home_sector_organization_v1_{$locale}", now()->addMinutes(10), function () {
|
||||
$sectors = \App\Services\SectorOrganization::catalog();
|
||||
$sectors = SectorOrganization::catalog();
|
||||
|
||||
$featured = Opportunity::with('sector:id,name,slug')
|
||||
->published()->where('is_featured', true)
|
||||
|
||||
@@ -8,11 +8,13 @@ use App\Http\Requests\StoreInterestRequestRequest;
|
||||
use App\Jobs\NotifyInterestReceived;
|
||||
use App\Models\ExistingBusiness;
|
||||
use App\Models\InterestRequest;
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\IdempotencyConflict;
|
||||
use App\Services\InterestSubmission;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@@ -26,7 +28,7 @@ class InterestRequestController extends Controller
|
||||
$validated = [...$request->validated(), '_actor_id' => $request->user()->id];
|
||||
$key = $request->header('Idempotency-Key');
|
||||
abort_unless(is_string($key), 422, 'Identificador do envio em falta.');
|
||||
Validator::make(['idempotency_key' => $key], ['idempotency_key' => [$key === null ? 'nullable' : 'required', 'uuid']])->validate();
|
||||
Validator::make(['idempotency_key' => $key], ['idempotency_key' => ['required', 'uuid']])->validate();
|
||||
|
||||
try {
|
||||
$interestRequest = $submission->receive($validated, $key, function () use ($request, $validated) {
|
||||
@@ -78,19 +80,20 @@ class InterestRequestController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
$row = \App\Models\InvestmentSubmission::create([
|
||||
$row = InvestmentSubmission::create([
|
||||
'user_id' => $request->user()->id, 'opportunity_id' => $opportunity?->id,
|
||||
'interest_request_id' => $interest->id, 'status' => 'submitted', 'version' => 1,
|
||||
'payload' => ['simple_participation' => true, 'opportunity_title' => $opportunity?->title ?? $business?->title, 'opportunity_reference' => $opportunity?->reference, 'phone' => $validated['phone'] ?? null, 'declared_origin_country_code' => $validated['declared_origin_country_code'] ?? null, 'full_name' => $validated['fullName'], 'email' => $validated['email'],
|
||||
'payload' => ['simple_participation' => true, 'opportunity_title' => $opportunity->title ?? $business?->public_title, 'opportunity_reference' => $opportunity?->reference, 'phone' => $validated['phone'] ?? null, 'declared_origin_country_code' => $validated['declared_origin_country_code'] ?? null, 'full_name' => $validated['fullName'], 'email' => $validated['email'],
|
||||
'objective' => $validated['message'], 'sector' => $validated['sectorInterest'],
|
||||
'country' => $validated['country'], 'existing_business_id' => $business?->id,
|
||||
'modality' => $validated['modality'] ?? null],
|
||||
]);
|
||||
\Illuminate\Support\Facades\DB::table('investment_submission_events')->insert([
|
||||
DB::table('investment_submission_events')->insert([
|
||||
'submission_id' => $row->id, 'actor_id' => $request->user()->id,
|
||||
'action' => 'submitted', 'version' => 1, 'created_at' => now(),
|
||||
]);
|
||||
$interest->forceFill(['metadata' => [...$interest->metadata, 'submission_id' => $row->id]])->save();
|
||||
|
||||
return $interest;
|
||||
});
|
||||
|
||||
@@ -99,7 +102,8 @@ class InterestRequestController extends Controller
|
||||
'message' => 'Manifestação de interesse registada com sucesso.',
|
||||
'data' => [
|
||||
'reference' => $interestRequest->reference,
|
||||
'status' => $interestRequest->status,
|
||||
// The POST acknowledgement is stable on replay; current workflow state is in tracking.
|
||||
'status' => InterestStatus::Received,
|
||||
'tracking_url' => '/investidor/manifestacoes',
|
||||
],
|
||||
], 201);
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sector;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\EconomicReferenceCatalog;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -44,7 +47,9 @@ class ReferenceCatalogController extends Controller
|
||||
|
||||
$cae = $catalog === 'economic-activities' && ($data['origin'] ?? 'portal') === 'cae';
|
||||
$versionQuery = DB::table('reference_catalog_versions')->where('catalog_code', $cae ? 'economic-activities-cae' : $catalog)->where('locale', 'pt-MZ');
|
||||
if ($cae && ! app(\App\Services\EconomicReferenceCatalog::class)->available()) { $versionQuery->whereRaw('false'); }
|
||||
if ($cae && ! app(EconomicReferenceCatalog::class)->available()) {
|
||||
$versionQuery->whereRaw('false');
|
||||
}
|
||||
if (isset($data['version'])) {
|
||||
$versionQuery->where('version_no', $data['version'])->whereIn('status', ['published', 'retired']);
|
||||
} else {
|
||||
@@ -82,7 +87,7 @@ class ReferenceCatalogController extends Controller
|
||||
$page = $query->orderBy('code')->orderBy('id')->paginate($data['per_page'] ?? 20)->withQueryString();
|
||||
$items = collect($page->items())->map(fn (object $row): array => $this->present($catalog, (array) $row))->values();
|
||||
if ($cae) {
|
||||
$paths = app(\App\Services\EconomicReferenceCatalog::class)->activities()->keyBy('id');
|
||||
$paths = app(EconomicReferenceCatalog::class)->activities()->keyBy('id');
|
||||
$items = $items->map(fn ($row) => [...$row, 'level' => $paths[$row['id']]->level, 'path' => $paths[$row['id']]->path, 'source_version' => $paths[$row['id']]->source_version, 'edition' => 'not_determined', 'divergence' => '600/579 documentalmente aberta']);
|
||||
}
|
||||
|
||||
@@ -169,15 +174,17 @@ class ReferenceCatalogController extends Controller
|
||||
->where('valid_from', '<=', today()->toDateString())
|
||||
->where(fn (Builder $builder) => $builder->whereNull('valid_to')->orWhere('valid_to', '>', today()->toDateString()))
|
||||
->select(['id', 'code', 'label_pt_mz as name', 'parent_id', 'sector_id', 'version_no']);
|
||||
if (empty($data['version'])) $query->where('id', '!=', \App\Services\SectorOrganization::LEGACY_ACTIVITY_ID);
|
||||
if (empty($data['version'])) {
|
||||
$query->where('id', '!=', SectorOrganization::LEGACY_ACTIVITY_ID);
|
||||
}
|
||||
if (array_key_exists('parent_id', $data) && $data['parent_id'] !== null) {
|
||||
$query->where('parent_id', $data['parent_id']);
|
||||
} else {
|
||||
$query->whereNull('parent_id');
|
||||
}
|
||||
if (! empty($data['sector_id'])) {
|
||||
$sector = \App\Models\Sector::find($data['sector_id']);
|
||||
$query->whereIn('sector_id', $sector ? \App\Models\Sector::whereIn('slug', \App\Services\SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
$sector = Sector::whereKey($data['sector_id'])->first();
|
||||
$query->whereIn('sector_id', $sector ? Sector::whereIn('slug', SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
}
|
||||
if (! empty($data['q'])) {
|
||||
$this->search($query, $data['q'], ['code', 'label_pt_mz']);
|
||||
|
||||
@@ -4,13 +4,14 @@ namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Sector;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
class SectorController extends Controller
|
||||
{
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$sectors = \App\Services\SectorOrganization::catalog()->map(fn ($s) => [
|
||||
$sectors = SectorOrganization::catalog()->map(fn ($s) => [
|
||||
'id' => $s->id, 'name' => $s->name, 'slug' => $s->slug,
|
||||
'shortDescription' => $s->short_description, 'description' => $s->description,
|
||||
'icon' => $s->icon, 'opportunitiesCount' => $s->opportunities_count,
|
||||
@@ -25,7 +26,8 @@ class SectorController extends Controller
|
||||
$sector = Sector::with(['opportunities' => fn ($q) => $q->published()])
|
||||
->where('slug', $slug)->firstOrFail();
|
||||
|
||||
$sector->setRelation('opportunities', \App\Services\SectorOrganization::opportunities($sector)->get());
|
||||
return response()->json(['success' => true, 'data' => $sector, 'canonical_url' => \App\Services\SectorOrganization::href($slug), 'specialization_of' => $slug === 'fintech' ? 'mercado-financeiro' : null]);
|
||||
$sector->setRelation('opportunities', SectorOrganization::opportunities($sector)->get());
|
||||
|
||||
return response()->json(['success' => true, 'data' => $sector, 'canonical_url' => SectorOrganization::href($slug), 'specialization_of' => $slug === 'fintech' ? 'mercado-financeiro' : null]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@ namespace App\Http\Controllers\Api\V1;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Services\SimulationFormDrafts;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use stdClass;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
class SimulationOfferDocumentController extends Controller
|
||||
{
|
||||
public function store(Request $request, string $id, SimulationFormDrafts $drafts)
|
||||
public function store(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
|
||||
{
|
||||
abort_unless(Str::isUuid($id), 404);
|
||||
$drafts->find($id, $request);
|
||||
@@ -20,6 +23,7 @@ class SimulationOfferDocumentController extends Controller
|
||||
$request->validate(['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,image/png,image/jpeg', 'max:10240']]);
|
||||
$file = $request->file('file');
|
||||
$hash = hash_file('sha256', $file->getRealPath());
|
||||
abort_unless(is_string($hash), 422, 'Não foi possível verificar o conteúdo do anexo.');
|
||||
$existing = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->where('sha256', $hash)->whereNull('deleted_at')->first();
|
||||
if ($existing) {
|
||||
return response()->json(['data' => $this->metadata($id, $existing)], 200)->header('Cache-Control', 'private, no-store');
|
||||
@@ -37,32 +41,37 @@ class SimulationOfferDocumentController extends Controller
|
||||
throw $e;
|
||||
}
|
||||
ScanDocumentForMalware::dispatch($documentId)->afterCommit();
|
||||
|
||||
return response()->json(['data' => $this->metadata($id, DB::table('documents')->where('id', $documentId)->first())], 201)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function index(Request $request, string $id, SimulationFormDrafts $drafts)
|
||||
public function index(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
|
||||
{
|
||||
abort_unless(Str::isUuid($id), 404);
|
||||
$drafts->find($id, $request);
|
||||
$documents = DB::table('documents')->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->get();
|
||||
|
||||
return response()->json(['data' => $documents->map(fn ($doc) => $this->metadata($id, $doc))])->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function download(Request $request, string $id, string $document, SimulationFormDrafts $drafts)
|
||||
public function download(Request $request, string $id, string $document, SimulationFormDrafts $drafts): BinaryFileResponse
|
||||
{
|
||||
abort_unless(Str::isUuid($id) && Str::isUuid($document), 404);
|
||||
$drafts->find($id, $request);
|
||||
$doc = DB::table('documents')->where('id', $document)->where('source_system', 'simulation-fiscal-offer')->where('external_id', $id)->whereNull('deleted_at')->first();
|
||||
abort_unless($doc, 404);
|
||||
abort_unless($doc !== null, 404);
|
||||
abort_unless($doc->status === 'active' && $doc->malware_scan_status === 'clean' && hash_equals(trim($doc->sha256), (string) $doc->malware_scanned_sha256), 409, 'Anexo conservado; descarga disponível após análise antimalware concluída.');
|
||||
$root = realpath(Storage::disk('local')->path('simulation-offers/'.$id));
|
||||
$path = realpath(Storage::disk('local')->path($doc->storage_key));
|
||||
abort_unless($root && $path && str_starts_with(strtolower($path), strtolower($root.DIRECTORY_SEPARATOR)) && is_file($path), 404);
|
||||
abort_unless(hash_equals(trim($doc->sha256), hash_file('sha256', $path)), 409, 'Conteúdo do anexo diverge do hash registado.');
|
||||
$hash = hash_file('sha256', $path);
|
||||
abort_unless(is_string($hash) && hash_equals(trim($doc->sha256), $hash), 409, 'Conteúdo do anexo diverge do hash registado.');
|
||||
|
||||
return response()->download($path, basename($doc->file_name), ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']);
|
||||
}
|
||||
|
||||
private function metadata(string $id, object $doc): array
|
||||
/** @return array{id: string, draftId: string, name: string, sha256: string, scan: string, url: string} */
|
||||
private function metadata(string $id, stdClass $doc): array
|
||||
{
|
||||
return ['id' => $doc->id, 'draftId' => $id, 'name' => $doc->file_name, 'sha256' => trim($doc->sha256),
|
||||
'scan' => $doc->malware_scan_status, 'url' => '/api/v1/simulation-drafts/'.$id.'/offer-documents/'.$doc->id];
|
||||
|
||||
@@ -7,9 +7,11 @@ use App\Services\CorridorAgribusinessOpportunities;
|
||||
use App\Services\OpportunityManagement;
|
||||
use App\Services\OpportunityPromotion;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
final class CorridorAgribusinessController extends Controller
|
||||
{
|
||||
@@ -18,31 +20,36 @@ final class CorridorAgribusinessController extends Controller
|
||||
return hash('sha256', json_encode($service->entries(), JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
public function show(Request $request, CorridorAgribusinessOpportunities $service)
|
||||
public function show(Request $request, CorridorAgribusinessOpportunities $service): Response
|
||||
{
|
||||
$access = app(PortalProfileAccess::class);
|
||||
abort_unless($access->internalAssignment($request->user(), 'investment_project') || $access->has($request->user(), 'publisher'), 403);
|
||||
$records = Opportunity::withTrashed()->whereIn('management_creation_key', array_column($service->entries(), 'key'))->get()->keyBy('management_creation_key');
|
||||
foreach ($records as $record) abort_unless($access->canView($request->user(), $record), 403);
|
||||
foreach ($records as $record) {
|
||||
abort_unless($access->canView($request->user(), $record), 403);
|
||||
}
|
||||
|
||||
return Inertia::render('management/corridor-agribusiness', ['manifestHash' => $this->manifest($service),
|
||||
'canRegister' => (bool) $access->internalAssignment($request->user(), 'investment_project'),
|
||||
'entries' => array_map(function ($entry) use ($records) {
|
||||
$r = $records->get($entry['key']);
|
||||
|
||||
return [...$entry, 'id' => $r?->id, 'status' => $r?->status, 'href' => $r ? '/oportunidades/'.$r->slug : null];
|
||||
}, $service->entries())]);
|
||||
}
|
||||
|
||||
public function register(Request $request, CorridorAgribusinessOpportunities $service)
|
||||
public function register(Request $request, CorridorAgribusinessOpportunities $service): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['manifest_hash' => 'required|string|size:64', 'confirm' => 'required|accepted']);
|
||||
abort_unless(hash_equals($this->manifest($service), $data['manifest_hash']), 409, 'A fonte mudou; recupere a prévia.');
|
||||
$access = app(PortalProfileAccess::class);
|
||||
$grant = $access->internalAssignment($request->user(), 'investment_project');
|
||||
abort_unless($grant && $access->mfa($request->user()), 403, 'Use a designação existente e MFA recente.');
|
||||
|
||||
return response()->json(['records' => $service->register($request->user(), $grant->organization_id)]);
|
||||
}
|
||||
|
||||
public function preview(Request $request, CorridorAgribusinessOpportunities $service)
|
||||
public function preview(Request $request, CorridorAgribusinessOpportunities $service): JsonResponse
|
||||
{
|
||||
$previews = [];
|
||||
foreach ($service->entries() as $entry) {
|
||||
@@ -52,21 +59,27 @@ final class CorridorAgribusinessController extends Controller
|
||||
$previews[] = ['id' => $record->id, ...app(OpportunityPromotion::class)->preview($request->user(), $record,
|
||||
[...$input, 'source_date' => $input['source_date'] ?: null, 'action' => 'disclose', 'etag' => app(OpportunityManagement::class)->etag($record)])];
|
||||
}
|
||||
|
||||
return response()->json(['previews' => $previews]);
|
||||
}
|
||||
|
||||
public function disclose(Request $request, CorridorAgribusinessOpportunities $service)
|
||||
public function disclose(Request $request, CorridorAgribusinessOpportunities $service): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['confirm' => 'required|accepted', 'reason' => 'required|string|min:20|max:1000',
|
||||
'previews' => 'required|array|size:34', 'previews.*.id' => 'required|integer|distinct',
|
||||
'previews.*.token' => 'required|string|max:300000', 'previews.*.operation_key' => 'required|uuid|distinct']);
|
||||
$expected = Opportunity::whereIn('management_creation_key', array_column($service->entries(), 'key'))->pluck('id')->sort()->values()->all();
|
||||
$supplied = array_column($data['previews'], 'id'); sort($supplied);
|
||||
$supplied = array_column($data['previews'], 'id');
|
||||
sort($supplied);
|
||||
abort_unless($expected === $supplied && count($expected) === 34, 409, 'A prévia deve identificar exactamente as 34 fichas deste lote.');
|
||||
|
||||
return response()->json(DB::transaction(function () use ($request, $data) {
|
||||
$result = [];
|
||||
foreach ($data['previews'] as $preview) $result[] = app(OpportunityPromotion::class)->confirm($request->user(), $preview['id'],
|
||||
[...$preview, 'confirm' => true, 'reason' => $data['reason']]);
|
||||
foreach ($data['previews'] as $preview) {
|
||||
$result[] = app(OpportunityPromotion::class)->confirm($request->user(), $preview['id'],
|
||||
[...$preview, 'confirm' => true, 'reason' => $data['reason']]);
|
||||
}
|
||||
|
||||
return ['records' => $result];
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Services\DocumentManagementAccess;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -12,7 +13,7 @@ class DocumentMalwareController extends Controller
|
||||
public function retry(Request $request, string $id): RedirectResponse
|
||||
{
|
||||
DB::transaction(function () use ($id, $request): void {
|
||||
$document = app(\App\Services\DocumentManagementAccess::class)->documents($request->user())->where('id', $id)->lockForUpdate()->first();
|
||||
$document = app(DocumentManagementAccess::class)->documents($request->user())->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($document && $document->status !== 'deleted' && $document->deleted_at === null, 404);
|
||||
abort_unless(in_array($document->malware_scan_status, ['pending', 'inconclusive', 'failed'], true), 422,
|
||||
'Só é possível repetir uma análise pendente, inconclusiva ou falhada. Para substituir um ficheiro, carregue uma nova versão.');
|
||||
|
||||
@@ -2,63 +2,79 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use App\Services\DocumentManagementAccess;
|
||||
use App\Services\EditorialInstallation;
|
||||
use App\Services\EditorialReading;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use Illuminate\Contracts\Encryption\DecryptException;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider;
|
||||
use Laravel\Fortify\Events\TwoFactorAuthenticationFailed;
|
||||
use Laravel\Fortify\Events\ValidTwoFactorAuthenticationCodeProvided;
|
||||
use Laravel\Fortify\Fortify;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EditorialArticleController extends Controller
|
||||
{
|
||||
public function installation(Request $request, \App\Services\EditorialInstallation $installation)
|
||||
public function installation(Request $request, EditorialInstallation $installation): Response
|
||||
{
|
||||
$plan = $installation->preview($request->user());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['plan' => $plan])->header('Cache-Control', 'private, no-store')
|
||||
: response()->view('management.editorial-installation', ['plan' => $plan, 'operationKey' => (string) \Illuminate\Support\Str::uuid(), 'receipt' => null])->header('Cache-Control', 'private, no-store');
|
||||
: response()->view('management.editorial-installation', ['plan' => $plan, 'operationKey' => (string) Str::uuid(), 'receipt' => null])->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function install(Request $request, \App\Services\EditorialInstallation $installation)
|
||||
public function install(Request $request, EditorialInstallation $installation): Response
|
||||
{
|
||||
$receipt = $installation->apply($request->user(), $request->all());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['receipt' => $receipt])->header('Cache-Control', 'private, no-store')
|
||||
: response()->view('management.editorial-installation', ['plan' => $installation->preview($request->user()), 'operationKey' => (string) \Illuminate\Support\Str::uuid(), 'receipt' => $receipt])->header('Cache-Control', 'private, no-store');
|
||||
: response()->view('management.editorial-installation', ['plan' => $installation->preview($request->user()), 'operationKey' => (string) Str::uuid(), 'receipt' => $receipt])->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function showMfa(Request $request)
|
||||
public function showMfa(Request $request): Response
|
||||
{
|
||||
return $this->confirmMfa($request);
|
||||
}
|
||||
|
||||
public function confirmMfa(Request $request)
|
||||
public function confirmMfa(Request $request): Response
|
||||
{
|
||||
$actor = $request->user();
|
||||
abort_unless($actor->two_factor_secret !== null && $actor->two_factor_confirmed_at !== null, 403, 'MFA configurado e confirmado necessário.');
|
||||
if (app(\App\Services\PortalProfileAccess::class)->mfa($actor)) {
|
||||
if (app(PortalProfileAccess::class)->mfa($actor)) {
|
||||
return redirect()->route('editorial.installation');
|
||||
}
|
||||
if ($request->isMethod('get')) {
|
||||
return response()->view('management.editorial-mfa')->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
abort_if(array_diff(array_keys($request->all()), ['code', 'recovery_code', '_token']), 422, 'A conta e o destino são obtidos da sessão e do servidor.');
|
||||
abort_if(array_diff(array_keys($request->all()), ['code', 'recovery_code', '_token']) !== [], 422, 'A conta e o destino são obtidos da sessão e do servidor.');
|
||||
$data = $request->validate(['code' => 'nullable|string|max:32', 'recovery_code' => 'nullable|string|max:255']);
|
||||
try {
|
||||
$recovery = collect($actor->recoveryCodes())->first(fn ($code) => isset($data['recovery_code']) && hash_equals($code, $data['recovery_code']));
|
||||
$valid = $recovery !== null || (!empty($data['code']) && app(\Laravel\Fortify\Contracts\TwoFactorAuthenticationProvider::class)->verify(
|
||||
\Laravel\Fortify\Fortify::currentEncrypter()->decrypt($actor->two_factor_secret), $data['code']
|
||||
$valid = $recovery !== null || (! empty($data['code']) && app(TwoFactorAuthenticationProvider::class)->verify(
|
||||
Fortify::currentEncrypter()->decrypt($actor->two_factor_secret), $data['code']
|
||||
));
|
||||
} catch (\Illuminate\Contracts\Encryption\DecryptException $e) {
|
||||
} catch (DecryptException $e) {
|
||||
$valid = false;
|
||||
}
|
||||
if (!$valid) {
|
||||
event(new \Laravel\Fortify\Events\TwoFactorAuthenticationFailed($actor));
|
||||
if (! $valid) {
|
||||
event(new TwoFactorAuthenticationFailed($actor));
|
||||
throw ValidationException::withMessages(['code' => 'Não foi possível confirmar o código MFA.']);
|
||||
}
|
||||
if ($recovery !== null) {
|
||||
$actor->replaceRecoveryCode($recovery);
|
||||
}
|
||||
event(new \Laravel\Fortify\Events\ValidTwoFactorAuthenticationCodeProvided($actor));
|
||||
event(new ValidTwoFactorAuthenticationCodeProvided($actor));
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->route('editorial.installation')->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
@@ -67,7 +83,7 @@ class EditorialArticleController extends Controller
|
||||
abort_unless(Schema::hasColumn('portal_resources', 'editorial_content'), 503, 'Editor editorial ainda não instalado.');
|
||||
}
|
||||
|
||||
private function publicResources()
|
||||
private function publicResources(): Builder
|
||||
{
|
||||
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
|
||||
->where('r.published', true)->where('r.rights_status', 'authorized')->where('d.rights_status', 'authorized')
|
||||
@@ -75,28 +91,29 @@ class EditorialArticleController extends Controller
|
||||
->where('d.malware_scan_status', 'clean')->whereColumn('d.sha256', 'd.malware_scanned_sha256');
|
||||
}
|
||||
|
||||
public function edit(int $id)
|
||||
public function edit(int $id): Response
|
||||
{
|
||||
$this->installed();
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource(request()->user(), $id);
|
||||
$r = DB::table('portal_resources')->find($id);
|
||||
abort_unless($r, 404);
|
||||
$reading = app(\App\Services\EditorialReading::class);
|
||||
app(DocumentManagementAccess::class)->assertEditableResource(request()->user(), $id);
|
||||
$r = DB::table('portal_resources')->where('id', $id)->first();
|
||||
abort_unless($r instanceof \stdClass, 404);
|
||||
$reading = app(EditorialReading::class);
|
||||
$original = $reading->original($r);
|
||||
$content = $reading->content($r, $original);
|
||||
return Inertia::render('portal/article-editor', ['resource' => ['id' => $r->id, 'title' => $r->title, 'metadata_version' => $r->metadata_version, 'published' => (bool) $r->published],
|
||||
|
||||
return tap(Inertia::render('portal/article-editor', ['resource' => ['id' => $r->id, 'title' => $r->title, 'metadata_version' => $r->metadata_version, 'published' => (bool) $r->published],
|
||||
'content' => $content, 'source' => $original ? array_intersect_key($original, array_flip(['source_url', 'published_at', 'modified_at', 'legacy_id', 'image_source_url'])) : null, 'images' => $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->get(['r.id', 'r.title']),
|
||||
'revisions' => DB::table('portal_resource_revisions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['revision_no', 'change_type', 'created_at'])])->toResponse(request())->header('Cache-Control', 'private, no-store');
|
||||
'revisions' => DB::table('portal_resource_revisions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['revision_no', 'change_type', 'created_at'])])->toResponse(request()), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
||||
}
|
||||
|
||||
public function save(Request $request, int $id)
|
||||
public function save(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
$this->installed();
|
||||
$data = $this->validateContent($request);
|
||||
DB::transaction(function () use ($request, $id, $data) {
|
||||
$r = DB::table('portal_resources')->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($r, 404);
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
abort_unless($r !== null, 404);
|
||||
app(DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
abort_unless((int) $r->metadata_version === (int) $data['metadata_version'], 409, 'O artigo foi alterado noutra sessão. Recarregue antes de editar.');
|
||||
if (! empty($data['image_resource_id'])) {
|
||||
abort_unless($this->publicResources()->where('r.id', $data['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->exists(), 422, 'Escolha uma imagem pública disponível na biblioteca.');
|
||||
@@ -108,7 +125,7 @@ class EditorialArticleController extends Controller
|
||||
$content = $this->edition($r, $data);
|
||||
$revision = (int) $r->metadata_version + 1;
|
||||
DB::table('portal_resources')->where('id', $id)->update(['editorial_content' => json_encode($content, JSON_THROW_ON_ERROR), 'published' => false, 'approved_by' => null, 'metadata_version' => $revision, 'last_reviewed_by' => $request->user()->id, 'last_reviewed_at' => now(), 'updated_at' => now()]);
|
||||
$resource = DB::table('portal_resources')->find($id);
|
||||
$resource = DB::table('portal_resources')->where('id', $id)->first();
|
||||
$links = DB::table('portal_resource_links')->where('resource_id', $id)->orderBy('id')->get();
|
||||
DB::table('portal_resource_revisions')->insert(['resource_id' => $id, 'revision_no' => $revision, 'change_type' => 'metadata_updated', 'snapshot' => json_encode(['resource' => $resource, 'associations' => $links], JSON_THROW_ON_ERROR), 'actor_user_id' => $request->user()->id, 'created_at' => now()]);
|
||||
DB::table('audit_events')->insert(['source_system' => 'portal-management', 'actor_user_id' => $request->user()->id, 'entity_type' => 'portal_resources', 'entity_id' => null, 'action' => 'editorial_updated', 'before_data' => json_encode(['record_id' => $id, 'metadata_version' => $r->metadata_version]), 'after_data' => json_encode(['record_id' => $id, 'metadata_version' => $revision, 'published' => false])]);
|
||||
@@ -117,46 +134,64 @@ class EditorialArticleController extends Controller
|
||||
return redirect('/management/portal-services/resources/'.$id.'/article')->with('success', 'Artigo guardado como rascunho. Publique expressamente no acervo quando estiver pronto.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function validateContent(Request $request): array
|
||||
{
|
||||
return $request->validate(['body' => ['required', 'string', 'max:100000'], 'image_resource_id' => ['nullable', 'integer'], 'image_mode' => ['nullable', 'in:original,selected,none'], 'metadata_version' => ['required', 'integer', 'min:1']]);
|
||||
}
|
||||
|
||||
private function edition(object $resource, array $data): array
|
||||
/**
|
||||
* @param array<array-key, mixed> $data
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function edition(\stdClass $resource, array $data): array
|
||||
{
|
||||
$reading = app(\App\Services\EditorialReading::class);
|
||||
$reading = app(EditorialReading::class);
|
||||
$original = $reading->original($resource);
|
||||
$mode = $data['image_mode'] ?? (!empty($data['image_resource_id']) ? 'selected' : 'none');
|
||||
if ($mode === 'selected') abort_unless(!empty($data['image_resource_id']) && $reading->publicResources()->where('r.id', $data['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->exists(), 422, 'Escolha uma imagem pública disponível na biblioteca.');
|
||||
return ['format' => 1, 'body' => trim($data['body']), 'image_mode' => $mode, 'image_resource_id' => $mode === 'selected' ? (int)$data['image_resource_id'] : null,
|
||||
$mode = $data['image_mode'] ?? (! empty($data['image_resource_id']) ? 'selected' : 'none');
|
||||
if ($mode === 'selected') {
|
||||
abort_unless(! empty($data['image_resource_id']) && $reading->publicResources()->where('r.id', $data['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->exists(), 422, 'Escolha uma imagem pública disponível na biblioteca.');
|
||||
}
|
||||
|
||||
return ['format' => 1, 'body' => trim($data['body']), 'image_mode' => $mode, 'image_resource_id' => $mode === 'selected' ? (int) $data['image_resource_id'] : null,
|
||||
'preserve_original_html' => $original && trim($data['body']) === $reading->plain($original['content_html'])];
|
||||
}
|
||||
|
||||
public function preview(Request $request, int $id)
|
||||
public function preview(Request $request, int $id): Response
|
||||
{
|
||||
$this->installed();
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
$resource = DB::table('portal_resources')->find($id); abort_unless($resource, 404);
|
||||
return Inertia::render('portal/article', ['article' => app(\App\Services\EditorialReading::class)->reading($resource, null, true)])
|
||||
->toResponse($request)->header('Cache-Control', 'private, no-store')->header('X-Robots-Tag', 'noindex, nofollow');
|
||||
app(DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
$resource = DB::table('portal_resources')->where('id', $id)->first();
|
||||
abort_unless($resource instanceof \stdClass, 404);
|
||||
|
||||
return tap(Inertia::render('portal/article', ['article' => app(EditorialReading::class)->reading($resource, null, true)])
|
||||
->toResponse($request), static function (Response $response): void {
|
||||
$response->headers->set('Cache-Control', 'private, no-store');
|
||||
$response->headers->set('X-Robots-Tag', 'noindex, nofollow');
|
||||
});
|
||||
}
|
||||
|
||||
public function previewContent(Request $request, int $id)
|
||||
public function previewContent(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$this->installed();
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
$resource = DB::table('portal_resources')->find($id); abort_unless($resource, 404);
|
||||
app(DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
$resource = DB::table('portal_resources')->where('id', $id)->first();
|
||||
abort_unless($resource instanceof \stdClass, 404);
|
||||
$data = $this->validateContent($request);
|
||||
abort_unless((int)$resource->metadata_version === (int)$data['metadata_version'], 409, 'A revisão mudou; recupere antes de rever a prévia.');
|
||||
return response()->json(['article' => app(\App\Services\EditorialReading::class)->reading($resource, $this->edition($resource, $data), true)])
|
||||
abort_unless((int) $resource->metadata_version === (int) $data['metadata_version'], 409, 'A revisão mudou; recupere antes de rever a prévia.');
|
||||
|
||||
return response()->json(['article' => app(EditorialReading::class)->reading($resource, $this->edition($resource, $data), true)])
|
||||
->header('Cache-Control', 'private, no-store')->header('X-Robots-Tag', 'noindex, nofollow');
|
||||
}
|
||||
|
||||
public function show(Request $request, int $id)
|
||||
public function show(Request $request, int $id): Response
|
||||
{
|
||||
$this->installed();
|
||||
$resource = app(\App\Services\EditorialReading::class)->publicResources()->where('r.id', $id)->first(['r.*']);
|
||||
abort_unless($resource, 404);
|
||||
return Inertia::render('portal/article', ['article' => app(\App\Services\EditorialReading::class)->reading($resource)])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
$resource = app(EditorialReading::class)->publicResources()->where('r.id', $id)->first(['r.*']);
|
||||
abort_unless($resource !== null, 404);
|
||||
|
||||
return tap(Inertia::render('portal/article', ['article' => app(EditorialReading::class)->reading($resource)])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Services\CountryCatalogue;
|
||||
use App\Services\ExistingBusinessDrafts;
|
||||
use App\Services\ExistingBusinessIntake;
|
||||
use App\Services\ExistingBusinessObjects;
|
||||
use App\Services\ExistingBusinessProfileCategory;
|
||||
use App\Services\ExistingBusinessVerification;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\Mne\BusinessRepository;
|
||||
@@ -67,7 +68,7 @@ class ExistingBusinessController extends Controller
|
||||
$business = app(BusinessRepository::class)->find($id);
|
||||
Gate::authorize('view', $business);
|
||||
app(CompatibilityShadow::class)->schedule($id);
|
||||
$props = ['countries' => app(CountryCatalogue::class)->all(), 'sectors' => Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']), 'formSchema' => BusinessFormSchema::fields(), 'modalityFormType' => \App\Services\ExistingBusinessProfileCategory::formType($business->modality), 'record' => $this->record($business), 'canEdit' => $request->user()->can('update', $business),
|
||||
$props = ['countries' => app(CountryCatalogue::class)->all(), 'sectors' => Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']), 'formSchema' => BusinessFormSchema::fields(), 'modalityFormType' => ExistingBusinessProfileCategory::formType($business->modality), 'record' => $this->record($business), 'canEdit' => $request->user()->can('update', $business),
|
||||
'owner' => $request->user()->can('owns', $business), 'canReview' => $request->user()->can('review', $business),
|
||||
'canTriage' => $request->user()->can('triage', $business),
|
||||
'canApproveLevelOne' => $request->user()->can('approveLevelOne', $business),
|
||||
|
||||
@@ -3,25 +3,32 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\InvestmentComplements;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
final class InvestmentComplementController extends Controller
|
||||
{
|
||||
public function index(Request $request, InvestmentComplements $service)
|
||||
public function index(Request $request, InvestmentComplements $service): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['origin' => [\Illuminate\Validation\Rule::requiredIf(!$request->has('contexts')), 'nullable', 'string', 'max:100'], 'territory_id' => 'nullable|uuid',
|
||||
$data = $request->validate(['origin' => [Rule::requiredIf(! $request->has('contexts')), 'nullable', 'string', 'max:100'], 'territory_id' => 'nullable|uuid',
|
||||
'contexts' => 'sometimes|array|max:100', 'contexts.*' => 'array:origin,territory_id', 'contexts.*.origin' => 'required|string|max:100', 'contexts.*.territory_id' => 'nullable|uuid']);
|
||||
$items = [];
|
||||
$issues = [];
|
||||
foreach ($data['contexts'] ?? [['origin' => $data['origin'], 'territory_id' => $data['territory_id'] ?? null]] as $context) {
|
||||
try {
|
||||
$items = [...$items, ...$service->options($context['origin'], $context['territory_id'] ?? null)];
|
||||
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException|\Symfony\Component\HttpKernel\Exception\HttpException $e) {
|
||||
$status = $e instanceof \Symfony\Component\HttpKernel\Exception\HttpException ? $e->getStatusCode() : 404;
|
||||
if (!isset($data['contexts']) || $status !== 404) throw $e;
|
||||
} catch (ModelNotFoundException|HttpException $e) {
|
||||
$status = $e instanceof HttpException ? $e->getStatusCode() : 404;
|
||||
if (! isset($data['contexts']) || $status !== 404) {
|
||||
throw $e;
|
||||
}
|
||||
$issues[] = ['origin' => $context['origin'], 'message' => 'Uma referência escolhida deixou de estar disponível para consulta pública. A relação guardada conserva-se no dossier; os outros complementos continuam disponíveis.'];
|
||||
}
|
||||
}
|
||||
|
||||
return response()->json(isset($data['contexts']) ? ['items' => $items, 'issues' => $issues] : $items)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,14 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\InvestmentDossier;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Services\CorridorAgribusinessOpportunities;
|
||||
use App\Services\InvestmentCatalogue;
|
||||
use App\Services\InvestmentComplements;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\InvestmentZones;
|
||||
use App\Services\OpportunityCorridorExplorer;
|
||||
use App\Services\SectorInvestment;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -26,9 +31,9 @@ class InvestmentDiscoveryController extends Controller
|
||||
}
|
||||
|
||||
return Inertia::render('investment/catalogue', ['filters' => $filters, 'catalogueUrl' => $request->routeIs('investment.education') ? '/educacao' : '/oportunidades', 'references' => $catalogue->references(false, $filters),
|
||||
'corridorGroups' => ($filters['view'] ?? '') === 'corridors' ? app(\App\Services\OpportunityCorridorExplorer::class)->groups($filters) : [],
|
||||
'corridorGroups' => ($filters['view'] ?? '') === 'corridors' ? app(OpportunityCorridorExplorer::class)->groups($filters) : [],
|
||||
'territorySelection' => app(InvestmentGeography::class)->selection($filters),
|
||||
'guides' => app(\App\Services\SectorInvestment::class)->guides(\App\Models\Sector::where('slug', $filters['sector'] ?? '')->where('is_active', true)->whereNotNull('published_at')->first()),
|
||||
'guides' => app(SectorInvestment::class)->guides(Sector::where('slug', $filters['sector'] ?? '')->where('is_active', true)->whereNotNull('published_at')->first()),
|
||||
'results' => $catalogue->query($filters)->paginate(12)->withQueryString()->through(fn ($record) => $catalogue->presentLocation($record)),
|
||||
'favourites' => $request->user() ? DB::table('opportunity_favourites')->where('user_id', $request->user()->id)->pluck('opportunity_id') : [],
|
||||
'territorial' => $this->territorial($filters)]);
|
||||
@@ -89,7 +94,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
$data = $request->validate(['zone_id' => ['nullable', 'uuid'], 'from_dossier' => ['nullable', 'uuid']]);
|
||||
$dossierOrigin = isset($data['from_dossier']) ? InvestmentPortfolioController::visible()->whereKey($data['from_dossier'])->firstOrFail() : null;
|
||||
if ($dossierOrigin) {
|
||||
$linked = in_array($record->id, app(\App\Services\CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossierOrigin), true);
|
||||
$linked = in_array($record->id, app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($dossierOrigin), true);
|
||||
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) {
|
||||
if ($dossierOrigin->$foreign && DB::table('opportunity_'.$relation)->where('opportunity_id', $record->id)->where($foreign, $dossierOrigin->$foreign)->exists()) {
|
||||
$linked = true;
|
||||
@@ -107,8 +112,8 @@ class InvestmentDiscoveryController extends Controller
|
||||
$returnFilters = $request->query();
|
||||
unset($returnFilters['zone_id'], $returnFilters['from_dossier']);
|
||||
|
||||
return Inertia::render('investment/detail', ['complements' => app(\App\Services\InvestmentComplements::class)->options('opportunity:'.$record->id), 'record' => $record, 'context' => $catalogue->context($record->id), 'zone_origin' => $origin,
|
||||
'guides' => app(\App\Services\SectorInvestment::class)->guides($record->sector),
|
||||
return Inertia::render('investment/detail', ['complements' => app(InvestmentComplements::class)->options('opportunity:'.$record->id), 'record' => $record, 'context' => $catalogue->context($record->id), 'zone_origin' => $origin,
|
||||
'guides' => app(SectorInvestment::class)->guides($record->sector),
|
||||
'dossierOrigin' => $dossierOrigin ? ['id' => $dossierOrigin->id, 'title' => $dossierOrigin->title] : null,
|
||||
'returnUrl' => $returnFilters === [] ? '/oportunidades' : '/oportunidades?'.http_build_query($returnFilters)]);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ use App\Models\InvestmentDossier;
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Project;
|
||||
use App\Services\ClusterCompetitiveness;
|
||||
use App\Services\CorridorAgribusinessOpportunities;
|
||||
use App\Services\InvestmentComplements;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\InvestmentRelations;
|
||||
use App\Services\InvestmentZones;
|
||||
@@ -112,7 +115,7 @@ class InvestmentPortfolioController extends Controller
|
||||
|
||||
$opportunities = Opportunity::published()->where(function ($query) use ($record) {
|
||||
$query->whereRaw('false');
|
||||
$query->orWhereIn('id', app(\App\Services\CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record));
|
||||
$query->orWhereIn('id', app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record));
|
||||
foreach (['clusters' => 'cluster_id', 'corridors' => 'corridor_id', 'projects' => 'project_id'] as $relation => $foreign) {
|
||||
if ($record->$foreign) {
|
||||
$query->orWhereIn('id', DB::table('opportunity_'.$relation)->where($foreign, $record->$foreign)->select('opportunity_id'));
|
||||
@@ -138,9 +141,9 @@ class InvestmentPortfolioController extends Controller
|
||||
->orderByDesc('updated_at')->limit(10)->get(['id', 'version', 'updated_at'])
|
||||
: collect();
|
||||
|
||||
return Inertia::render('portfolio/show', ['complements' => $management ? [] : app(\App\Services\InvestmentComplements::class)->options('portfolio:'.$record->id), 'record' => $record, 'related' => $related, 'management' => $management, 'zones' => $zones,
|
||||
return Inertia::render('portfolio/show', ['complements' => $management ? [] : app(InvestmentComplements::class)->options('portfolio:'.$record->id), 'record' => $record, 'related' => $related, 'management' => $management, 'zones' => $zones,
|
||||
'anchorProject' => $anchorProject, 'existingSubmissions' => $existingSubmissions,
|
||||
'competitiveness' => $record->cluster_id ? app(\App\Services\ClusterCompetitiveness::class)->publicData($record->cluster_id) : null,
|
||||
'competitiveness' => $record->cluster_id ? app(ClusterCompetitiveness::class)->publicData($record->cluster_id) : null,
|
||||
'geography' => app(InvestmentRelations::class)->dossier($record, $management),
|
||||
'origin' => $origin ? ['id' => $origin->id, 'title' => $origin->title] : null, 'opportunities' => $opportunities, 'chains' => $chains]);
|
||||
}
|
||||
@@ -149,6 +152,7 @@ class InvestmentPortfolioController extends Controller
|
||||
{
|
||||
self::visible()->findOrFail($id);
|
||||
$data = $request->validate(['catalogue_query' => ['nullable', 'string', 'max:4000']]);
|
||||
|
||||
return redirect('/carteira/'.$id.'?'.http_build_query([...$data, 'prepare_interest' => '1']));
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\Models\EconomicCluster;
|
||||
use App\Models\InvestmentGap;
|
||||
use App\Models\Project;
|
||||
use App\Models\ValueChain;
|
||||
use App\Services\ClusterCompetitiveness;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\PortalAudit;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -50,7 +51,7 @@ class InvestmentStructureController extends Controller
|
||||
->whereNull('origins.deleted_at')->whereNull('destinations.deleted_at')->whereNull('value_chain_links.deleted_at')
|
||||
->orderBy('origins.code')->orderBy('destinations.code')
|
||||
->get(['value_chain_links.id', 'origins.name as origin', 'destinations.name as destination', 'link_type']) : [],
|
||||
'competitiveness' => $structure === 'clusters' && $id ? app(\App\Services\ClusterCompetitiveness::class)->management($id) : null,
|
||||
'competitiveness' => $structure === 'clusters' && $id ? app(ClusterCompetitiveness::class)->management($id) : null,
|
||||
'viewerId' => $request->user()->id,
|
||||
'memberships' => $structure === 'clusters' && $id ? DB::table('cluster_projects')
|
||||
->join('projects', 'projects.id', '=', 'cluster_projects.project_id')
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Opportunity;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\InvestmentCatalogue;
|
||||
use App\Services\InvestmentConsumerScope;
|
||||
use App\Services\InvestmentWorkspace;
|
||||
use App\Services\InvestmentZones;
|
||||
use App\Services\InvestorContextConsumers;
|
||||
@@ -151,7 +152,7 @@ class InvestmentSubmissionController extends Controller
|
||||
public function bindContext(Request $request, string $id): JsonResponse
|
||||
{
|
||||
abort_unless(app(VirtualDesk::class)->installed(), 422, 'O vínculo ao percurso do investimento não está disponível neste destino. A manifestação e a sua origem permanecem guardadas.');
|
||||
abort_if(array_diff(array_keys($request->all()), ['case_id', 'case_version', 'version', '_token']), 422, 'Parâmetros de associação inválidos.');
|
||||
abort_if(array_diff(array_keys($request->all()), ['case_id', 'case_version', 'version', '_token']) !== [], 422, 'Parâmetros de associação inválidos.');
|
||||
$data = $request->validate(['case_id' => 'required|uuid', 'case_version' => 'required|integer|min:1', 'version' => 'required|integer|min:1']);
|
||||
$result = DB::transaction(function () use ($request, $id, $data) {
|
||||
// Serialize all links for this submission before locking the selected case.
|
||||
@@ -250,8 +251,12 @@ class InvestmentSubmissionController extends Controller
|
||||
}
|
||||
|
||||
$bound = DB::table('desk_cases')->where('submission_id', $request->route('id'))->where('user_id', $request->user()->id)->whereNotNull('journey_id')->first();
|
||||
if ($bound) $data['payload'] = app(\App\Services\InvestmentConsumerScope::class)->bind($request->user(), $bound->id, $data['payload'], $submitting);
|
||||
else unset($data['payload']['context_opportunity'], $data['payload']['context_opportunity_snapshot']);
|
||||
if ($bound) {
|
||||
$data['payload'] = app(InvestmentConsumerScope::class)->bind($request->user(), $bound->id, $data['payload'], $submitting);
|
||||
} else {
|
||||
unset($data['payload']['context_opportunity'], $data['payload']['context_opportunity_snapshot']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
@@ -271,7 +276,7 @@ class InvestmentSubmissionController extends Controller
|
||||
$version = DB::transaction(function () use ($request, $id, $data) {
|
||||
$row = $this->owned($request, $id, true, true);
|
||||
abort_unless($row->status === 'draft' && $row->version === (int) $data['version'], 409, 'Rascunho alterado. Recarregue antes de guardar.');
|
||||
$row->update([...(isset($data['payload']['context_opportunity']) ? ['opportunity_id'=>(int)substr($data['payload']['context_opportunity'],12)] : []), 'payload' => $this->preserveCountrySource($row, $data['payload']), 'version' => $row->version + 1]);
|
||||
$row->update([...(isset($data['payload']['context_opportunity']) ? ['opportunity_id' => (int) substr($data['payload']['context_opportunity'], 12)] : []), 'payload' => $this->preserveCountrySource($row, $data['payload']), 'version' => $row->version + 1]);
|
||||
InvestmentSpecificNeed::where('submission_id', $id)->delete();
|
||||
foreach ($data['needs'] ?? [] as $need) {
|
||||
InvestmentSpecificNeed::create([...$need, 'submission_id' => $id]);
|
||||
@@ -316,7 +321,7 @@ class InvestmentSubmissionController extends Controller
|
||||
}
|
||||
abort_unless($row->version === (int) $data['version'], 409, 'Rascunho alterado. Recarregue.');
|
||||
$scopeKey = $data['payload']['context_opportunity'] ?? null;
|
||||
$opportunity = $scopeKey ? Opportunity::published()->findOrFail(substr($scopeKey,12)) : ($row->opportunity_id ? Opportunity::published()->findOrFail($row->opportunity_id) : null);
|
||||
$opportunity = $scopeKey ? Opportunity::published()->findOrFail(substr($scopeKey, 12)) : ($row->opportunity_id ? Opportunity::published()->findOrFail($row->opportunity_id) : null);
|
||||
if ($row->dossier_id) {
|
||||
InvestmentPortfolioController::visible()->findOrFail($row->dossier_id);
|
||||
}
|
||||
@@ -344,7 +349,7 @@ class InvestmentSubmissionController extends Controller
|
||||
'incorporation_country_code' => $p['incorporation_country_code'] ?? null,
|
||||
...(isset($p['organization_country_snapshot']) ? ['organization_country_snapshot' => $p['organization_country_snapshot']] : []),
|
||||
...($zoneOrigin ? ['zone_origin' => (array) $zoneOrigin] : [])]]));
|
||||
$row->update(['opportunity_id'=>$opportunity?->id, 'payload' => $p, 'version' => $row->version + 1, 'interest_request_id' => $interest->id, 'status' => 'submitted']);
|
||||
$row->update(['opportunity_id' => $opportunity?->id, 'payload' => $p, 'version' => $row->version + 1, 'interest_request_id' => $interest->id, 'status' => 'submitted']);
|
||||
InvestmentSpecificNeed::where('submission_id', $id)->delete();
|
||||
foreach ($data['needs'] ?? [] as $need) {
|
||||
InvestmentSpecificNeed::create([...$need, 'submission_id' => $id]);
|
||||
|
||||
@@ -5,14 +5,17 @@ namespace App\Http\Controllers;
|
||||
use App\Models\InvestmentDossier;
|
||||
use App\Models\InvestmentZone;
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\EconomicReferenceCatalog;
|
||||
use App\Services\InvestmentCatalogue;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\InvestmentTerritorialTraceability;
|
||||
use App\Services\InvestmentZoneAccess;
|
||||
use App\Services\InvestmentZones;
|
||||
use App\Services\InvestmentZoneOperationalDetails;
|
||||
use App\Services\InvestmentZoneOperationalDetailsInstallation;
|
||||
use App\Services\InvestmentZoneOperationalProfile;
|
||||
use App\Services\InvestmentZonePathContext;
|
||||
use App\Services\InvestmentZoneProfileInstallation;
|
||||
use App\Services\InvestmentZones;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -33,7 +36,7 @@ class InvestmentZoneController extends Controller
|
||||
$zone->setAttribute('province', collect($territoryPath)->firstWhere('location_type', 'province')->name ?? $zone->province);
|
||||
$zone->setAttribute('district', collect($territoryPath)->firstWhere('location_type', 'district')->name ?? $zone->district);
|
||||
$zone->setAttribute('territory_path', $territoryPath);
|
||||
$zone->setAttribute('operational_details_public',app(\App\Services\InvestmentZoneOperationalDetails::class)->publicData($zone));
|
||||
$zone->setAttribute('operational_details_public', app(InvestmentZoneOperationalDetails::class)->publicData($zone));
|
||||
$publicFields = ['operational_details_public', 'version_no', 'published_at', 'id', 'code', 'category', 'physical_category', 'legal_regime', 'operational_status', 'development_status', 'availability_status', 'area_value', 'area_unit', 'area_components', 'source_evidence', 'activities', 'resources', 'aliases', 'historical_territory', 'data_as_of', 'sources_consulted_at', 'name', 'name_en', 'description', 'description_en', 'province', 'district', 'territory_path', 'validation_status', 'updated_at'];
|
||||
$locations = DB::table('investment_zone_locations as zl')->join('geo_locations as g', 'g.id', '=', 'zl.geo_location_id')
|
||||
->where('zl.zone_id', $id)->when(! $management, fn ($q) => $q->where('zl.validation_status', 'validated')->where('g.data_classification', 'public'))
|
||||
@@ -68,7 +71,7 @@ class InvestmentZoneController extends Controller
|
||||
});
|
||||
$props = ['record' => $management ? app(InvestmentZoneOperationalProfile::class)->management($zone) : app(InvestmentZoneOperationalProfile::class)->publicDetails($zone->only($publicFields)), 'source' => $source, 'operator' => $operator, 'management' => $management,
|
||||
'profileAvailable' => $management && app(InvestmentZoneOperationalProfile::class)->installed(),
|
||||
'zoneContext' => !$management && $zone->category === 'industrial_park' ? app(InvestmentZonePathContext::class)->context($id,(int)$zone->version_no) : null,
|
||||
'zoneContext' => ! $management && $zone->category === 'industrial_park' ? app(InvestmentZonePathContext::class)->context($id, (int) $zone->version_no) : null,
|
||||
'zoneExtensionsAvailable' => Schema::hasColumn('investment_zones', 'source_evidence'),
|
||||
'related' => $related->unique('id')->values(),
|
||||
'relations' => $management ? DB::table('investment_zone_relations')->where('zone_id', $id)->orderByDesc('created_at')->limit(100)->get() : [],
|
||||
@@ -77,7 +80,7 @@ class InvestmentZoneController extends Controller
|
||||
'dossiers' => InvestmentDossier::orderBy('title')->limit(200)->get(['id', 'title', 'project_id', 'cluster_id', 'corridor_id'])] : null,
|
||||
'opportunities' => $opportunities,
|
||||
'legalSources' => $management ? DB::table('investment_legal_sources')->orderByDesc('created_at')->limit(100)->get() : [],
|
||||
'economicActivities' => $management ? app(\App\Services\EconomicReferenceCatalog::class)->activities() : [],
|
||||
'economicActivities' => $management ? app(EconomicReferenceCatalog::class)->activities() : [],
|
||||
'operators' => $management ? DB::table('organizations')->whereNull('deleted_at')->orderBy('name')->limit(200)->get(['id', 'name']) : [],
|
||||
'links' => $management ? DB::table('investment_zone_links')->where('zone_id', $id)->orderByDesc('created_at')->limit(100)->get() : [],
|
||||
'locations' => $locations,
|
||||
@@ -99,7 +102,9 @@ class InvestmentZoneController extends Controller
|
||||
'Os filtros de desenvolvimento e disponibilidade estarão disponíveis após a migração do catálogo de zonas.');
|
||||
$path = app(InvestmentGeography::class)->selection($filters);
|
||||
$publicColumns = ['primary_territory_id', 'version_no', 'id', 'code', 'name', 'name_en', 'category', 'physical_category', 'legal_regime', 'operational_status', 'development_status', 'availability_status', 'area_value', 'area_unit', 'area_components', 'source_evidence', 'activities', 'resources', 'aliases', 'data_as_of', 'sources_consulted_at', 'province', 'district', 'description', 'description_en', 'validation_status', 'updated_at'];
|
||||
if(app(\App\Services\InvestmentZoneOperationalDetails::class)->installed()){$publicColumns[]='operational_details';}
|
||||
if (app(InvestmentZoneOperationalDetails::class)->installed()) {
|
||||
$publicColumns[] = 'operational_details';
|
||||
}
|
||||
$publicColumns = array_values(array_filter($publicColumns, fn ($column) => Schema::hasColumn('investment_zones', $column)));
|
||||
$rows = $zones->search($filters, $management)->orderBy('name')->orderBy('id')
|
||||
->paginate(12, $management ? ['*'] : $publicColumns)->withQueryString();
|
||||
@@ -113,11 +118,13 @@ class InvestmentZoneController extends Controller
|
||||
->when(! $management, fn ($q) => $q->where('zl.validation_status', 'validated')->where('g.data_classification', 'public'))
|
||||
->orderBy('g.location_type')->orderBy('g.name')->get(['g.id', 'g.name', 'g.location_type', 'zl.scope_type']));
|
||||
|
||||
if (! $management) { $zone->makeHidden('primary_territory_id'); }
|
||||
if (! $management) {
|
||||
$zone->makeHidden('primary_territory_id');
|
||||
}
|
||||
|
||||
return $management ? $zone : [...app(InvestmentZoneOperationalProfile::class)->publicDetails($zone->toArray()),'operational_details_public'=>app(\App\Services\InvestmentZoneOperationalDetails::class)->publicData($zone)];
|
||||
return $management ? $zone : [...app(InvestmentZoneOperationalProfile::class)->publicDetails($zone->toArray()), 'operational_details_public' => app(InvestmentZoneOperationalDetails::class)->publicData($zone)];
|
||||
});
|
||||
$props = ['detailsInstallation'=>$management ? app(\App\Services\InvestmentZoneOperationalDetailsInstallation::class)->preview($request->user()) : null, 'installation' => $management ? app(InvestmentZoneProfileInstallation::class)->preview($request->user()) : null, 'records' => $rows, 'filters' => $filters, 'management' => $management, 'territorySelection' => $path,
|
||||
$props = ['detailsInstallation' => $management ? app(InvestmentZoneOperationalDetailsInstallation::class)->preview($request->user()) : null, 'installation' => $management ? app(InvestmentZoneProfileInstallation::class)->preview($request->user()) : null, 'records' => $rows, 'filters' => $filters, 'management' => $management, 'territorySelection' => $path,
|
||||
'zoneExtensionsAvailable' => Schema::hasColumn('investment_zones', 'source_evidence')];
|
||||
|
||||
return $request->is('api/*') ? response()->json($props) : Inertia::render('investment/zones', $props);
|
||||
@@ -193,35 +200,35 @@ class InvestmentZoneController extends Controller
|
||||
'source_evidence.*.source_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
'area_value' => ['nullable', 'numeric', 'min:0'], 'area_unit' => ['nullable', 'string', 'max:24'],
|
||||
'aliases' => ['sometimes', 'array', 'max:20'], 'aliases.*' => ['string', 'max:120'],
|
||||
'operational_details' => ['sometimes','array'],
|
||||
'operational_details' => ['sometimes', 'array'],
|
||||
'activities' => ['sometimes', 'array', 'max:50'], 'activities.*' => ['array:id,name,status,source_reference,source_date,kind,scope,capacity_type,capacity_value,capacity_unit,focus,economic_activity_id,support_document_ids,private_note'],
|
||||
'activities.*.id' => ['nullable','uuid'],
|
||||
'activities.*.source_date' => ['nullable','date_format:Y-m-d'],
|
||||
'activities.*.kind' => ['nullable','in:infrastructure,service,access,other'],
|
||||
'activities.*.scope' => ['nullable','in:inside,regional,unknown'],
|
||||
'activities.*.capacity_type' => ['nullable','string','max:120'],
|
||||
'activities.*.capacity_value' => ['nullable','numeric','min:0'],
|
||||
'activities.*.capacity_unit' => ['nullable','string','max:40'],
|
||||
'activities.*.focus' => ['nullable','in:promotional,potential,installed,unknown'],
|
||||
'activities.*.economic_activity_id' => ['nullable','uuid','exists:economic_activities,id'],
|
||||
'activities.*.support_document_ids' => ['sometimes','array','max:10'],
|
||||
'activities.*.id' => ['nullable', 'uuid'],
|
||||
'activities.*.source_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
'activities.*.kind' => ['nullable', 'in:infrastructure,service,access,other'],
|
||||
'activities.*.scope' => ['nullable', 'in:inside,regional,unknown'],
|
||||
'activities.*.capacity_type' => ['nullable', 'string', 'max:120'],
|
||||
'activities.*.capacity_value' => ['nullable', 'numeric', 'min:0'],
|
||||
'activities.*.capacity_unit' => ['nullable', 'string', 'max:40'],
|
||||
'activities.*.focus' => ['nullable', 'in:promotional,potential,installed,unknown'],
|
||||
'activities.*.economic_activity_id' => ['nullable', 'uuid', 'exists:economic_activities,id'],
|
||||
'activities.*.support_document_ids' => ['sometimes', 'array', 'max:10'],
|
||||
'activities.*.support_document_ids.*' => ['uuid'],
|
||||
'activities.*.private_note' => ['nullable','string','max:2000'],
|
||||
'activities.*.private_note' => ['nullable', 'string', 'max:2000'],
|
||||
'activities.*.name' => ['required', 'string', 'max:300'], 'activities.*.status' => ['required', 'in:proposed,authorized,conditional,excluded,potential'],
|
||||
'activities.*.source_reference' => ['nullable', 'string', 'max:2000'],
|
||||
'resources' => ['sometimes', 'array', 'max:50'], 'resources.*' => ['array:id,name,status,source_reference,source_date,kind,scope,capacity_type,capacity_value,capacity_unit,focus,economic_activity_id,support_document_ids,private_note'],
|
||||
'resources.*.id' => ['nullable','uuid'],
|
||||
'resources.*.source_date' => ['nullable','date_format:Y-m-d'],
|
||||
'resources.*.kind' => ['nullable','in:infrastructure,service,access,other'],
|
||||
'resources.*.scope' => ['nullable','in:inside,regional,unknown'],
|
||||
'resources.*.capacity_type' => ['nullable','string','max:120'],
|
||||
'resources.*.capacity_value' => ['nullable','numeric','min:0'],
|
||||
'resources.*.capacity_unit' => ['nullable','string','max:40'],
|
||||
'resources.*.focus' => ['nullable','in:promotional,potential,installed,unknown'],
|
||||
'resources.*.economic_activity_id' => ['nullable','uuid','exists:economic_activities,id'],
|
||||
'resources.*.support_document_ids' => ['sometimes','array','max:10'],
|
||||
'resources.*.id' => ['nullable', 'uuid'],
|
||||
'resources.*.source_date' => ['nullable', 'date_format:Y-m-d'],
|
||||
'resources.*.kind' => ['nullable', 'in:infrastructure,service,access,other'],
|
||||
'resources.*.scope' => ['nullable', 'in:inside,regional,unknown'],
|
||||
'resources.*.capacity_type' => ['nullable', 'string', 'max:120'],
|
||||
'resources.*.capacity_value' => ['nullable', 'numeric', 'min:0'],
|
||||
'resources.*.capacity_unit' => ['nullable', 'string', 'max:40'],
|
||||
'resources.*.focus' => ['nullable', 'in:promotional,potential,installed,unknown'],
|
||||
'resources.*.economic_activity_id' => ['nullable', 'uuid', 'exists:economic_activities,id'],
|
||||
'resources.*.support_document_ids' => ['sometimes', 'array', 'max:10'],
|
||||
'resources.*.support_document_ids.*' => ['uuid'],
|
||||
'resources.*.private_note' => ['nullable','string','max:2000'],
|
||||
'resources.*.private_note' => ['nullable', 'string', 'max:2000'],
|
||||
'resources.*.name' => ['required', 'string', 'max:120'], 'resources.*.status' => ['required', 'in:existing,planned,unconfirmed'],
|
||||
'resources.*.source_reference' => ['nullable', 'string', 'max:2000'], 'data_as_of' => ['nullable', 'date_format:Y-m-d'],
|
||||
'sources_consulted_at' => ['nullable', 'date_format:Y-m-d'], 'parent_zone_id' => ['nullable', 'uuid', 'exists:investment_zones,id'],
|
||||
@@ -257,17 +264,26 @@ class InvestmentZoneController extends Controller
|
||||
$data['district'] = $path[1]->name ?? null;
|
||||
}
|
||||
|
||||
$saved=$zones->save($id, $data, $request->user()->id);
|
||||
$saved = $zones->save($id, $data, $request->user()->id);
|
||||
|
||||
return response()->json(app(InvestmentZoneOperationalProfile::class)->management($saved), $id ? 200 : 201);
|
||||
}
|
||||
|
||||
public function transition(Request $request, InvestmentZones $zones, string $id, string $action): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['version_no' => ['required', 'integer', 'min:1'], 'base_public_version'=>['required_if:action,reconcile','nullable','integer','min:1']]);
|
||||
if($action==='reconcile'){
|
||||
$request->validate(['base_public_version'=>'required|integer|min:1']);
|
||||
$saved=DB::transaction(function()use($id,$data,$request){$zone=InvestmentZone::whereKey($id)->lockForUpdate()->firstOrFail();abort_unless($zone->category==='industrial_park',422);return app(InvestmentZoneOperationalProfile::class)->reconcile($zone,(int)$data['version_no'],(int)$data['base_public_version'],$request->user()->id);});
|
||||
}else{$saved=$action === 'publish' ? $zones->publish($id,$data['version_no'],$request->user()->id) : $zones->suspend($id,$data['version_no'],$request->user()->id);}
|
||||
$data = $request->validate(['version_no' => ['required', 'integer', 'min:1'], 'base_public_version' => ['required_if:action,reconcile', 'nullable', 'integer', 'min:1']]);
|
||||
if ($action === 'reconcile') {
|
||||
$request->validate(['base_public_version' => 'required|integer|min:1']);
|
||||
$saved = DB::transaction(function () use ($id, $data, $request) {
|
||||
$zone = InvestmentZone::whereKey($id)->lockForUpdate()->firstOrFail();
|
||||
abort_unless($zone->category === 'industrial_park', 422);
|
||||
|
||||
return app(InvestmentZoneOperationalProfile::class)->reconcile($zone, (int) $data['version_no'], (int) $data['base_public_version'], $request->user()->id);
|
||||
});
|
||||
} else {
|
||||
$saved = $action === 'publish' ? $zones->publish($id, $data['version_no'], $request->user()->id) : $zones->suspend($id, $data['version_no'], $request->user()->id);
|
||||
}
|
||||
|
||||
return response()->json(app(InvestmentZoneOperationalProfile::class)->management($saved));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,82 +2,106 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Services\InvestmentComplements;
|
||||
use App\Services\InvestmentCorridorContext;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\InvestmentOpportunityChoices;
|
||||
use App\Services\InvestorContext;
|
||||
use App\Services\InvestorContextInstallation;
|
||||
use App\Services\InvestorPath;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
final class InvestorContextController extends Controller
|
||||
{
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function entryComplements(Request $request): array
|
||||
{
|
||||
$data = $request->validate(['complement_origin' => 'nullable|string|max:100', 'complement_code' => 'required_with:complement_origin|string|max:40',
|
||||
'complement_target' => 'nullable|string|max:100', 'complement_territory' => 'nullable|uuid']);
|
||||
if (empty($data['complement_origin'])) return [];
|
||||
return app(\App\Services\InvestmentComplements::class)->resolve([['origin' => $data['complement_origin'], 'code' => $data['complement_code'],
|
||||
if (empty($data['complement_origin'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return app(InvestmentComplements::class)->resolve([['origin' => $data['complement_origin'], 'code' => $data['complement_code'],
|
||||
'target' => $data['complement_target'] ?? null, 'territory_id' => $data['complement_territory'] ?? null]]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function entryChoices(Request $r, InvestorContext $context): array
|
||||
{
|
||||
$data = $r->validate(['source' => 'nullable|string|max:100', 'from_dossier' => 'nullable|uuid', 'opportunities' => 'sometimes|array|max:100', 'opportunities.*' => 'integer|distinct|min:1',
|
||||
'province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid']);
|
||||
$source = !empty($data['source']) ? $context->reference($data['source']) : null;
|
||||
$source = ! empty($data['source']) ? $context->reference($data['source']) : null;
|
||||
$ids = $data['opportunities'] ?? (($source['kind'] ?? '') === 'opportunity' ? [$source['id']] : []);
|
||||
abort_if(!empty($data['from_dossier']) && !$ids, 422, 'Escolha a oportunidade associada à ficha da carteira.');
|
||||
abort_if(! empty($data['from_dossier']) && ! $ids, 422, 'Escolha a oportunidade associada à ficha da carteira.');
|
||||
$path = app(InvestmentGeography::class)->selection($data);
|
||||
$input = array_map(fn($id) => ['source' => 'opportunity:'.$id, ...($path ? ['territory' => 'territory:'.$path[array_key_last($path)]->id] : [])], $ids);
|
||||
if (!empty($data['from_dossier'])) $input[0]['portfolio_origins'] = [$data['from_dossier']];
|
||||
$input = array_map(fn ($id) => ['source' => 'opportunity:'.$id, ...($path ? ['territory' => 'territory:'.$path[array_key_last($path)]->id] : [])], $ids);
|
||||
if (! empty($data['from_dossier'])) {
|
||||
$input[0]['portfolio_origins'] = [$data['from_dossier']];
|
||||
}
|
||||
foreach ($this->entryComplements($r) as $complement) {
|
||||
$keys = array_filter([$complement['origin']['key'], $complement['target']['key'] ?? null], fn($key) => str_starts_with($key, 'opportunity:'));
|
||||
$keys = array_filter([$complement['origin']['key'], $complement['target']['key'] ?? null], fn ($key) => str_starts_with($key, 'opportunity:'));
|
||||
foreach ($keys as $key) {
|
||||
if (in_array($key, array_column($input, 'source'), true)) continue;
|
||||
if (in_array($key, array_column($input, 'source'), true)) {
|
||||
continue;
|
||||
}
|
||||
$input[] = ['source' => $key, ...($complement['territory_id'] ? ['territory' => 'territory:'.$complement['territory_id']] : []),
|
||||
...(str_starts_with($complement['origin']['key'], 'portfolio:') ? ['portfolio_origins' => [substr($complement['origin']['key'], 10)]] : [])];
|
||||
}
|
||||
}
|
||||
return app(\App\Services\InvestmentOpportunityChoices::class)->resolve($input, [], true);
|
||||
|
||||
return app(InvestmentOpportunityChoices::class)->resolve($input, [], true);
|
||||
}
|
||||
|
||||
public function current(Request $r, InvestorContext $context)
|
||||
public function current(Request $r, InvestorContext $context): JsonResponse
|
||||
{
|
||||
return response()->json($context->current($r->user()));
|
||||
}
|
||||
|
||||
public function focus(Request $r, InvestorContext $context)
|
||||
public function focus(Request $r, InvestorContext $context): JsonResponse
|
||||
{
|
||||
return response()->json($context->focus($r->user(), $r->all()));
|
||||
}
|
||||
|
||||
public function show(Request $r, string $id, InvestorContext $context)
|
||||
public function show(Request $r, string $id, InvestorContext $context): JsonResponse
|
||||
{
|
||||
$data = $r->validate(['opportunity' => 'nullable|string|max:100']);
|
||||
|
||||
return response()->json($context->view($r->user(), $id, $data['opportunity'] ?? null));
|
||||
}
|
||||
|
||||
public function options(Request $r, InvestorContext $context)
|
||||
public function options(Request $r, InvestorContext $context): JsonResponse
|
||||
{
|
||||
$d = $r->validate(['q' => 'nullable|string|max:120']);
|
||||
|
||||
return response()->json($context->options($d['q'] ?? ''));
|
||||
}
|
||||
|
||||
public function corridorOptions(Request $r)
|
||||
public function corridorOptions(Request $r): JsonResponse
|
||||
{
|
||||
$data = $r->validate(['corridor' => ['nullable', \Illuminate\Validation\Rule::in(array_keys(config('investment_corridors')))], 'territory' => 'nullable|uuid']);
|
||||
$service = app(\App\Services\InvestmentCorridorContext::class);
|
||||
return response()->json(['corridors' => collect(config('investment_corridors'))->map(fn($entry,$key)=>['key'=>$key,'name'=>$entry['name'],'provinces'=>$service->provinces($key)])->values(),
|
||||
$data = $r->validate(['corridor' => ['nullable', Rule::in(array_keys(config('investment_corridors')))], 'territory' => 'nullable|uuid']);
|
||||
$service = app(InvestmentCorridorContext::class);
|
||||
|
||||
return response()->json(['corridors' => collect(Config::array('investment_corridors'))->map(fn ($entry, $key) => ['key' => $key, 'name' => $entry['name'], 'provinces' => $service->provinces($key)])->values(),
|
||||
'parks' => $service->parks($data['corridor'] ?? null, $data['territory'] ?? null)]);
|
||||
}
|
||||
|
||||
public function saveDraft(Request $r, string $id, InvestorContext $context)
|
||||
public function saveDraft(Request $r, string $id, InvestorContext $context): JsonResponse
|
||||
{
|
||||
return response()->json($context->saveDraft($r->user(), $id, $r->all()));
|
||||
}
|
||||
|
||||
public function edit(Request $r, string $id, InvestorContext $context)
|
||||
public function edit(Request $r, string $id, InvestorContext $context): Response
|
||||
{
|
||||
$data = $context->view($r->user(), $id);
|
||||
abort_unless($data['can_write'], 403);
|
||||
@@ -85,72 +109,84 @@ final class InvestorContextController extends Controller
|
||||
$entries = $this->entryChoices($r, $context);
|
||||
$choices = $entries ?: null;
|
||||
$source = $r->query('source') ? $context->reference($r->validate(['source' => 'required|string|max:100'])['source']) : null;
|
||||
if ($choices === null && ($source['kind'] ?? '') === 'opportunity') $choices = app(\App\Services\InvestmentOpportunityChoices::class)->resolve([['source' => $source['key']]]);
|
||||
$explicitChoices=$choices;
|
||||
$selectionKey=$r->validate(['selection_key'=>'nullable|uuid'])['selection_key']??null;
|
||||
$entryKey=fn($c)=>$c['opportunity']['key'].($selectionKey?'@'.$selectionKey:'');
|
||||
$entrySources=array_map($entryKey,$explicitChoices??[]);
|
||||
if ($choices !== null) $choices = app(\App\Services\InvestmentOpportunityChoices::class)->mergeEntries($data['context']['opportunity_choices'] ?? [], $choices);
|
||||
if ($choices === null && ($source['kind'] ?? '') === 'opportunity') {
|
||||
$choices = app(InvestmentOpportunityChoices::class)->resolve([['source' => $source['key']]]);
|
||||
}
|
||||
$explicitChoices = $choices;
|
||||
$selectionKey = $r->validate(['selection_key' => 'nullable|uuid'])['selection_key'] ?? null;
|
||||
$entryKey = fn ($c) => $c['opportunity']['key'].($selectionKey ? '@'.$selectionKey : '');
|
||||
$entrySources = array_map($entryKey, $explicitChoices ?? []);
|
||||
if ($choices !== null) {
|
||||
$choices = app(InvestmentOpportunityChoices::class)->mergeEntries($data['context']['opportunity_choices'] ?? [], $choices);
|
||||
}
|
||||
$draft = $context->draft($r->user(), $id);
|
||||
if ($draft && isset($draft['opportunity_choices'])) {
|
||||
$recovered = app(\App\Services\InvestmentOpportunityChoices::class)->resolve($draft['opportunity_choices'], $data['context']['opportunity_choices'] ?? [], true);
|
||||
$new=array_filter($explicitChoices??[],fn($c)=>!in_array($entryKey($c),$draft['entry_sources']??[],true));
|
||||
$choices = app(\App\Services\InvestmentOpportunityChoices::class)->mergeEntries($recovered, array_values($new));
|
||||
$recovered = app(InvestmentOpportunityChoices::class)->resolve($draft['opportunity_choices'], $data['context']['opportunity_choices'] ?? [], true);
|
||||
$new = array_filter($explicitChoices ?? [], fn ($c) => ! in_array($entryKey($c), $draft['entry_sources'] ?? [], true));
|
||||
$choices = app(InvestmentOpportunityChoices::class)->mergeEntries($recovered, array_values($new));
|
||||
}
|
||||
$differences = [];
|
||||
foreach ($explicitChoices ?? [] as $entry) {
|
||||
$kept = collect($choices ?? [])->firstWhere('opportunity.key', $entry['opportunity']['key']);
|
||||
if ($kept && ($kept['territory']['key'] ?? null) !== ($entry['territory']['key'] ?? null)) $differences[] = ['title'=>$entry['opportunity']['title'],'saved'=>$kept['territory']['title']??'Por definir','requested'=>$entry['territory']['title']??'Por definir'];
|
||||
if ($kept && ($kept['territory']['key'] ?? null) !== ($entry['territory']['key'] ?? null)) {
|
||||
$differences[] = ['title' => $entry['opportunity']['title'], 'saved' => $kept['territory']['title'] ?? 'Por definir', 'requested' => $entry['territory']['title'] ?? 'Por definir'];
|
||||
}
|
||||
}
|
||||
$complements = isset($draft['complements']) ? app(\App\Services\InvestmentComplements::class)->resolve($draft['complements'], $data['context']['complements'] ?? []) : ($data['context']['complements'] ?? []);
|
||||
$complements = isset($draft['complements']) ? app(InvestmentComplements::class)->resolve($draft['complements'], $data['context']['complements'] ?? []) : ($data['context']['complements'] ?? []);
|
||||
$entryTerritory = null;
|
||||
if (($source['kind'] ?? null) === 'portfolio') {
|
||||
$path = app(InvestmentGeography::class)->selection($r->validate(['province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid']));
|
||||
if ($path) $entryTerritory = $context->reference('territory:'.$path[array_key_last($path)]->id);
|
||||
if ($path) {
|
||||
$entryTerritory = $context->reference('territory:'.$path[array_key_last($path)]->id);
|
||||
}
|
||||
}
|
||||
foreach ($this->entryComplements($r) as $entry) {
|
||||
$tag = 'complement:'.hash('sha256', $entry['key'].'@'.($selectionKey ?? ''));
|
||||
$entrySources[] = $tag;
|
||||
if (!in_array($tag, $draft['entry_sources'] ?? [], true)) $complements = collect([...$complements, $entry])->unique('key')->values()->all();
|
||||
if (! in_array($tag, $draft['entry_sources'] ?? [], true)) {
|
||||
$complements = collect([...$complements, $entry])->unique('key')->values()->all();
|
||||
}
|
||||
}
|
||||
return Inertia::render('investor-path/context', ['entryTerritory' => $entryTerritory, 'entryComplements' => $complements, 'entryLocationDifferences'=>$differences,'entrySources'=>$entrySources,'draft' => $draft, 'requestedChoices' => $choices, 'initial' => $data, 'options' => $context->options(), 'source' => $source, 'territorySelection' => isset($data['context']['selection']['territory']) ? app(InvestmentGeography::class)->path($data['context']['selection']['territory']['id']) : ($entryTerritory ? app(InvestmentGeography::class)->path($entryTerritory['id']) : [])]);
|
||||
|
||||
return Inertia::render('investor-path/context', ['entryTerritory' => $entryTerritory, 'entryComplements' => $complements, 'entryLocationDifferences' => $differences, 'entrySources' => $entrySources, 'draft' => $draft, 'requestedChoices' => $choices, 'initial' => $data, 'options' => $context->options(), 'source' => $source, 'territorySelection' => isset($data['context']['selection']['territory']) ? app(InvestmentGeography::class)->path($data['context']['selection']['territory']['id']) : ($entryTerritory ? app(InvestmentGeography::class)->path($entryTerritory['id']) : [])]);
|
||||
}
|
||||
|
||||
public function preview(Request $r, string $id, InvestorContext $context)
|
||||
public function preview(Request $r, string $id, InvestorContext $context): JsonResponse
|
||||
{
|
||||
return response()->json($context->preview($r->user(), $id, $r->all()));
|
||||
}
|
||||
|
||||
public function commit(Request $r, string $id, InvestorContext $context)
|
||||
public function commit(Request $r, string $id, InvestorContext $context): JsonResponse
|
||||
{
|
||||
return response()->json($context->commit($r->user(), $id, $r->all()));
|
||||
}
|
||||
|
||||
public function start(Request $r, InvestorPath $path, InvestorContext $context)
|
||||
public function start(Request $r, InvestorPath $path, InvestorContext $context): Response
|
||||
{
|
||||
$d = $r->validate(['source' => 'nullable|string|max:100', 'step' => ['nullable', 'regex:/^S(0[1-9]|1[0-9])$/']]);
|
||||
$ref = isset($d['source']) ? $context->reference($d['source']) : null;
|
||||
$submissionId = $r->validate(['submission' => 'nullable|uuid'])['submission'] ?? null;
|
||||
$entrySubmission = null;
|
||||
if ($submissionId) {
|
||||
$submission = \App\Models\InvestmentSubmission::where('id', $submissionId)->where('user_id', $r->user()->id)->where('status', 'draft')->firstOrFail();
|
||||
$submission = InvestmentSubmission::where('id', $submissionId)->where('user_id', $r->user()->id)->where('status', 'draft')->firstOrFail();
|
||||
abort_unless(($ref['kind'] ?? null) === 'portfolio' && $submission->dossier_id === $ref['id'], 422, 'Conserve a ficha de origem da manifestação.');
|
||||
$entrySubmission = ['id' => $submission->id, 'version' => $submission->version];
|
||||
}
|
||||
|
||||
$choices = $this->entryChoices($r, $context);
|
||||
|
||||
return Inertia::render('investor-path/context-start', ['entrySubmission' => $entrySubmission, 'entryComplements' => $this->entryComplements($r), 'entryQuery' => $r->only(['complement_origin', 'complement_code', 'complement_target', 'complement_territory', 'from_dossier', 'province_id', 'district_id', 'admin_post_id', 'locality_id']), 'opportunityChoices' => $choices, 'contexts' => $path->contexts($r->user()), 'source' => $ref, 'step' => $d['step'] ?? null]);
|
||||
}
|
||||
|
||||
public function installation(Request $r, InvestorContextInstallation $install)
|
||||
public function installation(Request $r, InvestorContextInstallation $install): JsonResponse|Response
|
||||
{
|
||||
$plan = $install->preview($r->user());
|
||||
|
||||
return $r->expectsJson() ? response()->json($plan) : Inertia::render('investor-path/context-installation', ['plan' => $plan]);
|
||||
}
|
||||
|
||||
public function install(Request $r, InvestorContextInstallation $install)
|
||||
public function install(Request $r, InvestorContextInstallation $install): JsonResponse
|
||||
{
|
||||
return response()->json($install->apply($r->user(),$r->all()));
|
||||
return response()->json($install->apply($r->user(), $r->all()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\FintechInvestment;
|
||||
use App\Services\InvestorPath;
|
||||
use App\Services\InvestmentZonePathContext;
|
||||
use App\Services\InvestorPath;
|
||||
use App\Services\LegacyContent;
|
||||
use App\Services\SectorInvestment;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -39,24 +39,29 @@ class InvestorPathController extends Controller
|
||||
return $request->expectsJson() ? response()->json(['ok' => true]) : redirect('/investimentos/'.$id)->with('success', 'Setor associado ao mesmo dossier.');
|
||||
}
|
||||
|
||||
public function zoneStart(Request $request,string $zoneId,InvestorPath $path): JsonResponse|Response
|
||||
public function zoneStart(Request $request, string $zoneId, InvestorPath $path): JsonResponse|Response
|
||||
{
|
||||
abort_unless($path->available(),404);
|
||||
$data=$request->validate(['consulted_public_version'=>'required|integer|min:1']);
|
||||
$data=['contexts'=>$path->contexts($request->user()),'zoneSource'=>app(InvestmentZonePathContext::class)->context($zoneId,(int)$data['consulted_public_version'])];
|
||||
return $request->expectsJson()?response()->json($data):Inertia::render('investor-path/index',$data);
|
||||
abort_unless($path->available(), 404);
|
||||
$data = $request->validate(['consulted_public_version' => 'required|integer|min:1']);
|
||||
$data = ['contexts' => $path->contexts($request->user()), 'zoneSource' => app(InvestmentZonePathContext::class)->context($zoneId, (int) $data['consulted_public_version'])];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('investor-path/index', $data);
|
||||
}
|
||||
|
||||
public function zoneAttach(Request $request,string $id): JsonResponse|RedirectResponse
|
||||
public function zoneAttach(Request $request, string $id): JsonResponse|RedirectResponse
|
||||
{
|
||||
app(InvestmentZonePathContext::class)->attach($request->user(),$id,$request->all());
|
||||
return $request->expectsJson()?response()->json(['ok'=>true]):redirect('/investimentos/'.$id)->with('success','Referência do parque guardada; não constitui reserva ou autorização.');
|
||||
app(InvestmentZonePathContext::class)->attach($request->user(), $id, $request->all());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['ok' => true]) : redirect('/investimentos/'.$id)->with('success', 'Referência do parque guardada; não constitui reserva ou autorização.');
|
||||
}
|
||||
|
||||
public function create(Request $request, InvestorPath $path): JsonResponse|RedirectResponse
|
||||
{
|
||||
$zone=$request->has('zone_id') ? app(InvestmentZonePathContext::class)->input($request->all()) : null;
|
||||
if($zone){ $request->validate(['confirm'=>'required|accepted']); app(InvestmentZonePathContext::class)->context($zone['zone_id'],(int)$zone['consulted_public_version']); }
|
||||
$zone = $request->has('zone_id') ? app(InvestmentZonePathContext::class)->input($request->all()) : null;
|
||||
if ($zone) {
|
||||
$request->validate(['confirm' => 'required|accepted']);
|
||||
app(InvestmentZonePathContext::class)->context($zone['zone_id'], (int) $zone['consulted_public_version']);
|
||||
}
|
||||
$sector = $request->validate(['sector_slug' => 'nullable|string|max:150'])['sector_slug'] ?? null;
|
||||
if ($sector !== null) {
|
||||
app(SectorInvestment::class)->context($sector);
|
||||
@@ -79,7 +84,11 @@ class InvestorPathController extends Controller
|
||||
app(SectorInvestment::class)->attach($request->user(), $id, ['version' => $case['version'], 'sector_slug' => $sector]);
|
||||
}
|
||||
|
||||
if($zone){$case=$path->view($request->user(),$id)['case']; app(InvestmentZonePathContext::class)->attach($request->user(),$id,[...$zone,'version'=>$case['version'],'confirm'=>true]);}
|
||||
if ($zone) {
|
||||
$case = $path->view($request->user(), $id)['case'];
|
||||
app(InvestmentZonePathContext::class)->attach($request->user(), $id, [...$zone, 'version' => $case['version'], 'confirm' => true]);
|
||||
}
|
||||
|
||||
return $id;
|
||||
});
|
||||
|
||||
|
||||
@@ -377,6 +377,9 @@ class LandAssistanceController extends Controller
|
||||
'declaredOrigin' => $countries->describe($payload['declared_origin_country_code'] ?? null), 'territoryPaths' => $paths, 'returnDossier' => $this->dossierContext($case)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>|null
|
||||
*/
|
||||
private function dossierContext(LandAssistanceCase $case): ?array
|
||||
{
|
||||
$id = $case->payload['investor_path_case_id'] ?? null;
|
||||
|
||||
@@ -12,9 +12,12 @@ use App\Services\OpportunityManagement;
|
||||
use App\Services\OpportunityManagementInstallation;
|
||||
use App\Services\OpportunityMaturity;
|
||||
use App\Services\OpportunityProfileWorkflow;
|
||||
use App\Services\OpportunityPromotion;
|
||||
use App\Services\OpportunityResources;
|
||||
use App\Services\OpportunityWorkflowTasks;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\ProactiveInvestment;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Database\Eloquent\SoftDeletingScope;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -76,8 +79,8 @@ class OpportunityManagementController extends Controller
|
||||
$query->where('opportunity_category', $filters['category']);
|
||||
}
|
||||
if ($filters['sector_id'] ?? null) {
|
||||
$sector = Sector::find($filters['sector_id']);
|
||||
$query->whereIn('sector_id', $sector ? Sector::whereIn('slug', \App\Services\SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
$sector = Sector::whereKey($filters['sector_id'])->first();
|
||||
$query->whereIn('sector_id', $sector ? Sector::whereIn('slug', SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
}
|
||||
if ($filters['geography_scope'] ?? null) {
|
||||
$query->where('geography_scope', $filters['geography_scope']);
|
||||
@@ -103,7 +106,7 @@ class OpportunityManagementController extends Controller
|
||||
$records = $query->orderByDesc('id')->paginate(20)->withQueryString();
|
||||
$records->getCollection()->each(function (Opportunity $r) use ($access, $service, $request) {
|
||||
$r->setAttribute('can_edit', $access->canEdit($request->user(), $r));
|
||||
$r->setAttribute('can_promote', app(\App\Services\OpportunityPromotion::class)->allowed($request->user(), $r));
|
||||
$r->setAttribute('can_promote', app(OpportunityPromotion::class)->allowed($request->user(), $r));
|
||||
$r->setAttribute('working_status', $access->state($r));
|
||||
$r->setAttribute('publication', $r->publicationPresentation());
|
||||
$r->setAttribute('draft_title', $service->values($r)['canonical']['official_title'] ?? $r->title);
|
||||
@@ -131,7 +134,7 @@ class OpportunityManagementController extends Controller
|
||||
'profileReauthenticationRequired' => $record && $record->owner_user_id !== null
|
||||
? app(OpportunityProfileWorkflow::class)->reauthenticationRequired($request->user(), $record) : false,
|
||||
'publicationState' => $record?->publicationPresentation(),
|
||||
'promotionHref' => $record && app(\App\Services\OpportunityPromotion::class)->allowed($request->user(), $record) ? '/minhas-oportunidades/'.$record->id.'/divulgacao' : null,
|
||||
'promotionHref' => $record && app(OpportunityPromotion::class)->allowed($request->user(), $record) ? '/minhas-oportunidades/'.$record->id.'/divulgacao' : null,
|
||||
'canonicalResponse' => $record?->owner_user_id !== null ? app(OpportunityCanonicalResponse::class)->read($request->user(), $record) : null,
|
||||
'canEditDraft' => $record ? $access->canEdit($request->user(), $record) : false,
|
||||
'workflowParticipants' => $record && $record->owner_user_id !== null ? app(OpportunityProfileWorkflow::class)->participants($request->user(), $record) : ['validators' => [], 'publishers' => []],
|
||||
@@ -147,7 +150,7 @@ class OpportunityManagementController extends Controller
|
||||
Inertia::share('publicationPreview', app(OpportunityCanonicalContract::class)->publicProjection($values['canonical'] ?? []));
|
||||
$classification = $id ? DB::table('audit_events')->where('source_system', 'opportunity-origin-review')->where('after_data->opportunity_id', $id)->orderByDesc('occurred_at')->orderByDesc('id')->value('after_data') : null;
|
||||
|
||||
return Inertia::render('management/opportunity-draft', ['proactiveHistory' => app(\App\Services\ProactiveInvestment::class)->history($record), 'proactiveOptions' => app(\App\Services\ProactiveInvestment::class)->options($record), 'resourcesInstalled' => app(OpportunityResources::class)->installed(), 'record' => $record, 'values' => $values, 'etag' => $record ? $service->etag($record) : null, 'readOnly' => str_ends_with($request->path(), '/view') || ($record && ! $access->canEdit($request->user(), $record)), 'closedVersion' => $record ? $service->closed($record) : false, 'classification' => $classification ? json_decode($classification, true)['classification'] : 'undetermined', 'sectors' => Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']), 'categories' => OpportunityManagement::CATEGORIES,
|
||||
return Inertia::render('management/opportunity-draft', ['proactiveHistory' => app(ProactiveInvestment::class)->history($record), 'proactiveOptions' => app(ProactiveInvestment::class)->options($record), 'resourcesInstalled' => app(OpportunityResources::class)->installed(), 'record' => $record, 'values' => $values, 'etag' => $record ? $service->etag($record) : null, 'readOnly' => str_ends_with($request->path(), '/view') || ($record && ! $access->canEdit($request->user(), $record)), 'closedVersion' => $record ? $service->closed($record) : false, 'classification' => $classification ? json_decode($classification, true)['classification'] : 'undetermined', 'sectors' => Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']), 'categories' => OpportunityManagement::CATEGORIES,
|
||||
'maturityCatalogue' => ['version' => OpportunityMaturity::VERSION, 'stages' => OpportunityMaturity::STAGES, 'legacyVersion' => OpportunityMaturity::LEGACY, 'legacyLabels' => OpportunityMaturity::LEGACY_LABELS],
|
||||
'territoryPath' => app(InvestmentGeography::class)->path($values['primary_territory_id'] ?? null, true),
|
||||
'history' => $id ? DB::table('audit_events')->where('source_system', 'opportunity-management')->where('after_data->record_id', $id)->orderByDesc('occurred_at')->get(['id', 'action', 'actor_user_id', 'occurred_at']) : []]);
|
||||
|
||||
@@ -4,22 +4,24 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\OpportunityPromotion;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
final class OpportunityPromotionController extends Controller
|
||||
{
|
||||
public function show(Request $request, int $id, OpportunityPromotion $service): \Inertia\Response
|
||||
public function show(Request $request, int $id, OpportunityPromotion $service): Response
|
||||
{
|
||||
return Inertia::render('management/opportunity-promotion', $service->editor($request->user(), Opportunity::findOrFail($id)));
|
||||
}
|
||||
|
||||
public function preview(Request $request, int $id, OpportunityPromotion $service): \Illuminate\Http\JsonResponse
|
||||
public function preview(Request $request, int $id, OpportunityPromotion $service): JsonResponse
|
||||
{
|
||||
return response()->json($service->preview($request->user(), Opportunity::findOrFail($id), $request->all()));
|
||||
}
|
||||
|
||||
public function confirm(Request $request, int $id, OpportunityPromotion $service): \Illuminate\Http\JsonResponse
|
||||
public function confirm(Request $request, int $id, OpportunityPromotion $service): JsonResponse
|
||||
{
|
||||
return response()->json($service->confirm($request->user(), $id, $request->all()));
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ use App\Models\Opportunity;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\OpportunityImageProcessor;
|
||||
use App\Services\OpportunityLibrarySupport;
|
||||
use App\Services\OpportunityManagement;
|
||||
use App\Services\OpportunityResourceInstallation;
|
||||
use App\Services\OpportunityResources;
|
||||
use App\Services\OpportunityTerritorialPresentation;
|
||||
use App\Services\OrganizationCountryInstallation;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
@@ -98,12 +100,13 @@ class OpportunityResourceController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return Inertia::render('management/opportunity-resources', ['opportunity' => $opportunity, 'etag' => app(OpportunityManagement::class)->etag($opportunity), 'resources' => $resources, 'librarySources' => !app(\App\Services\PortalProfileAccess::class)->canEdit(request()->user(), $opportunity) ? [] : app(\App\Services\OpportunityLibrarySupport::class)->choices(request()->user()), 'existingLocations' => DB::table('opportunity_locations as l')->join('geo_locations as g', 'g.id', '=', 'l.geo_location_id')->where('opportunity_id', $id)->get(['g.id', 'g.name', 'g.location_type']), 'locationReference' => app(\App\Services\OpportunityTerritorialPresentation::class)->describe($opportunity, $resources['locations']), 'assessment' => $this->assessment($resources)]);
|
||||
return Inertia::render('management/opportunity-resources', ['opportunity' => $opportunity, 'etag' => app(OpportunityManagement::class)->etag($opportunity), 'resources' => $resources, 'librarySources' => ! app(PortalProfileAccess::class)->canEdit(request()->user(), $opportunity) ? [] : app(OpportunityLibrarySupport::class)->choices(request()->user()), 'existingLocations' => DB::table('opportunity_locations as l')->join('geo_locations as g', 'g.id', '=', 'l.geo_location_id')->where('opportunity_id', $id)->get(['g.id', 'g.name', 'g.location_type']), 'locationReference' => app(OpportunityTerritorialPresentation::class)->describe($opportunity, $resources['locations']), 'assessment' => $this->assessment($resources)]);
|
||||
}
|
||||
|
||||
public function reuseLibrary(Request $request, int $id): JsonResponse
|
||||
{
|
||||
$row = app(\App\Services\OpportunityLibrarySupport::class)->attach($request->user(), Opportunity::findOrFail($id), $request->all());
|
||||
$row = app(OpportunityLibrarySupport::class)->attach($request->user(), Opportunity::findOrFail($id), $request->all());
|
||||
|
||||
return response()->json(['id' => $row->id, 'message' => 'Original associado como suporte privado, sem copiar ficheiros ou publicar.']);
|
||||
}
|
||||
|
||||
@@ -150,15 +153,18 @@ class OpportunityResourceController extends Controller
|
||||
{
|
||||
abort_unless(in_array($kind, ['documents', 'images'], true), 404);
|
||||
$opportunity = Opportunity::findOrFail($id);
|
||||
app(\App\Services\PortalProfileAccess::class)->authorize(request()->user(), 'view', $opportunity);
|
||||
app(PortalProfileAccess::class)->authorize(request()->user(), 'view', $opportunity);
|
||||
abort_if($opportunity->status === 'archived', 404);
|
||||
$record = DB::table($service->table($kind).' as r')->join('documents as d', 'd.id', '=', 'r.document_id')->where('r.id', $resourceId)->where('r.opportunity_id', $id)->whereNull('r.deleted_at')->whereNull('d.deleted_at')->first(['d.*']);
|
||||
abort_unless($record !== null, 404, 'Recurso indisponível ou excluído logicamente.');
|
||||
abort_unless($record->status === 'active' && $record->malware_scan_status === 'clean' && hash_equals(trim($record->sha256), (string) $record->malware_scanned_sha256), 423, 'Scan pendente, inconclusivo ou inseguro. O ficheiro permanece em quarentena.');
|
||||
|
||||
$support = DB::table($service->table($kind))->where('id', $resourceId)->first();
|
||||
app(\App\Services\OpportunityLibrarySupport::class)->assertAvailable($support, $record);
|
||||
if (request()->filled('source_sha256')) abort_unless(hash_equals(trim($record->sha256), (string)request('source_sha256')), 409, 'O original já não corresponde à versão consultada.');
|
||||
app(OpportunityLibrarySupport::class)->assertAvailable($support, $record);
|
||||
if (request()->filled('source_sha256')) {
|
||||
abort_unless(hash_equals(trim($record->sha256), (string) request('source_sha256')), 409, 'O original já não corresponde à versão consultada.');
|
||||
}
|
||||
|
||||
return app(VerifiedDocumentBinary::class)->respond($record);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Models\ExistingBusiness;
|
||||
use App\Models\Sector;
|
||||
use App\Models\User;
|
||||
use App\Services\InvestmentCatalogue;
|
||||
use App\Services\InvestmentGeography;
|
||||
@@ -11,6 +12,7 @@ use App\Services\InvestmentRelations;
|
||||
use App\Services\PartnershipExchange;
|
||||
use App\Services\PartnershipMatching;
|
||||
use App\Services\PartnershipSources;
|
||||
use App\Services\SectorOrganization;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -45,9 +47,11 @@ class PartnershipExchangeController extends Controller
|
||||
foreach (['sector_id', 'partnership_type', 'kind'] as $field) {
|
||||
if (! empty($filters[$field])) {
|
||||
if ($field === 'sector_id') {
|
||||
$sector = \App\Models\Sector::find($filters[$field]);
|
||||
$query->whereIn('p.sector_id', $sector ? \App\Models\Sector::whereIn('slug', \App\Services\SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
} else { $query->where('p.'.$field, $filters[$field]); }
|
||||
$sector = Sector::whereKey($filters[$field])->first();
|
||||
$query->whereIn('p.sector_id', $sector ? Sector::whereIn('slug', SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
} else {
|
||||
$query->where('p.'.$field, $filters[$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (! empty($filters['q'])) {
|
||||
@@ -393,7 +397,7 @@ class PartnershipExchangeController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
return ['types' => config('partnerships.types'), 'criteria' => config('partnerships.criteria'), 'sources' => $sources, 'sectors' => \App\Models\Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']),
|
||||
return ['types' => config('partnerships.types'), 'criteria' => config('partnerships.criteria'), 'sources' => $sources, 'sectors' => Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']),
|
||||
'territories' => app(InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id', 'name', 'location_type', 'parent_id']),
|
||||
'organizations' => $request->user() ? DB::table('memberships as m')->join('organizations as o', 'o.id', '=', 'm.organization_id')->where('m.user_id', $request->user()->id)
|
||||
->whereNull('m.deleted_at')->whereNull('o.deleted_at')->whereIn('m.role', ['admin', 'manager', 'officer'])->get(['o.id', 'o.name']) : []];
|
||||
|
||||
@@ -15,6 +15,8 @@ use App\Services\InvestmentWorkspace;
|
||||
use App\Services\OpportunityManagement;
|
||||
use App\Services\PortalAudit;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\ScopedPortalAdministration;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -55,32 +57,38 @@ class PortalManagementController extends Controller
|
||||
|
||||
private function interestAssignmentEtag(InterestRequest $interest): string
|
||||
{
|
||||
return hash('sha256',json_encode([$interest->id,$interest->status,$interest->assigned_to,$interest->updated_at?->format('Y-m-d H:i:s.u')],JSON_THROW_ON_ERROR));
|
||||
return hash('sha256', json_encode([$interest->id, $interest->status, $interest->assigned_to, $interest->updated_at?->format('Y-m-d H:i:s.u')], JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
public function interestAssignments(Request $request): Response|JsonResponse
|
||||
{
|
||||
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()),403);
|
||||
$rows=InterestRequest::whereNotIn('status',['cancelled','closed','refused','converted'])->orderBy('id')->get(['id','reference','status','assigned_to','updated_at'])
|
||||
->map(fn($row)=>['id'=>$row->id,'protocol'=>$row->reference,'status'=>$row->status,'analyst_id'=>$row->assigned_to,'version'=>0,'etag'=>$this->interestAssignmentEtag($row),'due_at'=>null]);
|
||||
$data=['kind'=>'interest','requests'=>$rows,'analysts'=>$this->interestAssignees()->get(['id','name'])];
|
||||
return $request->expectsJson()?response()->json($data):Inertia::render('management/desk-assignments',$data);
|
||||
abort_unless(app(ScopedPortalAdministration::class)->administrative($request->user()), 403);
|
||||
$rows = InterestRequest::whereNotIn('status', ['cancelled', 'closed', 'refused', 'converted'])->orderBy('id')->get(['id', 'reference', 'status', 'assigned_to', 'updated_at'])
|
||||
->map(fn ($row) => ['id' => $row->id, 'protocol' => $row->reference, 'status' => $row->status, 'analyst_id' => $row->assigned_to, 'version' => 0, 'etag' => $this->interestAssignmentEtag($row), 'due_at' => null]);
|
||||
$data = ['kind' => 'interest', 'requests' => $rows, 'analysts' => $this->interestAssignees()->get(['id', 'name'])];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('management/desk-assignments', $data);
|
||||
}
|
||||
|
||||
public function assignInterest(Request $request, InterestRequest $interest): RedirectResponse|JsonResponse
|
||||
{
|
||||
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()),403);
|
||||
$data=$request->validate(['etag'=>'required|string|size:64','analyst_id'=>['required','integer',Rule::in($this->interestAssignees()->pluck('id'))],'reason'=>'required|string|min:10|max:2000'],['reason.required'=>'Explique o motivo da atribuição.','reason.min'=>'Explique o motivo da atribuição com pelo menos 10 caracteres.','analyst_id.required'=>'Seleccione o responsável.','analyst_id.in'=>'Seleccione um responsável autorizado.']);
|
||||
DB::transaction(function()use($request,$interest,$data){
|
||||
$row=InterestRequest::whereKey($interest->id)->lockForUpdate()->firstOrFail();
|
||||
if (!hash_equals($this->interestAssignmentEtag($row),$data['etag'])) throw ValidationException::withMessages(['etag'=>'A atribuição mudou. Recupere a lista actual e tente novamente.']);
|
||||
if (in_array($row->status,['cancelled','closed','refused','converted'],true)) throw ValidationException::withMessages(['status'=>'Este pedido já não permite uma nova atribuição. Recupere a lista actual.']);
|
||||
$owner=(int)($row->metadata['portal_user_id']??0);
|
||||
abort_if($owner===(int)$data['analyst_id'] || InvestmentSubmission::where('interest_request_id',$row->id)->where('user_id',$data['analyst_id'])->exists(),422,'O interessado não pode acompanhar o seu próprio pedido como equipa.');
|
||||
PortalAudit::save($row,['assigned_to'=>(int)$data['analyst_id']],$request->user()->id);
|
||||
app(\App\Services\PortalProfileAccess::class)->audit($request->user(),'interest_assigned',['interest_id'=>$row->id,'assigned_to'=>(int)$data['analyst_id'],'reason'=>$data['reason']]);
|
||||
abort_unless(app(ScopedPortalAdministration::class)->administrative($request->user()), 403);
|
||||
$data = $request->validate(['etag' => 'required|string|size:64', 'analyst_id' => ['required', 'integer', Rule::in($this->interestAssignees()->pluck('id'))], 'reason' => 'required|string|min:10|max:2000'], ['reason.required' => 'Explique o motivo da atribuição.', 'reason.min' => 'Explique o motivo da atribuição com pelo menos 10 caracteres.', 'analyst_id.required' => 'Seleccione o responsável.', 'analyst_id.in' => 'Seleccione um responsável autorizado.']);
|
||||
DB::transaction(function () use ($request, $interest, $data) {
|
||||
$row = InterestRequest::whereKey($interest->id)->lockForUpdate()->firstOrFail();
|
||||
if (! hash_equals($this->interestAssignmentEtag($row), $data['etag'])) {
|
||||
throw ValidationException::withMessages(['etag' => 'A atribuição mudou. Recupere a lista actual e tente novamente.']);
|
||||
}
|
||||
if (in_array($row->status, ['cancelled', 'closed', 'refused', 'converted'], true)) {
|
||||
throw ValidationException::withMessages(['status' => 'Este pedido já não permite uma nova atribuição. Recupere a lista actual.']);
|
||||
}
|
||||
$owner = (int) ($row->metadata['portal_user_id'] ?? 0);
|
||||
abort_if($owner === (int) $data['analyst_id'] || InvestmentSubmission::where('interest_request_id', $row->id)->where('user_id', $data['analyst_id'])->exists(), 422, 'O interessado não pode acompanhar o seu próprio pedido como equipa.');
|
||||
PortalAudit::save($row, ['assigned_to' => (int) $data['analyst_id']], $request->user()->id);
|
||||
app(PortalProfileAccess::class)->audit($request->user(), 'interest_assigned', ['interest_id' => $row->id, 'assigned_to' => (int) $data['analyst_id'], 'reason' => $data['reason']]);
|
||||
});
|
||||
return $request->expectsJson()?response()->json(['ok'=>true]):back()->with('success','Responsável atribuído.');
|
||||
|
||||
return $request->expectsJson() ? response()->json(['ok' => true]) : back()->with('success', 'Responsável atribuído.');
|
||||
}
|
||||
|
||||
public function interests(Request $request): Response
|
||||
@@ -95,7 +103,7 @@ class PortalManagementController extends Controller
|
||||
$query = InterestRequest::query()->with('assignee:id,name')->select([
|
||||
'id', 'reference', 'full_name', 'email', 'country', 'metadata', 'sector_interest', 'existing_business_id', 'modality', 'status', 'assigned_to', 'next_action', 'next_action_due_at', 'created_at',
|
||||
]);
|
||||
app(\App\Services\ScopedPortalAdministration::class)->interestQuery($query, $request->user());
|
||||
app(ScopedPortalAdministration::class)->interestQuery($query, $request->user());
|
||||
if ($country = $filters['country'] ?? null) {
|
||||
$values = app(CountryCatalogue::class)->filterValues($country);
|
||||
$query->whereIn(DB::raw('LOWER(BTRIM(country))'), $values);
|
||||
@@ -142,7 +150,7 @@ class PortalManagementController extends Controller
|
||||
|
||||
public function interest(InterestRequest $interest): Response
|
||||
{
|
||||
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->interest(request()->user(), $interest),403);
|
||||
abort_unless(app(ScopedPortalAdministration::class)->interest(request()->user(), $interest), 403);
|
||||
$history = DB::table('business_workflow_transitions')->where('interest_request_id', $interest->id)
|
||||
->orderByDesc('created_at')->limit(50)->get(['from_status', 'to_status', 'actor_id', 'reason', 'created_at'])
|
||||
->map(function ($row) {
|
||||
@@ -170,7 +178,7 @@ class PortalManagementController extends Controller
|
||||
|
||||
public function review(Request $request, InterestRequest $interest, InterestWorkflow $workflow): RedirectResponse|JsonResponse
|
||||
{
|
||||
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->interest(request()->user(), $interest),403);
|
||||
abort_unless(app(ScopedPortalAdministration::class)->interest(request()->user(), $interest), 403);
|
||||
$validated = $request->validate([
|
||||
'status' => ['required', Rule::in(self::STATES)],
|
||||
'note' => ['nullable', 'string', 'min:10', 'max:2000'],
|
||||
@@ -198,7 +206,7 @@ class PortalManagementController extends Controller
|
||||
|
||||
public function withdrawProponentSharingConsent(Request $request, InterestRequest $interest): RedirectResponse|JsonResponse
|
||||
{
|
||||
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->interest(request()->user(), $interest),403);
|
||||
abort_unless(app(ScopedPortalAdministration::class)->interest(request()->user(), $interest), 403);
|
||||
$data = $request->validate(['reason' => ['required', 'string', 'min:10', 'max:2000']]);
|
||||
$this->withdrawConsent($request, $interest, $data['reason'], 'share_contact_with_proponent');
|
||||
|
||||
@@ -208,7 +216,7 @@ class PortalManagementController extends Controller
|
||||
|
||||
public function withdrawContactConsent(Request $request, InterestRequest $interest): RedirectResponse|JsonResponse
|
||||
{
|
||||
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->interest(request()->user(), $interest),403);
|
||||
abort_unless(app(ScopedPortalAdministration::class)->interest(request()->user(), $interest), 403);
|
||||
$data = $request->validate(['reason' => ['required', 'string', 'min:10', 'max:2000']]);
|
||||
$this->withdrawConsent($request, $interest, $data['reason'], 'respond_to_interest');
|
||||
|
||||
@@ -301,8 +309,8 @@ class PortalManagementController extends Controller
|
||||
}
|
||||
}
|
||||
if ($resource === 'opportunities' && ($filters['sector_id'] ?? null)) {
|
||||
$sector = Sector::find($filters['sector_id']);
|
||||
$query->whereIn('sector_id', $sector ? Sector::whereIn('slug', \App\Services\SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
$sector = Sector::whereKey($filters['sector_id'])->first();
|
||||
$query->whereIn('sector_id', $sector ? Sector::whereIn('slug', SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
}
|
||||
|
||||
return Inertia::render('management/catalog', [
|
||||
@@ -319,7 +327,7 @@ class PortalManagementController extends Controller
|
||||
public function edit(string $resource, ?int $id = null): Response
|
||||
{
|
||||
$model = $resource === 'sectors' ? Sector::class : Opportunity::class;
|
||||
$record = $id ? $model::findOrFail($id) : null;
|
||||
$record = $id ? $model::whereKey($id)->firstOrFail() : null;
|
||||
|
||||
return Inertia::render('management/editor', [
|
||||
'resource' => $resource,
|
||||
@@ -335,7 +343,7 @@ class PortalManagementController extends Controller
|
||||
throw ValidationException::withMessages(['status' => 'Use Gestão de Oportunidades para guardar uma versão de rascunho; publicação não é uma edição.']);
|
||||
}
|
||||
$model = $resource === 'sectors' ? Sector::class : Opportunity::class;
|
||||
$record = $id ? $model::findOrFail($id) : new $model;
|
||||
$record = $id ? $model::whereKey($id)->firstOrFail() : new $model;
|
||||
$rules = $resource === 'sectors' ? [
|
||||
'name' => ['required', 'string', 'max:150'],
|
||||
'slug' => ['required', 'alpha_dash:ascii', 'max:180', Rule::unique('sectors')->ignore($record->id)],
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use App\Services\OpportunityCanonicalContract;
|
||||
use App\Services\OpportunityContacts;
|
||||
use App\Services\OpportunityManagement;
|
||||
@@ -51,6 +52,10 @@ class PortalProfileController extends Controller
|
||||
return Inertia::render('profiles/administration', ['requests' => DB::table('portal_profile_requests')->latest()->get(),
|
||||
'assignments' => DB::table('portal_role_assignments')->get(), 'pair' => DB::table('portal_superuser_pair')->first(),
|
||||
'operatorOrganizations' => app(PortalInternalDesignation::class)->organizations(),
|
||||
'staffAccounts' => User::query()->orderBy('name')->get(['id', 'name', 'email', 'email_verified_at', 'two_factor_confirmed_at'])->map(fn (User $user) => [
|
||||
'id' => $user->id, 'name' => $user->name, 'email' => $user->email,
|
||||
'verified' => $user->email_verified_at !== null, 'mfa' => $user->two_factor_confirmed_at !== null,
|
||||
]),
|
||||
'internalTypes' => OpportunityCanonicalContract::BASE_CATEGORIES,
|
||||
'actorId' => $request->user()->id, 'types' => OpportunityManagement::CATEGORIES,
|
||||
'organizations' => DB::table('organizations')->whereNull('deleted_at')->get(['id', 'name']),
|
||||
|
||||
@@ -30,6 +30,7 @@ class PortalProfileInstallationController extends Controller
|
||||
public function bootstrapPreview(Request $request, string $id, PortalProfileInstallation $installation): JsonResponse|Response
|
||||
{
|
||||
$plan = $installation->bootstrapPreview($request->user(), $id);
|
||||
|
||||
return $request->expectsJson() ? response()->json($plan) : Inertia::render('profiles/bootstrap-confirmation', $plan);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,20 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Models\InterestRequest;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\DocumentManagementAccess;
|
||||
use App\Services\EconomicReferenceCatalog;
|
||||
use App\Services\EditorialReading;
|
||||
use App\Services\IdempotencyConflict;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\LegacyContent;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\PublicDocumentPresentation;
|
||||
use App\Services\SectorOrganization;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
@@ -18,6 +25,7 @@ use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
@@ -26,6 +34,7 @@ use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class PortalServicesController extends Controller
|
||||
{
|
||||
@@ -70,7 +79,7 @@ class PortalServicesController extends Controller
|
||||
]);
|
||||
$props = ['section' => $page, 'filters' => $filters, 'countries' => app(CountryCatalogue::class)->all()];
|
||||
if ($page === 'sectors') {
|
||||
$props['sectors'] = \App\Services\SectorOrganization::catalog();
|
||||
$props['sectors'] = SectorOrganization::catalog();
|
||||
}
|
||||
if (in_array($page, ['documents', 'legislation'], true)) {
|
||||
$query = $this->resources();
|
||||
@@ -116,7 +125,7 @@ class PortalServicesController extends Controller
|
||||
'r.source_confirmation', 'r.legal_status', 'r.rights_status', 'r.last_reviewed_at', 'r.last_reviewed_by', 'r.independent_review', 'r.metadata_version', 'r.source_reference', 'r.essential',
|
||||
'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.issued_on', 'd.valid_until', 'd.version_no',
|
||||
])->paginate(20)->withQueryString();
|
||||
$pageResult->getCollection()->transform(fn ($resource) => app(\App\Services\PublicDocumentPresentation::class)->resource($resource));
|
||||
$pageResult->getCollection()->transform(fn ($resource) => app(PublicDocumentPresentation::class)->resource($resource));
|
||||
$props['resources'] = $pageResult;
|
||||
$props['documentTypes'] = (clone $this->resources())->distinct()->orderBy('r.document_type')->pluck('r.document_type');
|
||||
$props['languages'] = (clone $this->resources())->distinct()->orderBy('r.language')->pluck('r.language');
|
||||
@@ -188,7 +197,7 @@ class PortalServicesController extends Controller
|
||||
}
|
||||
$resources = $query->orderByDesc('r.essential')->orderBy('r.title')->limit(10)
|
||||
->get(['r.id', 'r.title', 'r.description', 'r.document_type', 'r.document_date', 'r.document_number', 'r.applicable_from', 'r.applicable_until', 'r.language', 'r.territorial_scope', 'r.legal_status', 'r.source_confirmation', 'r.source_url', 'd.issuer', 'd.media_type', 'd.size_bytes']);
|
||||
$resources->transform(fn ($resource) => app(\App\Services\PublicDocumentPresentation::class)->resource($resource));
|
||||
$resources->transform(fn ($resource) => app(PublicDocumentPresentation::class)->resource($resource));
|
||||
|
||||
return response()->json(['data' => $resources]);
|
||||
}
|
||||
@@ -266,7 +275,7 @@ class PortalServicesController extends Controller
|
||||
abort_unless($resource !== null, 404);
|
||||
$resource->territory_name = $resource->territory_id
|
||||
? DB::table('geo_locations')->where('id', $resource->territory_id)->value('name') : null;
|
||||
$resource = app(\App\Services\PublicDocumentPresentation::class)->resource($resource);
|
||||
$resource = app(PublicDocumentPresentation::class)->resource($resource);
|
||||
$back = $request->query('back');
|
||||
$backUrl = is_string($back) && (str_starts_with($back, '/documentos') || str_starts_with($back, '/legislacao')) && ! str_starts_with($back, '//')
|
||||
? $back : '/documentos';
|
||||
@@ -289,7 +298,7 @@ class PortalServicesController extends Controller
|
||||
|
||||
public function downloadForManager(Request $request, string $id): BinaryFileResponse|\Illuminate\Http\Response
|
||||
{
|
||||
$document = app(\App\Services\DocumentManagementAccess::class)->documents($request->user())->where('id', $id)->first();
|
||||
$document = app(DocumentManagementAccess::class)->documents($request->user())->where('id', $id)->first();
|
||||
abort_unless($document && $document->status === 'active' && $document->malware_scan_status === 'clean'
|
||||
&& hash_equals(trim($document->sha256), trim((string) $document->malware_scanned_sha256)), 404);
|
||||
|
||||
@@ -299,7 +308,7 @@ class PortalServicesController extends Controller
|
||||
public function manage(Request $request, MalwareScanner $scanner): Response
|
||||
{
|
||||
$managementFilters = $request->validate(['q' => 'nullable|string|max:150', 'state' => 'nullable|in:draft,published,withdrawn', 'intent' => 'nullable|in:document,legislation']);
|
||||
$access = app(\App\Services\DocumentManagementAccess::class);
|
||||
$access = app(DocumentManagementAccess::class);
|
||||
if (! $access->full($request->user())) {
|
||||
$documents = $access->documents($request->user())->where('status', '!=', 'deleted');
|
||||
$health = $scanner->health();
|
||||
@@ -314,6 +323,7 @@ class PortalServicesController extends Controller
|
||||
$resource->links = $links->get($resource->id, collect())->values();
|
||||
$resource->revisions = $revisions->get($resource->id, collect())->values();
|
||||
});
|
||||
|
||||
return Inertia::render('portal/manage', [
|
||||
'managementFilters' => $managementFilters, 'associationOptions' => $this->contentAssociations($request),
|
||||
'documentAccessOnly' => ! $editorial, 'editorialScopeOnly' => $editorial, 'resources' => $resources, 'partnerships' => [], 'territories' => [],
|
||||
@@ -371,6 +381,9 @@ class PortalServicesController extends Controller
|
||||
'possibleDuplicates' => DB::table('portal_resources')->whereNotNull('duplicate_of_resource_id')->count()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, mixed> $filters
|
||||
*/
|
||||
private function filterManagedResources(Builder $query, array $filters): Builder
|
||||
{
|
||||
if (! empty($filters['q'])) {
|
||||
@@ -384,23 +397,28 @@ class PortalServicesController extends Controller
|
||||
$query->$method(fn ($q) => $q->selectRaw('1')->from('portal_resource_revisions as history')->whereColumn('history.resource_id', 'r.id')->whereIn('history.change_type', ['unpublished', 'withdrawn']));
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function contentAssociations(Request $request): array
|
||||
{
|
||||
$access = app(\App\Services\DocumentManagementAccess::class);
|
||||
$access = app(DocumentManagementAccess::class);
|
||||
$resources = $access->full($request->user()) ? DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at')
|
||||
: ($access->editorial($request->user()) ? $access->editorialResources($request->user()) : null);
|
||||
$documents = $resources ? $resources->orderBy('r.title')->get(['r.id as value', 'r.title as label', 'r.document_type']) : collect();
|
||||
|
||||
return [
|
||||
'sector' => \App\Models\Sector::choices()->orderBy('name')->get(['id as value', 'name as label']),
|
||||
'territory' => app(\App\Services\InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id as value', 'name as label']),
|
||||
'opportunity' => app(PortalProfileAccess::class)->scope(\App\Models\Opportunity::query(), $request->user())->orderBy('title')->get(['id as value', 'title as label']),
|
||||
'sector' => Sector::choices()->orderBy('name')->get(['id as value', 'name as label']),
|
||||
'territory' => app(InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id as value', 'name as label']),
|
||||
'opportunity' => app(PortalProfileAccess::class)->scope(Opportunity::query(), $request->user())->orderBy('title')->get(['id as value', 'title as label']),
|
||||
'document' => $documents,
|
||||
'procedure' => $documents->where('document_type', 'procedure')->values(),
|
||||
'stage' => collect(config('virtual-desk.steps', []))->map(fn ($step, $key) => ['value' => $key, 'label' => $step['title']])->values(),
|
||||
'activity' => app(\App\Services\EconomicReferenceCatalog::class)->activities()->map(fn ($row) => ['value' => $row->id, 'label' => $row->display_label])->values(),
|
||||
'stage' => collect(Config::array('virtual-desk.steps', []))->map(fn ($step, $key) => ['value' => $key, 'label' => $step['title']])->values(),
|
||||
'activity' => app(EconomicReferenceCatalog::class)->activities()->map(fn ($row) => ['value' => $row->id, 'label' => $row->display_label])->values(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -445,7 +463,7 @@ class PortalServicesController extends Controller
|
||||
$id = DB::transaction(function () use ($data, $request): int {
|
||||
$document = DB::table('documents')->where('id', $data['document_id'])->where('status', '!=', 'deleted')->whereNull('deleted_at')->lockForUpdate()->first();
|
||||
abort_unless($document !== null, 422, 'Seleccione um documento existente e não eliminado.');
|
||||
abort_unless(app(\App\Services\DocumentManagementAccess::class)->documents($request->user())->where('id', $document->id)->exists(), 403);
|
||||
abort_unless(app(DocumentManagementAccess::class)->documents($request->user())->where('id', $document->id)->exists(), 403);
|
||||
$identity = $this->identityKey($data);
|
||||
if ($identity) {
|
||||
DB::select('SELECT pg_advisory_xact_lock(hashtext(?))', ['portal-resource-identity:'.$identity]);
|
||||
@@ -474,11 +492,11 @@ class PortalServicesController extends Controller
|
||||
DB::transaction(function () use ($data, $request, $id, $expectedVersion): void {
|
||||
$resource = DB::table('portal_resources')->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($resource !== null, 404);
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
app(DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
abort_if((int) $resource->metadata_version !== $expectedVersion, 409, 'O documento foi alterado por outra sessão. Recarregue a revisão antes de editar.');
|
||||
$document = DB::table('documents')->where('id', $data['document_id'])->where('status', '!=', 'deleted')->whereNull('deleted_at')->lockForUpdate()->first();
|
||||
abort_unless($document !== null, 422, 'Seleccione um documento existente e não eliminado.');
|
||||
abort_unless(app(\App\Services\DocumentManagementAccess::class)->documents($request->user())->where('id', $document->id)->exists(), 403);
|
||||
abort_unless(app(DocumentManagementAccess::class)->documents($request->user())->where('id', $document->id)->exists(), 403);
|
||||
$identity = $this->identityKey($data);
|
||||
if ($identity) {
|
||||
DB::select('SELECT pg_advisory_xact_lock(hashtext(?))', ['portal-resource-identity:'.$identity]);
|
||||
@@ -507,7 +525,7 @@ class PortalServicesController extends Controller
|
||||
|
||||
public function moderate(Request $request, string $type, int $id): RedirectResponse
|
||||
{
|
||||
$access = app(\App\Services\DocumentManagementAccess::class);
|
||||
$access = app(DocumentManagementAccess::class);
|
||||
if (! $access->full($request->user())) {
|
||||
abort_unless($type === 'resources', 403);
|
||||
$access->assertEditableResource($request->user(), $id);
|
||||
@@ -519,7 +537,9 @@ class PortalServicesController extends Controller
|
||||
$table = $type === 'resources' ? 'portal_resources' : 'portal_partnerships';
|
||||
DB::transaction(function () use ($request, $data, $table, $type, $id) {
|
||||
abort_unless(DB::table($table)->where('id', $id)->lockForUpdate()->first() !== null, 404);
|
||||
if ($type === 'resources') app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
if ($type === 'resources') {
|
||||
app(DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
}
|
||||
if ($type === 'resources' && $data['published']) {
|
||||
$resource = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
|
||||
->where('r.id', $id)->first(['r.document_type', 'r.legislation', 'r.source_confirmation', 'r.legal_status', 'r.rights_status',
|
||||
@@ -832,17 +852,6 @@ class PortalServicesController extends Controller
|
||||
&& DB::table('migrations')->where('migration', '2026_10_06_200000_add_document_requirement_evidence_links')->exists();
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function resourceLinkColumns(): array
|
||||
{
|
||||
$columns = ['target_type', 'target_key', 'relationship', 'applicability', 'condition_text', 'basis_reference'];
|
||||
if (Schema::hasColumn('portal_resource_links', 'review_status')) {
|
||||
$columns[] = 'review_status';
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function territoryScopeIds(string $id): array
|
||||
{
|
||||
@@ -866,17 +875,23 @@ SQL, [$id]);
|
||||
|
||||
return $ids ?: [$id];
|
||||
}
|
||||
|
||||
private function articleUrl(int $id): ?string
|
||||
{
|
||||
if (!Schema::hasColumn('portal_resources', 'editorial_content')) return null;
|
||||
$resource = DB::table('portal_resources')->find($id);
|
||||
if (!$resource) return null;
|
||||
if ($resource->editorial_content !== null) return '/artigos/'.$id;
|
||||
if (! Schema::hasColumn('portal_resources', 'editorial_content')) {
|
||||
return null;
|
||||
}
|
||||
$resource = DB::table('portal_resources')->where('id', $id)->first();
|
||||
if (! $resource) {
|
||||
return null;
|
||||
}
|
||||
if ($resource->editorial_content !== null) {
|
||||
return '/artigos/'.$id;
|
||||
}
|
||||
try {
|
||||
return app(\App\Services\EditorialReading::class)->original($resource) ? '/artigos/'.$resource->id : null;
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException) {
|
||||
return app(EditorialReading::class)->original($resource) ? '/artigos/'.$resource->id : null;
|
||||
} catch (HttpException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Services\ProactiveSupply;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
final class ProactiveSupplyController extends Controller
|
||||
{
|
||||
public function index(Request $request,ProactiveSupply $service): JsonResponse
|
||||
public function index(Request $request, ProactiveSupply $service): JsonResponse
|
||||
{
|
||||
$v=$request->validate(['record_id'=>'nullable|integer|min:1']);
|
||||
$record=isset($v['record_id'])?Opportunity::findOrFail($v['record_id']):null;
|
||||
return response()->json($service->search($request->user(),$request->all(),$record))->header('Cache-Control','private, no-store');
|
||||
$v = $request->validate(['record_id' => 'nullable|integer|min:1']);
|
||||
$record = isset($v['record_id']) ? Opportunity::whereKey($v['record_id'])->firstOrFail() : null;
|
||||
|
||||
return response()->json($service->search($request->user(), $request->all(), $record))->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,22 +3,26 @@
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Procurement;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ProcurementController extends Controller
|
||||
{
|
||||
private const STATE_SQL = "CASE WHEN v.payload->>'procedural_state' IN ('cancelled','results') THEN v.payload->>'procedural_state' WHEN ((v.payload->>'deadline')::timestamp AT TIME ZONE (v.payload->>'timezone'))<=CURRENT_TIMESTAMP THEN 'deadline_ended' WHEN v.payload->>'extended'='true' THEN 'extended' ELSE 'open' END";
|
||||
|
||||
private function query(Procurement $service)
|
||||
private function query(Procurement $service): Builder
|
||||
{
|
||||
abort_unless($service->available(), 404);
|
||||
|
||||
return DB::table('portal_tenders as t')->join('portal_tender_versions as v', fn ($q) => $q->on('v.id', '=', 't.published_version_id')->on('v.tender_id', '=', 't.id'))->whereNotNull('v.published_at');
|
||||
}
|
||||
|
||||
public function index(Request $request, Procurement $service)
|
||||
public function index(Request $request, Procurement $service): Response
|
||||
{
|
||||
$filters = $request->validate(['q' => 'nullable|string|max:150', 'entity' => 'nullable|uuid', 'sector' => 'nullable|integer', 'territory' => 'nullable|uuid', 'modality' => 'nullable|string|max:200', 'financier' => 'nullable|string|max:200', 'state' => 'nullable|in:open,extended,deadline_ended,cancelled,results', 'deadline_before' => 'nullable|date_format:Y-m-d', 'regime' => 'nullable|in:state,external,private,other']);
|
||||
$filters = array_filter($filters, fn ($value) => $value !== null && $value !== '');
|
||||
@@ -48,9 +52,12 @@ class ProcurementController extends Controller
|
||||
$options[$name] = $facets->filter(fn ($p) => ! empty($p[$value]))->map(fn ($p) => ['id' => (string) $p[$value], 'name' => $p[$label]])->unique('id')->values();
|
||||
}
|
||||
|
||||
return Inertia::render('procurement/index', ['items' => $rows, 'filters' => $filters, 'options' => $options, 'regimes' => config('procurement.regimes'), 'management' => $service->access($request->user())])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
return tap(Inertia::render('procurement/index', ['items' => $rows, 'filters' => $filters, 'options' => $options, 'regimes' => config('procurement.regimes'), 'management' => $service->access($request->user())])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function summary(Procurement $service, string $vid): array
|
||||
{
|
||||
$v = $service->publishedVersion($vid);
|
||||
@@ -58,62 +65,62 @@ class ProcurementController extends Controller
|
||||
return ['publication' => $v, 'state' => $service->state($v)];
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id, Procurement $service)
|
||||
public function show(Request $request, string $id, Procurement $service): Response
|
||||
{
|
||||
return $this->detail($request, $id, $service, false);
|
||||
}
|
||||
|
||||
public function print(Request $request, string $id, Procurement $service)
|
||||
public function print(Request $request, string $id, Procurement $service): Response
|
||||
{
|
||||
return $this->detail($request, $id, $service, true);
|
||||
}
|
||||
|
||||
private function detail(Request $request, string $id, Procurement $service, bool $print)
|
||||
private function detail(Request $request, string $id, Procurement $service, bool $print): Response
|
||||
{
|
||||
$row = $this->query($service)->where('t.id', $id)->firstOrFail(['t.id', 'v.id as vid']);
|
||||
$filters = $request->only(['q', 'entity', 'sector', 'territory', 'modality', 'financier', 'state', 'deadline_before', 'regime', 'page']);
|
||||
|
||||
return Inertia::render('procurement/show', ['id' => $id, ...$this->summary($service, $row->vid), 'printMode' => $print, 'backUrl' => '/concursos'.($filters ? '?'.http_build_query($filters) : ''),
|
||||
return tap(Inertia::render('procurement/show', ['id' => $id, ...$this->summary($service, $row->vid), 'printMode' => $print, 'backUrl' => '/concursos'.($filters ? '?'.http_build_query($filters) : ''),
|
||||
'history' => DB::table('portal_tender_versions')->where('tender_id', $id)->whereNotNull('published_at')->orderBy('version')->pluck('id')->map(fn ($vid) => $service->publishedVersion($vid)),
|
||||
'watching' => $request->user() && DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->exists()])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
'watching' => $request->user() && DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->exists()])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
||||
}
|
||||
|
||||
public function manage(Request $request, Procurement $service)
|
||||
public function manage(Request $request, Procurement $service): Response
|
||||
{
|
||||
$service->guard($request->user());
|
||||
|
||||
return Inertia::render('procurement/manage', ['items' => DB::table('portal_tenders')->orderByDesc('updated_at')->get(['id', 'draft', 'version', 'published_version_id'])->map(fn ($r) => ['id' => $r->id, 'title' => json_decode($r->draft, true)['title'] ?? 'Rascunho sem título', 'version' => $r->version, 'published' => (bool) $r->published_version_id])])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
return tap(Inertia::render('procurement/manage', ['items' => DB::table('portal_tenders')->orderByDesc('updated_at')->get(['id', 'draft', 'version', 'published_version_id'])->map(fn ($r) => ['id' => $r->id, 'title' => json_decode($r->draft, true)['title'] ?? 'Rascunho sem título', 'version' => $r->version, 'published' => (bool) $r->published_version_id])])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
||||
}
|
||||
|
||||
public function edit(Request $request, Procurement $service, ?string $id = null)
|
||||
public function edit(Request $request, Procurement $service, ?string $id = null): Response
|
||||
{
|
||||
$service->guard($request->user());
|
||||
$r = $id ? $service->record($id) : null;
|
||||
|
||||
return Inertia::render('procurement/edit', ['record' => $r ? ['id' => $id, 'version' => $r->version, 'draft' => json_decode($r->draft, true), 'published' => (bool) $r->published_version_id] : null,
|
||||
'references' => $service->references($request->user()), 'history' => $id ? DB::table('portal_tender_versions')->where('tender_id', $id)->orderByDesc('version')->get(['version', 'act', 'reason', 'actor_id', 'published_at', 'created_at']) : []])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
return tap(Inertia::render('procurement/edit', ['record' => $r ? ['id' => $id, 'version' => $r->version, 'draft' => json_decode($r->draft, true), 'published' => (bool) $r->published_version_id] : null,
|
||||
'references' => $service->references($request->user()), 'history' => $id ? DB::table('portal_tender_versions')->where('tender_id', $id)->orderByDesc('version')->get(['version', 'act', 'reason', 'actor_id', 'published_at', 'created_at']) : []])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
||||
}
|
||||
|
||||
public function save(Request $request, Procurement $service, ?string $id = null)
|
||||
public function save(Request $request, Procurement $service, ?string $id = null): RedirectResponse
|
||||
{
|
||||
$id = $service->save($request->user(), $id, $request->all());
|
||||
|
||||
return redirect('/management/concursos/'.$id)->with('success', 'Rascunho guardado. A publicação anterior permanece intacta.');
|
||||
}
|
||||
|
||||
public function preview(Request $request, string $id, Procurement $service)
|
||||
public function preview(Request $request, string $id, Procurement $service): JsonResponse
|
||||
{
|
||||
return response()->json($service->preview($request->user(), $id, (string) $request->input('act')))->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function publish(Request $request, string $id, Procurement $service)
|
||||
public function publish(Request $request, string $id, Procurement $service): RedirectResponse
|
||||
{
|
||||
$service->publish($request->user(), $id, $request->all());
|
||||
|
||||
return redirect('/management/concursos/'.$id)->with('success', 'Acto publicado, com versão e auditoria. Nenhuma proposta recebida pelo portal.');
|
||||
}
|
||||
|
||||
public function watch(Request $request, string $id, Procurement $service)
|
||||
public function watch(Request $request, string $id, Procurement $service): RedirectResponse
|
||||
{
|
||||
$row = $this->query($service)->where('t.id', $id)->firstOrFail(['v.id']);
|
||||
DB::table('portal_tender_watches')->updateOrInsert(['tender_id' => $id, 'user_id' => $request->user()->id], ['seen_version_id' => $row->id, 'created_at' => now(), 'updated_at' => now()]);
|
||||
@@ -121,7 +128,7 @@ class ProcurementController extends Controller
|
||||
return back()->with('success', 'Concurso guardado para acompanhamento. Isto não apresenta uma proposta.');
|
||||
}
|
||||
|
||||
public function unwatch(Request $request, string $id, Procurement $service)
|
||||
public function unwatch(Request $request, string $id, Procurement $service): RedirectResponse
|
||||
{
|
||||
abort_unless($service->available(), 404);
|
||||
DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->delete();
|
||||
@@ -129,10 +136,10 @@ class ProcurementController extends Controller
|
||||
return back();
|
||||
}
|
||||
|
||||
public function watched(Request $request, Procurement $service)
|
||||
public function watched(Request $request, Procurement $service): Response
|
||||
{
|
||||
$rows = $this->query($service)->join('portal_tender_watches as w', 'w.tender_id', '=', 't.id')->where('w.user_id', $request->user()->id)->get(['t.id', 'v.id as vid', 'w.seen_version_id'])->map(fn ($r) => ['id' => $r->id, 'changed' => $r->seen_version_id !== $r->vid, ...$this->summary($service, $r->vid)]);
|
||||
|
||||
return Inertia::render('procurement/manage', ['watched' => true, 'items' => $rows])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
return tap(Inertia::render('procurement/manage', ['watched' => true, 'items' => $rows])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Resources\PublicBusinessOpportunityResource;
|
||||
use App\Models\ExistingBusiness;
|
||||
use App\Models\Sector;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\ExistingBusinessDrafts;
|
||||
use App\Services\ExistingBusinessClassification;
|
||||
use App\Services\OpportunityClassification;
|
||||
use App\Services\ExistingBusinessDrafts;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\OpportunityClassification;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -43,8 +45,8 @@ class PublicBusinessOpportunityController extends Controller
|
||||
->orWhereLike('existing_businesses.objective', '%'.$term.'%'));
|
||||
}
|
||||
if (! empty($filters['sector'])) {
|
||||
$sector = \App\Models\Sector::find($filters['sector']);
|
||||
$query->whereIn('existing_businesses.sector_id', $sector ? \App\Models\Sector::whereIn('slug', \App\Services\SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
$sector = Sector::whereKey($filters['sector'])->first();
|
||||
$query->whereIn('existing_businesses.sector_id', $sector ? Sector::whereIn('slug', SectorOrganization::slugs($sector->slug))->pluck('id') : []);
|
||||
}
|
||||
if (! empty($filters['modality'])) {
|
||||
$query->where('existing_businesses.modality', $filters['modality']);
|
||||
@@ -60,6 +62,9 @@ class PublicBusinessOpportunityController extends Controller
|
||||
'authorities' => DB::table('authorities')->where('status', 'published')->orderBy('label_pt_mz')->get(['id', 'label_pt_mz'])]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<array-key, mixed>
|
||||
*/
|
||||
private function filters(Request $request): array
|
||||
{
|
||||
return $request->validate([
|
||||
|
||||
@@ -2,21 +2,26 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\InvestmentCorridorContext;
|
||||
use App\Services\InvestmentGeography;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class PublicGeographyController extends Controller
|
||||
{
|
||||
public function options(Request $request, InvestmentGeography $geography): JsonResponse
|
||||
{
|
||||
$data = $request->validate(['corridor' => ['nullable', \Illuminate\Validation\Rule::in(array_keys(config('investment_corridors')))], 'type' => ['required', 'in:province,district,admin_post,locality'], 'id' => ['nullable', 'uuid'], 'parent_id' => ['nullable', 'uuid'], 'q' => ['nullable', 'string', 'max:100'], 'page' => ['nullable', 'integer', 'min:1']]);
|
||||
$data = $request->validate(['corridor' => ['nullable', Rule::in(array_keys(config('investment_corridors')))], 'type' => ['required', 'in:province,district,admin_post,locality'], 'id' => ['nullable', 'uuid'], 'parent_id' => ['nullable', 'uuid'], 'q' => ['nullable', 'string', 'max:100'], 'page' => ['nullable', 'integer', 'min:1']]);
|
||||
$query = $geography->publicReferences()->where('location_type', $data['type']);
|
||||
if (!empty($data['corridor'])) {
|
||||
$corridors = app(\App\Services\InvestmentCorridorContext::class);
|
||||
if ($data['type'] === 'province') $query->whereIn('id', array_column($corridors->provinces($data['corridor']), 'id'));
|
||||
elseif (!$corridors->compatible($data['corridor'], $data['parent_id'] ?? null)) abort(422, 'O território não pertence ao corredor escolhido.');
|
||||
if (! empty($data['corridor'])) {
|
||||
$corridors = app(InvestmentCorridorContext::class);
|
||||
if ($data['type'] === 'province') {
|
||||
$query->whereIn('id', array_column($corridors->provinces($data['corridor']), 'id'));
|
||||
} elseif (! $corridors->compatible($data['corridor'], $data['parent_id'] ?? null)) {
|
||||
abort(422, 'O território não pertence ao corredor escolhido.');
|
||||
}
|
||||
}
|
||||
if (! empty($data['id'])) {
|
||||
$query->where('id', $data['id']);
|
||||
|
||||
@@ -6,6 +6,8 @@ use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Services\CountryCatalogue;
|
||||
use App\Services\SectorInvestment;
|
||||
use App\Services\SectorOrganization;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
@@ -19,10 +21,14 @@ class PublicPortalController extends Controller
|
||||
return Inertia::render('PortalInvestidor', ['initialView' => 'opportunities', 'initialQuery' => $filters['q'] ?? '', 'initialSector' => $filters['sector'] ?? '']);
|
||||
}
|
||||
|
||||
public function sector(string $slug): Response|\Illuminate\Http\RedirectResponse
|
||||
public function sector(string $slug): Response|RedirectResponse
|
||||
{
|
||||
if (in_array($slug, ['energia', 'fintech'], true)) return redirect(\App\Services\SectorOrganization::href($slug), 301);
|
||||
if ($slug === 'mercado-financeiro/fintech') $slug = 'fintech';
|
||||
if (in_array($slug, ['energia', 'fintech'], true)) {
|
||||
return redirect(SectorOrganization::href($slug), 301);
|
||||
}
|
||||
if ($slug === 'mercado-financeiro/fintech') {
|
||||
$slug = 'fintech';
|
||||
}
|
||||
$sector = Sector::where('slug', $slug)->where('is_active', true)->whereNotNull('published_at')->firstOrFail();
|
||||
|
||||
return Inertia::render('portal/services', ['section' => 'sector', ...app(SectorInvestment::class)->page($sector)]);
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\SectorReconciliationExecution;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class SectorReconciliationController extends Controller
|
||||
{
|
||||
public function preview(Request $request, SectorReconciliationExecution $execution)
|
||||
public function preview(Request $request, SectorReconciliationExecution $execution): JsonResponse|Response
|
||||
{
|
||||
$plan = $execution->preview($request);
|
||||
|
||||
return $request->expectsJson() ? response()->json($plan)->header('Cache-Control', 'private, no-store')
|
||||
: response()->view('management.sector-reconciliation', ['plan' => $plan, 'operationKey' => (string)Str::uuid()])->header('Cache-Control', 'private, no-store');
|
||||
: response()->view('management.sector-reconciliation', ['plan' => $plan, 'operationKey' => (string) Str::uuid()])->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function apply(Request $request, SectorReconciliationExecution $execution)
|
||||
public function apply(Request $request, SectorReconciliationExecution $execution): JsonResponse
|
||||
{
|
||||
return response()->json($execution->apply($request))->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Services\CountryCatalogue;
|
||||
use App\Services\InstitutionalLinkMonitor;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\InvestorContextConsumers;
|
||||
use App\Services\ScopedPortalAdministration;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -21,6 +22,7 @@ use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
@@ -148,7 +150,7 @@ class VirtualDeskController extends Controller
|
||||
$data = ['boundContextRevision' => app(InvestorContextConsumers::class)->requestRevision($row), 'countries' => app(CountryCatalogue::class)->all(), 'toolPrefill' => $desk->requestToolPrefill($request->user(), $row), 'request' => [...(array) $row, 'payload' => $payload, 'submitted_snapshot' => $desk->decode($row->submitted_snapshot), 'editable_fields' => json_decode($row->editable_fields ?? '[]', true)],
|
||||
'case' => ['id' => $case->id, 'title' => $case->title], 'caseVersion' => $case->version, 'definition' => $definition, 'territoryPaths' => $territoryPaths,
|
||||
'serviceSource' => json_decode(DB::table('desk_service_versions')->where('id', $row->service_version_id)->value('source'), true),
|
||||
'owner' => $desk->owns($request->user(), $case), 'manager' => app(\App\Services\ScopedPortalAdministration::class)->deskDecision($request->user(),$row), 'analyst' => (int) $row->analyst_id === $request->user()->id,
|
||||
'owner' => $desk->owns($request->user(), $case), 'manager' => app(ScopedPortalAdministration::class)->deskDecision($request->user(), $row), 'analyst' => (int) $row->analyst_id === $request->user()->id,
|
||||
'fieldsEditable' => in_array($row->status, ['draft', 'returned'], true), 'states' => config('virtual-desk.request_states'), 'transitions' => config('virtual-desk.transitions.'.$row->status, []),
|
||||
'findings' => $desk->findings($payload), 'ai' => config('virtual-desk.ai'), 'documents' => $this->documents($case->id, $request->user())->filter(fn ($d) => ! $d->request_id || $d->request_id === $id)->values(),
|
||||
'messages' => DB::table('desk_messages as m')->join('users as u', 'u.id', '=', 'm.actor_id')->where('request_id', $id)->orderBy('m.created_at')->get(['m.id', 'm.actor_id', 'm.body', 'm.created_at', 'u.name'])->map(fn ($m) => [...(array) $m, 'body' => Crypt::decryptString($m->body),
|
||||
@@ -178,24 +180,29 @@ class VirtualDeskController extends Controller
|
||||
|
||||
public function assignments(Request $request): JsonResponse|Response
|
||||
{
|
||||
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()),403);
|
||||
abort_unless(app(ScopedPortalAdministration::class)->administrative($request->user()), 403);
|
||||
// Triage projection deliberately excludes payloads, documents, investor identity and case titles.
|
||||
$data=['requests'=>DB::table('desk_requests')->whereNotIn('status',['draft','cancelled','closed'])->orderBy('created_at')->get(['id','protocol','status','version','analyst_id','due_at']),
|
||||
'analysts'=>User::whereNotNull('email_verified_at')->orderBy('name')->get(['id','name'])];
|
||||
return $request->expectsJson()?response()->json($data):Inertia::render('management/desk-assignments',$data);
|
||||
$data = ['requests' => DB::table('desk_requests')->whereNotIn('status', ['draft', 'cancelled', 'closed'])->orderBy('created_at')->get(['id', 'protocol', 'status', 'version', 'analyst_id', 'due_at']),
|
||||
'analysts' => User::whereNotNull('email_verified_at')->orderBy('name')->get(['id', 'name'])];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('management/desk-assignments', $data);
|
||||
}
|
||||
|
||||
public function assign(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
abort_unless(app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()), 403);
|
||||
$data = $request->validate(['version' => 'required|integer', 'analyst_id' => 'required|integer|exists:users,id', 'reason' => 'required|string|min:3|max:3000', 'due_at' => 'nullable|date'], ['reason.required'=>'Explique o motivo da atribuição.','reason.min'=>'Explique o motivo da atribuição com pelo menos 3 caracteres.','analyst_id.required'=>'Seleccione o responsável.','analyst_id.exists'=>'Seleccione um responsável com conta disponível.']);
|
||||
abort_unless(app(ScopedPortalAdministration::class)->administrative($request->user()), 403);
|
||||
$data = $request->validate(['version' => 'required|integer', 'analyst_id' => 'required|integer|exists:users,id', 'reason' => 'required|string|min:3|max:3000', 'due_at' => 'nullable|date'], ['reason.required' => 'Explique o motivo da atribuição.', 'reason.min' => 'Explique o motivo da atribuição com pelo menos 3 caracteres.', 'analyst_id.required' => 'Seleccione o responsável.', 'analyst_id.exists' => 'Seleccione um responsável com conta disponível.']);
|
||||
DB::transaction(function () use ($request, $id, $desk, $data) {
|
||||
$row = DB::table('desk_requests')->where('id',$id)->lockForUpdate()->first();
|
||||
abort_unless($row,404);
|
||||
$case = DB::table('desk_cases')->where('id',$row->case_id)->first();
|
||||
abort_unless($case,404);
|
||||
if ((int)$row->version !== (int)$data['version']) throw \Illuminate\Validation\ValidationException::withMessages(['version'=>'Este pedido foi actualizado. Recupere a lista e tente novamente.']);
|
||||
if (in_array($row->status,['draft','cancelled','closed'],true)) throw \Illuminate\Validation\ValidationException::withMessages(['status'=>'Este pedido já não permite uma nova atribuição. Recupere a lista actual.']);
|
||||
$row = DB::table('desk_requests')->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($row !== null, 404);
|
||||
$case = DB::table('desk_cases')->where('id', $row->case_id)->first();
|
||||
abort_unless($case !== null, 404);
|
||||
if ((int) $row->version !== (int) $data['version']) {
|
||||
throw ValidationException::withMessages(['version' => 'Este pedido foi actualizado. Recupere a lista e tente novamente.']);
|
||||
}
|
||||
if (in_array($row->status, ['draft', 'cancelled', 'closed'], true)) {
|
||||
throw ValidationException::withMessages(['status' => 'Este pedido já não permite uma nova atribuição. Recupere a lista actual.']);
|
||||
}
|
||||
$analyst = User::where('id', $data['analyst_id'])->firstOrFail();
|
||||
abort_unless($analyst->hasVerifiedEmail(), 422, 'O responsável deve ter conta verificada.');
|
||||
abort_if($desk->owns($analyst, $case, false), 422, 'O proponente não pode ser o analista.');
|
||||
|
||||
Reference in New Issue
Block a user