217 lines
13 KiB
PHP
217 lines
13 KiB
PHP
<?php
|
|
|
|
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\OpportunityTextSearch;
|
|
use App\Services\PublicSearchMetrics;
|
|
use App\Services\SectorInvestment;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Inertia\Inertia;
|
|
use Inertia\Response;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
class InvestmentDiscoveryController extends Controller
|
|
{
|
|
public function index(Request $request, InvestmentCatalogue $catalogue): Response
|
|
{
|
|
$filters = $catalogue->filters($request);
|
|
if ($request->routeIs('investment.education')) {
|
|
$filters['education'] = true;
|
|
}
|
|
$results = $catalogue->query($filters)->paginate(12)->withQueryString()->through(fn ($record) => $catalogue->presentLocation($record));
|
|
$relatedQuery = $results->total() === 0 ? app(OpportunityTextSearch::class)->relatedQuery($filters['q'] ?? '') : null;
|
|
$relatedFilters = [...Arr::except($filters, ['page', 'q']), 'q' => $relatedQuery];
|
|
$related = $relatedQuery === null ? null : [
|
|
'query' => $filters['q'], 'subject' => $relatedQuery,
|
|
'href' => ($request->routeIs('investment.education') ? '/educacao' : '/oportunidades').'?'.http_build_query($relatedFilters),
|
|
'items' => $catalogue->query($relatedFilters)->limit(6)->get()->map(fn ($record) => $catalogue->presentLocation($record)),
|
|
];
|
|
app(PublicSearchMetrics::class)->candidate($request, $request->routeIs('investment.education') ? 'education_page' : 'opportunities_page', $results->total(), $request->routeIs('investment.education') ? Arr::except($filters, ['education']) : $filters);
|
|
|
|
return Inertia::render('investment/catalogue', ['filters' => $filters, 'catalogueUrl' => $request->routeIs('investment.education') ? '/educacao' : '/oportunidades', 'references' => $catalogue->references(false, $filters),
|
|
'corridorGroups' => ($filters['view'] ?? '') === 'corridors' ? app(OpportunityCorridorExplorer::class)->groups($filters) : [],
|
|
'territorySelection' => app(InvestmentGeography::class)->selection($filters),
|
|
'guides' => app(SectorInvestment::class)->guides(Sector::where('slug', $filters['sector'] ?? '')->where('is_active', true)->whereNotNull('published_at')->first()),
|
|
'results' => $results, 'relatedSearch' => $related,
|
|
'favourites' => $request->user() ? DB::table('opportunity_favourites')->where('user_id', $request->user()->id)->pluck('opportunity_id') : [],
|
|
'territorial' => $this->territorial($filters)]);
|
|
}
|
|
|
|
/**
|
|
* @param array<string,mixed> $filters
|
|
* @return Collection<int,InvestmentDossier>
|
|
*/
|
|
private function territorial(array $filters): Collection
|
|
{
|
|
$query = InvestmentPortfolioController::visible();
|
|
$path = app(InvestmentGeography::class)->selection($filters);
|
|
if ($path !== []) {
|
|
$ids = app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id);
|
|
app(InvestmentGeography::class)->constrainDossiersToTerritories($query, $ids);
|
|
}
|
|
if (in_array($filters['dimension'] ?? null, ['zee', 'zfi'], true)) {
|
|
return $query->whereRaw('false')->get();
|
|
}
|
|
if (in_array($filters['dimension'] ?? null, ['cluster', 'corridor'], true)) {
|
|
$query->whereNotNull($filters['dimension'].'_id');
|
|
}
|
|
if (! empty($filters['geo_location_id'])) {
|
|
app(InvestmentGeography::class)->constrainDossiersToTerritories($query, app(InvestmentGeography::class)->descendants($filters['geo_location_id']));
|
|
}
|
|
foreach (['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'zee_id', 'zfi_id', 'park_id', 'education', 'facets', 'sector', 'province', 'district', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level', 'preparation'] as $field) {
|
|
if (isset($filters[$field]) && $filters[$field] !== '' && $filters[$field] !== false && $filters[$field] !== []) {
|
|
return $query->whereRaw('false')->get();
|
|
}
|
|
}
|
|
if (! empty($filters['q'])) {
|
|
$query->where(fn ($q) => $q->where('title', 'ilike', '%'.$filters['q'].'%')->orWhereRaw('content::text ILIKE ?', ['%'.$filters['q'].'%']));
|
|
}
|
|
foreach (['cluster_id', 'corridor_id', 'project_id'] as $field) {
|
|
if (! empty($filters[$field])) {
|
|
$query->where($field, $filters[$field]);
|
|
}
|
|
}
|
|
|
|
return $query->orderBy('title')->limit(30)->get();
|
|
}
|
|
|
|
public function search(Request $request, InvestmentCatalogue $catalogue): JsonResponse
|
|
{
|
|
$filters = $catalogue->filters($request);
|
|
$results = $catalogue->query($filters)->paginate($request->integer('per_page', 12))->withQueryString()->through(fn ($record) => $catalogue->presentLocation($record));
|
|
app(PublicSearchMetrics::class)->candidate($request, 'opportunities_api', $results->total(), $filters);
|
|
|
|
return response()->json($results);
|
|
}
|
|
|
|
public function references(Request $request, InvestmentCatalogue $catalogue): JsonResponse
|
|
{
|
|
return response()->json($catalogue->references(false, $catalogue->filters($request)));
|
|
}
|
|
|
|
public function detail(Request $request, string $slug, InvestmentCatalogue $catalogue): Response
|
|
{
|
|
$record = Opportunity::published()->with('sector:id,name,slug')->where('slug', $slug)->firstOrFail();
|
|
|
|
$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(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;
|
|
}
|
|
}
|
|
abort_unless($linked, 404);
|
|
}
|
|
$origin = null;
|
|
if (! empty($data['zone_id'])) {
|
|
$origin = app(InvestmentZones::class)->links()->join('investment_item_opportunities as m', 'm.item_id', '=', 'zl.item_id')
|
|
->join('investment_zones as z', 'z.id', '=', 'zl.zone_id')->where('zl.zone_id', $data['zone_id'])->where('m.opportunity_id', $record->id)->first(['z.id', 'z.name']);
|
|
abort_unless($origin !== null, 404);
|
|
}
|
|
|
|
$returnFilters = $request->query();
|
|
unset($returnFilters['zone_id'], $returnFilters['from_dossier']);
|
|
|
|
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)]);
|
|
}
|
|
|
|
public function detailApi(string $slug, InvestmentCatalogue $catalogue): JsonResponse
|
|
{
|
|
$record = Opportunity::published()->with('sector:id,name,slug')->where('slug', $slug)->firstOrFail();
|
|
|
|
return response()->json(['record' => $record, 'context' => $catalogue->context($record->id)]);
|
|
}
|
|
|
|
public function suggest(Request $request, InvestmentCatalogue $catalogue): JsonResponse
|
|
{
|
|
$filters = $catalogue->filters($request);
|
|
if (mb_strlen(trim($filters['q'] ?? '')) < 2) {
|
|
return response()->json(['opportunities' => [], 'territorial' => []]);
|
|
}
|
|
|
|
return response()->json(['opportunities' => $catalogue->query($filters)->limit(5)->get(['id', 'title', 'slug']), 'territorial' => $this->territorial($filters)->take(5)->values()]);
|
|
}
|
|
|
|
public function map(Request $request, InvestmentCatalogue $catalogue): JsonResponse
|
|
{
|
|
$filters = $catalogue->filters($request);
|
|
$context = $this->territorial($filters)->map(fn ($d) => ['id' => 'dossier:'.$d->id, 'title' => $d->title, 'kind' => $d->cluster_id ? 'Cluster' : ($d->corridor_id ? 'Corredor' : 'Projeto âncora'), 'url' => '/carteira/'.$d->id]);
|
|
$opportunityOnlyFilters = array_filter(Arr::only($filters, ['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'education', 'sector', 'facets', 'project_id', 'cluster_id', 'corridor_id', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level', 'preparation']), fn ($value) => $value !== null && $value !== '' && $value !== false && $value !== []);
|
|
if ($opportunityOnlyFilters === [] && ! in_array($filters['dimension'] ?? null, ['cluster', 'corridor'], true)) {
|
|
$zoneFilters = $filters;
|
|
if (in_array($filters['dimension'] ?? null, ['zee', 'zfi'], true)) {
|
|
$zoneFilters['category'] = $filters['dimension'];
|
|
}
|
|
$zones = app(InvestmentZones::class)->search($zoneFilters);
|
|
foreach (['zee_id', 'zfi_id', 'park_id'] as $key) {
|
|
if (! empty($filters[$key])) {
|
|
$zones->where('id', $filters[$key]);
|
|
}
|
|
}
|
|
$context = $context->concat($zones->orderBy('name')->limit(30)->get(['id', 'name', 'category'])->map(fn ($z) => ['id' => 'zone:'.$z->id, 'title' => $z->name, 'kind' => ['zee' => 'ZEE', 'zfi' => 'ZFI', 'industrial_park' => 'Parque industrial'][$z->category] ?? $z->category, 'url' => '/zonas/'.$z->id]));
|
|
}
|
|
|
|
return response()->json([...$catalogue->map($filters), 'territorial_context' => $context->values(), 'context_limit_per_collection' => 30]);
|
|
}
|
|
|
|
public function compare(Request $request, InvestmentCatalogue $catalogue): JsonResponse
|
|
{
|
|
$data = $request->validate(['ids' => ['required', 'array', 'min:1', 'max:4'], 'ids.*' => ['required', 'integer', 'distinct']]);
|
|
$records = Opportunity::published()->with('sector:id,name,slug')->whereIn('id', $data['ids'])->get();
|
|
abort_unless($records->count() === count($data['ids']), 404);
|
|
|
|
return response()->json(['records' => $records->map(fn ($r) => ['record' => $r, 'context' => $catalogue->context($r->id)])]);
|
|
}
|
|
|
|
public function favourite(Request $request, int $id): JsonResponse
|
|
{
|
|
Opportunity::published()->findOrFail($id);
|
|
$data = $request->validate(['saved' => ['required', 'boolean']]);
|
|
$key = ['user_id' => $request->user()->id, 'opportunity_id' => $id];
|
|
if ($data['saved']) {
|
|
DB::table('opportunity_favourites')->insertOrIgnore([...$key, 'created_at' => now(), 'updated_at' => now()]);
|
|
} else {
|
|
DB::table('opportunity_favourites')->where($key)->delete();
|
|
}
|
|
|
|
return response()->json(['saved' => $data['saved']]);
|
|
}
|
|
|
|
public function export(Request $request, InvestmentCatalogue $catalogue): StreamedResponse
|
|
{
|
|
$records = $catalogue->query($catalogue->filters($request))->limit(1000)->get();
|
|
|
|
return response()->streamDownload(function () use ($records) {
|
|
$stream = fopen('php://output', 'w');
|
|
if ($stream === false) {
|
|
return;
|
|
}
|
|
fwrite($stream, "\xEF\xBB\xBF");
|
|
fputcsv($stream, ['Referência', 'Título', 'Sector', 'Província', 'Montante', 'Moeda', 'Maturidade', 'URL'], ';', '"', '');
|
|
foreach ($records as $r) {
|
|
$fields = [$r->reference, $r->title, $r->sector?->name, $r->province, $r->estimated_investment_amount, $r->estimated_investment_currency, $r->maturity_label, url('/oportunidades/'.$r->slug)];
|
|
fputcsv($stream, array_map(fn ($v) => preg_match('/^[\s]*[=+@-]/u', (string) $v) ? "'".$v : $v, $fields), ';', '"', '');
|
|
}
|
|
fclose($stream);
|
|
}, 'oportunidades.csv', ['Content-Type' => 'text/csv; charset=UTF-8', 'X-Export-Limit' => '1000']);
|
|
}
|
|
}
|