$left && $b->$right && DB::table($table)->where($left, $a->$left)->where($right, $b->$right)->whereNull('deleted_at')->exists()) { return true; } } } return false; } /** @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']]); $query = $management ? InvestmentDossier::query() : self::visible(); if (! empty($filters['kind'])) { $query->whereNotNull($filters['kind'].'_id'); } if (! empty($filters['q'])) { $query->where('title', 'ilike', '%'.$filters['q'].'%'); } return Inertia::render('portfolio/index', ['records' => $query->orderBy('title')->paginate(30)->withQueryString(), 'management' => $management, 'filters' => $filters]); } 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] ?? [], DB::table($table) ->where($origin, $record->$origin)->whereNull('deleted_at')->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'); 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'])); } } return Inertia::render('portfolio/show', ['record' => $record, 'related' => $related, 'management' => $management, 'zones' => $zones, 'origin' => $origin ? ['id' => $origin->id, 'title' => $origin->title] : null, 'opportunities' => $opportunities, 'chains' => $chains]); } 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. Pode aprovar e publicar a ficha.'); } 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.'); 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.'); } }