Files
Investidor-V2/app/Http/Controllers/InvestmentPortfolioController.php
T
nala 250ac35fd1
tests / ci (push) Canceled after 0s
first commit
2026-09-28 10:01:09 +02:00

132 lines
7.7 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\InvestmentDossier;
use App\Models\Opportunity;
use App\Services\InvestmentZones;
use App\Services\PortalAudit;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class InvestmentPortfolioController extends Controller
{
public static function related(InvestmentDossier $first, InvestmentDossier $second): bool
{
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 && DB::table($table)->where($left, $a->$left)->where($right, $b->$right)->whereNull('deleted_at')->exists()) {
return true;
}
}
}
return false;
}
/** @return Builder<InvestmentDossier> */
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.');
}
}