242 lines
16 KiB
PHP
242 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Models\Sector;
|
|
use App\Services\DocumentManagementAccess;
|
|
use App\Services\FundingDossierPreparation;
|
|
use App\Services\FundingInstitutionClassification;
|
|
use App\Services\FundingInstitutionDetails;
|
|
use App\Services\FundingProgrammeReferences;
|
|
use App\Services\FundingProgrammes;
|
|
use App\Services\InvestmentGeography;
|
|
use App\Services\InvestorContext;
|
|
use App\Services\InvestorPath;
|
|
use App\Services\Procurement;
|
|
use App\Services\VirtualDesk;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\Rule;
|
|
use Inertia\Inertia;
|
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
|
|
final class FundingProgrammeController extends Controller
|
|
{
|
|
public function reference(Request $request, string $code, FundingProgrammeReferences $references): \Symfony\Component\HttpFoundation\Response
|
|
{
|
|
$plan = $references->preview($request->user(), $code);
|
|
if ($request->expectsJson()) {
|
|
return response()->json($plan)->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
return tap(Inertia::render('funding/reference', ['reference' => $plan])->toResponse($request),
|
|
static fn (\Symfony\Component\HttpFoundation\Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function prepareReference(Request $request, string $code, FundingProgrammeReferences $references): RedirectResponse|JsonResponse
|
|
{
|
|
$data = $request->validate(['hash' => 'required|string|size:64']);
|
|
$result = $references->prepare($request->user(), $code, $data['hash']);
|
|
if ($request->expectsJson()) {
|
|
$reference = $references->preview($request->user(), $code);
|
|
$service = app(FundingProgrammes::class);
|
|
$matches = $service->matchesReference($request->user(), $result['id'], $reference['draft']);
|
|
$publication = $matches ? $service->preview($request->user(), $result['id']) : null;
|
|
|
|
return response()->json([...$result, 'matches_reference' => $matches, 'publication' => $publication ? ['version' => $publication['version'], 'hash' => $publication['hash']] : null])->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
return redirect('/management/fundos/'.$result['id'])->with('success', $result['created']
|
|
? 'Rascunho preparado com as fontes indicadas. Ainda não está publicado.'
|
|
: 'Esta referência já tem ficha. Aberta a versão existente, sem substituir alterações ou acrescentar chamadas.');
|
|
}
|
|
|
|
public function assisted(Request $request, FundingProgrammeReferences $references): \Symfony\Component\HttpFoundation\Response
|
|
{
|
|
return tap(Inertia::render('funding/assisted', ['batch' => $references->assisted($request->user())])->toResponse($request), static fn ($response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function manage(Request $request, FundingProgrammes $service): \Symfony\Component\HttpFoundation\Response
|
|
{
|
|
$service->guard($request->user());
|
|
$items = DB::table('funding_programmes')->orderByDesc('updated_at')->paginate(20);
|
|
$items->through(fn ($row) => ['id' => $row->id, 'title' => json_decode($row->draft, true)['title'] ?? 'Rascunho sem título',
|
|
'version' => (int) $row->version, 'published' => $row->published_version_id !== null]);
|
|
|
|
return tap(Inertia::render('funding/manage', ['items' => $items, 'references' => app(FundingProgrammeReferences::class)->choices($request->user())])->toResponse($request),
|
|
static fn (\Symfony\Component\HttpFoundation\Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function edit(Request $request, FundingProgrammes $service, ?string $id = null): \Symfony\Component\HttpFoundation\Response
|
|
{
|
|
$service->guard($request->user());
|
|
$row = $id ? DB::table('funding_programmes')->where('id', $id)->firstOrFail() : null;
|
|
$editable = app(DocumentManagementAccess::class)->documents($request->user())->select('id');
|
|
$references = ['organizations' => DB::table('organizations')->whereNull('deleted_at')->orderBy('name')->get(['id', 'name']),
|
|
'sectors' => Sector::choices()->whereNotNull('published_at')->orderBy('name')->get(['id', 'name']),
|
|
'territories' => app(InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id', 'name']),
|
|
'documents' => DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
|
|
->whereNull('d.deleted_at')->where(fn ($q) => $q->whereIn('d.id', $editable)
|
|
->orWhereIn('r.id', app(Procurement::class)->publicDocuments()->select('r.id')))
|
|
->orderBy('r.title')->get(['r.id', 'r.title'])];
|
|
|
|
return tap(Inertia::render('funding/edit', ['record' => $row ? ['id' => $row->id, 'code' => $row->code, 'version' => (int) $row->version,
|
|
'draft' => json_decode($row->draft, true), 'published' => $row->published_version_id !== null] : null,
|
|
'references' => Arr::only($references, ['organizations', 'sectors', 'territories', 'documents']),
|
|
'categories' => FundingInstitutionClassification::CATEGORIES, 'institutionTypes' => FundingInstitutionClassification::TYPES, 'institutionNatures' => FundingInstitutionDetails::NATURES, 'needs' => FundingInstitutionDetails::NEEDS,
|
|
'kinds' => FundingProgrammes::KINDS, 'windowModes' => FundingProgrammes::WINDOWS,
|
|
'history' => $id ? DB::table('funding_programme_versions')->where('programme_id', $id)->orderByDesc('version')->get(['version', 'reason', 'published_at']) : []])->toResponse($request),
|
|
static fn (\Symfony\Component\HttpFoundation\Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function save(Request $request, FundingProgrammes $service, ?string $id = null): RedirectResponse
|
|
{
|
|
$id = $service->save($request->user(), $id, $request->all());
|
|
|
|
return redirect('/management/fundos/'.$id)->with('success', 'Rascunho guardado. Confira a prévia antes de publicar.');
|
|
}
|
|
|
|
public function preview(Request $request, string $id, FundingProgrammes $service): JsonResponse
|
|
{
|
|
return response()->json($service->preview($request->user(), $id))->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
public function publish(Request $request, string $id, FundingProgrammes $service): RedirectResponse|JsonResponse
|
|
{
|
|
$versionId = $service->publish($request->user(), $id, $request->all());
|
|
if ($request->expectsJson()) {
|
|
return response()->json(['id' => $id, 'version_id' => $versionId, 'published' => true])->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
return back()->with('success', 'Nova versão publicada. Os interesses anteriores conservam a versão guardada.');
|
|
}
|
|
|
|
public function withdraw(Request $request, string $id, FundingProgrammes $service): RedirectResponse
|
|
{
|
|
$data = $request->validate(['version' => 'required|integer|min:1', 'reason' => 'required|string|min:20|max:2000']);
|
|
$service->withdraw($request->user(), $id, $data['version'], $data['reason']);
|
|
|
|
return back()->with('success', 'Publicação retirada. Versões e interesses preservados.');
|
|
}
|
|
|
|
public function saved(Request $request, FundingProgrammes $service): \Symfony\Component\HttpFoundation\Response
|
|
{
|
|
abort_unless($service->installed(), 404);
|
|
$items = DB::table('funding_programme_interests as i')
|
|
->join('funding_programme_versions as v', fn ($join) => $join->on('v.id', '=', 'i.version_id')->on('v.programme_id', '=', 'i.programme_id'))
|
|
->join('funding_programmes as p', 'p.id', '=', 'i.programme_id')
|
|
->where('i.user_id', $request->user()->id)->whereNull('i.removed_at')
|
|
->orderByDesc('i.saved_at')->orderBy('i.programme_id')
|
|
->paginate(20, ['i.programme_id', 'i.saved_at', 'v.version', 'v.payload', 'p.published_version_id'])->withQueryString();
|
|
$items->through(function ($row) use ($service) {
|
|
$current = $service->publicQuery()->where('p.id', $row->programme_id)->first(['v.version']);
|
|
$snapshot = json_decode($row->payload, true, 512, JSON_THROW_ON_ERROR);
|
|
|
|
return ['id' => $row->programme_id, 'title' => $snapshot['title'], 'saved_version' => (int) $row->version,
|
|
'saved_at' => $row->saved_at, 'available' => $current !== null, 'current_version' => $current ? (int) $current->version : null];
|
|
});
|
|
|
|
return tap(Inertia::render('funding/saved', ['items' => $items])->toResponse($request),
|
|
static fn (\Symfony\Component\HttpFoundation\Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function index(Request $request, FundingProgrammes $service): \Symfony\Component\HttpFoundation\Response
|
|
{
|
|
$filters = $request->validate(['q' => 'nullable|string|max:150', 'kind' => 'nullable|in:institution,fund,programme,incentive',
|
|
'category' => ['nullable', Rule::in(array_keys(FundingInstitutionClassification::CATEGORIES))], 'institution_type' => ['nullable', Rule::in(array_keys(FundingInstitutionClassification::TYPES))], 'location_mode' => 'nullable|in:physical,coverage', 'include_historical' => 'nullable|boolean',
|
|
'need' => ['nullable', Rule::in(array_keys(FundingInstitutionDetails::NEEDS))],
|
|
'context' => 'nullable|uuid', 'opportunity' => 'nullable|string|max:150',
|
|
'territory' => ['nullable', 'uuid', Rule::in(app(InvestmentGeography::class)->publicReferences()->pluck('id')->all())],
|
|
'sector' => ['nullable', 'integer', Rule::in(Sector::choices()->whereNotNull('published_at')->pluck('id')->all())],
|
|
'certainty' => 'nullable|in:include_unknown,documented', 'state' => 'nullable|in:open,closed,upcoming,unknown']);
|
|
$filters = array_filter($filters, fn ($value) => $value !== null && $value !== '');
|
|
$dossier = $request->user() && ! empty($filters['context']) ? app(FundingDossierPreparation::class)->context($request->user(), $filters['context'], $filters['opportunity'] ?? null) : null;
|
|
if ($dossier && empty($filters['territory']) && $dossier['territory']) {
|
|
$filters['territory'] = $dossier['territory'];
|
|
}
|
|
$query = $service->catalogueQuery($filters);
|
|
if (! empty($filters['q'])) {
|
|
$term = '%'.str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $filters['q']).'%';
|
|
$query->where(fn ($q) => $q->whereRaw("v.payload->>'title' ilike ?", [$term])->orWhereRaw("v.payload->>'summary' ilike ?", [$term])->orWhereRaw("(v.payload->'aliases')::text ilike ?", [$term])->orWhereRaw("EXISTS (SELECT 1 FROM jsonb_array_elements(COALESCE(v.payload->'locations','[]'::jsonb)) l WHERE l->>'label' ilike ? OR l->>'address' ilike ? OR l->>'location_text' ilike ?)", [$term, $term, $term]));
|
|
}
|
|
if (! empty($filters['kind'])) {
|
|
$query->whereRaw("v.payload->>'kind' = ?", [$filters['kind']]);
|
|
}
|
|
$items = $query->orderByDesc('v.published_at')->orderBy('p.id')->paginate(20, ['p.id', 'p.code', 'v.id as version_id', 'v.version', 'v.payload', 'v.published_at'])->withQueryString();
|
|
$items->through(function ($row) use ($service, $filters) {
|
|
$record = $service->present($row);
|
|
|
|
return [...$record, 'filter_explanation' => $service->filterExplanation($record, $filters)];
|
|
});
|
|
|
|
return tap(Inertia::render('funding/index', ['items' => $items, 'filters' => $filters, 'categories' => FundingInstitutionClassification::CATEGORIES, 'institutionTypes' => FundingInstitutionClassification::TYPES, 'kinds' => FundingProgrammes::KINDS, 'needs' => FundingInstitutionDetails::NEEDS, 'dossier' => $dossier,
|
|
'options' => ['territories' => app(InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id', 'name']),
|
|
'sectors' => Sector::choices()->whereNotNull('published_at')->orderBy('name')->get(['id', 'name'])], 'management' => $service->access($request->user())])
|
|
->toResponse($request), static fn (\Symfony\Component\HttpFoundation\Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function show(Request $request, string $id, FundingProgrammes $service): \Symfony\Component\HttpFoundation\Response
|
|
{
|
|
$request->validate(['context' => 'nullable|uuid', 'opportunity' => 'nullable|string|max:150', 'need' => ['nullable', Rule::in(array_keys(FundingInstitutionDetails::NEEDS))], 'territory' => 'nullable|uuid', 'category' => 'nullable|string|max:80', 'institution_type' => 'nullable|string|max:80', 'location_mode' => 'nullable|in:physical,coverage']);
|
|
$dossier = $request->user() && $request->filled('context') ? app(FundingDossierPreparation::class)->context($request->user(), $request->string('context')->toString(), $request->query('opportunity')) : null;
|
|
if ($dossier && ! $request->filled('territory') && $dossier['territory']) {
|
|
$request->merge(['territory' => $dossier['territory']]);
|
|
}
|
|
$record = $service->published($id);
|
|
$destinations = [];
|
|
if ($request->user()) {
|
|
foreach (app(InvestorPath::class)->contexts($request->user()) as $item) {
|
|
$case = app(VirtualDesk::class)->case($request->user(), $item['id']);
|
|
$state = app(InvestorContext::class)->state($case);
|
|
$choices = $state['opportunity_choices'] ?? [];
|
|
if ($choices === []) {
|
|
$destinations[] = ['id' => $item['id'], 'opportunity' => null, 'title' => $item['title']];
|
|
}
|
|
foreach ($choices as $choice) {
|
|
$destinations[] = ['id' => $item['id'], 'opportunity' => $choice['opportunity']['key'], 'title' => $item['title'].' — '.$choice['opportunity']['title']];
|
|
}
|
|
}
|
|
}
|
|
$saved = $request->user() ? DB::table('funding_programme_interests as i')->join('funding_programme_versions as v', 'v.id', '=', 'i.version_id')
|
|
->where('i.programme_id', $id)->where('i.user_id', $request->user()->id)->whereNull('i.removed_at')->first(['v.version', 'i.saved_at']) : null;
|
|
|
|
return tap(Inertia::render('funding/show', ['record' => $record, 'saved' => $saved, 'authenticated' => $request->user() !== null, 'dossier' => $dossier, 'selection' => [...$request->only(['need', 'territory', 'context', 'opportunity', 'category', 'institution_type', 'location_mode']), 'territory_label' => app(InvestmentGeography::class)->publicReferences()->where('id', $request->query('territory'))->value('name') ?? 'Local por seleccionar'], 'needs' => FundingInstitutionDetails::NEEDS, 'destinations' => $destinations])
|
|
->toResponse($request), static fn (\Symfony\Component\HttpFoundation\Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function dossier(Request $request, string $id, FundingDossierPreparation $service): JsonResponse
|
|
{
|
|
return response()->json($service->save($request->user(), $id, $request->all()))->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
public function assess(Request $request, string $id, FundingProgrammes $service): JsonResponse
|
|
{
|
|
return response()->json($service->assess($id, $request->all()))->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
public function saveInterest(Request $request, string $id, FundingProgrammes $service): RedirectResponse
|
|
{
|
|
$data = $request->validate(['version_id' => 'required|uuid', 'consent' => 'required|accepted']);
|
|
$service->saveInterest($request->user(), $id, $data['version_id'], true);
|
|
|
|
return back()->with('success', 'Programa guardado na sua área privada. Consulte a versão indicada na ficha.');
|
|
}
|
|
|
|
public function removeInterest(Request $request, string $id, FundingProgrammes $service): RedirectResponse
|
|
{
|
|
abort_unless($service->installed(), 404);
|
|
$service->removeInterest($request->user(), $id);
|
|
|
|
return back()->with('success', 'Interesse removido da sua lista.');
|
|
}
|
|
|
|
public function download(string $id, string $version, int $resource, FundingProgrammes $service): Response|BinaryFileResponse
|
|
{
|
|
return $service->download($id, $version, $resource);
|
|
}
|
|
}
|