cluster_id && $project->project_id && app(InvestmentRelations::class)->memberships() ->where('cp.cluster_id', $cluster->cluster_id)->where('cp.project_id', $project->project_id)->exists()) { return true; } } foreach ([['corridor_projects', 'corridor_id', 'project_id'], ['corridor_clusters', 'corridor_id', 'cluster_id'], ['cluster_projects', 'cluster_id', 'project_id']] as [$table, $left, $right]) { foreach ([[$first, $second], [$second, $first]] as [$a, $b]) { if ($a->$left && $b->$right && self::currentRelation(DB::table($table)->where($left, $a->$left)->where($right, $b->$right))->exists()) { return true; } } } return false; } private static function currentRelation(QueryBuilder $query): QueryBuilder { return $query->whereNull('deleted_at')->where('validation_status', 'validated') ->whereDate('valid_from', '<=', today()->toDateString()) ->where(fn ($q) => $q->whereNull('valid_until')->orWhereDate('valid_until', '>=', today()->toDateString())); } /** @return Builder */ public static function visible(): Builder { return InvestmentDossier::select(['id', 'title', 'content', 'source', 'project_id', 'cluster_id', 'corridor_id', 'publication_status', 'published_at'])->where('publication_status', 'published')->where(function ($q) { foreach (['project_id' => 'projects', 'cluster_id' => 'economic_clusters', 'corridor_id' => 'development_corridors'] as $foreign => $table) { $q->orWhereExists(fn ($sub) => $sub->selectRaw('1')->from($table) ->whereColumn($table.'.id', 'investment_dossiers.'.$foreign)->whereNull($table.'.deleted_at')); } }); } public function index(Request $request): Response { $management = $request->routeIs('management.portfolio*'); $filters = $request->validate(['q' => ['nullable', 'string', 'max:120'], 'kind' => ['nullable', 'in:corridor,cluster,project'], 'sector' => ['nullable', 'string', 'max:500'], 'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'], 'admin_post_id' => ['nullable', 'uuid'], 'locality_id' => ['nullable', 'uuid']]); $query = $management ? InvestmentDossier::query() : self::visible(); $sectorOptions = ($management ? InvestmentDossier::query() : self::visible()) ->whereNotNull('content->sector')->get()->pluck('content.sector') ->filter(fn ($sector) => is_string($sector) && trim($sector) !== '')->unique()->sort()->values(); if (! empty($filters['sector'])) { $query->where('content->sector', $filters['sector']); } if (! empty($filters['kind'])) { $query->whereNotNull($filters['kind'].'_id'); } if (! empty($filters['q'])) { $query->where(fn ($q) => $q->where('title', 'ilike', '%'.$filters['q'].'%')->orWhereRaw('content::text ILIKE ?', ['%'.$filters['q'].'%'])); } $territorySelection = app(InvestmentGeography::class)->selection($filters); if ($territorySelection !== []) { $territoryIds = app(InvestmentGeography::class)->descendants($territorySelection[array_key_last($territorySelection)]->id); app(InvestmentGeography::class)->constrainDossiersToTerritories($query, $territoryIds); } return Inertia::render('portfolio/index', ['records' => $query->orderBy('title')->paginate(30)->withQueryString(), 'management' => $management, 'filters' => $filters, 'sectors' => $sectorOptions, 'territorySelection' => $territorySelection]); } public function show(Request $request, string $id): Response { $management = $request->routeIs('management.portfolio*'); $record = ($management ? InvestmentDossier::query() : self::visible())->findOrFail($id); $relatedIds = []; foreach ([['corridor_projects', 'corridor_id', 'project_id'], ['corridor_clusters', 'corridor_id', 'cluster_id'], ['cluster_projects', 'cluster_id', 'project_id']] as [$table, $left, $right]) { foreach ([[$left, $right], [$right, $left]] as [$origin, $destination]) { if ($record->$origin) { $relatedIds[$destination] = array_merge($relatedIds[$destination] ?? [], self::currentRelation(DB::table($table) ->where($origin, $record->$origin))->pluck($destination)->all()); } } } $related = ($management ? InvestmentDossier::query() : self::visible())->where(function ($q) use ($relatedIds) { $q->whereRaw('false'); foreach ($relatedIds as $column => $ids) { $q->orWhereIn($column, $ids); } })->orderBy('title')->get(['id', 'title', 'project_id', 'cluster_id', 'corridor_id']); $context = $request->validate(['from' => ['nullable', 'uuid']]); $origin = isset($context['from']) ? self::visible()->whereKey($context['from'])->firstOrFail() : null; abort_if($origin && ! self::related($record, $origin), 422, 'Contexto sem relação com esta ficha.'); $opportunities = Opportunity::published()->where(function ($query) use ($record) { $query->whereRaw('false'); $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')); } } })->orderBy('title')->get(['id', 'title', 'slug', 'summary']); $chains = $record->cluster_id ? DB::table('value_chains')->where('cluster_id', $record->cluster_id)->whereNull('deleted_at') ->when(! $management, fn ($q) => $q->where('data_classification', 'public'))->orderBy('name')->get(['id', 'name', 'stage']) : collect(); $zones = collect(); foreach (['cluster_id', 'corridor_id', 'project_id'] as $foreign) { if ($record->$foreign) { $zones = $zones->merge(app(InvestmentZones::class)->links(true)->join('investment_zones as z', 'z.id', '=', 'zl.zone_id') ->where('zl.'.$foreign, $record->$foreign)->get(['z.id', 'z.name', 'z.category', 'zl.association_type', 'zl.source_reference'])); } } $anchorProject = $record->project_id ? Project::whereKey($record->project_id)->whereNull('deleted_at')->where('status', '!=', 'draft')->first(['id', 'code', 'name', 'status', 'budget_amount', 'budget_currency', 'start_date', 'end_date']) : null; $existingSubmissions = ! $management && $request->user() ? InvestmentSubmission::where('user_id', $request->user()->id)->where('dossier_id', $record->id)->where('status', 'draft') ->orderByDesc('updated_at')->limit(10)->get(['id', 'version', 'updated_at']) : collect(); 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(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]); } public function resumeInterest(Request $request, string $id): RedirectResponse { self::visible()->findOrFail($id); $data = $request->validate(['catalogue_query' => ['nullable', 'string', 'max:4000']]); return redirect('/carteira/'.$id.'?'.http_build_query([...$data, 'prepare_interest' => '1'])); } public function save(Request $request, InvestmentDossier $dossier): RedirectResponse { $data = $request->validate(['version_no' => ['required', 'integer', 'min:1'], 'title' => ['required', 'string', 'max:255'], 'source' => ['required', 'string', 'max:2000'], 'content' => ['required', 'array:name,description,location,region,sector,promotion,facts,partners,cta,anchors,gaps,priority,clusters,focus,notice,code'], 'content.*' => ['nullable', 'string', 'max:15000']]); DB::transaction(function () use ($request, $dossier, $data) { $dossier = InvestmentDossier::whereKey($dossier->id)->lockForUpdate()->firstOrFail(); abort_if($dossier->version_no !== (int) $data['version_no'], 409, 'A ficha foi alterada. Recarregue a página e reveja a versão actual antes de guardar.'); PortalAudit::save($dossier, [...$data, 'version_no' => $dossier->version_no + 1, 'editor_id' => $request->user()->id, 'reviewer_id' => null, 'publication_status' => 'draft', 'published_at' => null, 'approval_evidence' => null, 'approval_basis' => 'manager'], $request->user()->id); }); return back()->with('success', 'Rascunho guardado. A publicacao aguarda um revisor diferente do editor.'); } public function publish(Request $request, InvestmentDossier $dossier): RedirectResponse { $data = $request->validate(['version_no' => ['required', 'integer', 'min:1'], 'approval_evidence' => ['required', 'string', 'min:15', 'max:3000']]); DB::transaction(function () use ($request, $dossier, $data) { $dossier = InvestmentDossier::whereKey($dossier->id)->lockForUpdate()->firstOrFail(); abort_if($dossier->version_no !== (int) $data['version_no'], 409, 'A ficha foi alterada. Recarregue a página e reveja a versão actual antes de publicar.'); abort_unless($dossier->editor_id !== null, 422, 'A ficha deve ser assumida e revista antes da publicacao.'); abort_if((int) $dossier->editor_id === (int) $request->user()->id, 403, 'A publicacao exige um revisor diferente do editor.'); PortalAudit::save($dossier, [...$data, 'version_no' => $dossier->version_no + 1, 'approval_basis' => 'manager', 'reviewer_id' => $request->user()->id, 'publication_status' => 'published', 'published_at' => now()], $request->user()->id); }); return back()->with('success', 'Ficha publicada com evidência de revisão.'); } }