This commit is contained in:
2026-11-02 20:31:15 +02:00
parent 8ddfa93f45
commit 1da273f1f3
131 changed files with 14802 additions and 526 deletions
@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Models\Opportunity;
use App\Services\InvestmentCatalogue;
use App\Services\OpportunityMaturity;
use App\Services\OpportunityTextSearch;
use App\Services\SectorOrganization;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -41,16 +42,7 @@ class OpportunityController extends Controller
}
if (! empty($validated['q'])) {
$term = $validated['q'];
$query->where(fn ($q) => $q
->whereLike('title', "%{$term}%")
->orWhereLike('description', "%{$term}%")
->orWhereLike('summary', "%{$term}%")
->orWhereLike('reference', "%{$term}%")
->orWhereLike('province', "%{$term}%")
->orWhereHas('sector', fn ($sectorQuery) => $sectorQuery
->whereLike('name', "%{$term}%")
->orWhereLike('slug', "%{$term}%")));
app(OpportunityTextSearch::class)->constrain($query, $validated['q']);
}
if (! empty($validated['sector'])) {
@@ -0,0 +1,35 @@
<?php
namespace App\Http\Controllers;
use App\Services\DeskServiceEditor;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
final class DeskServiceEditorController extends Controller
{
public function index(Request $request, DeskServiceEditor $editor): Response|JsonResponse
{
$data = ['services' => $editor->listing()];
return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/service-editor', $data);
}
public function publish(Request $request, DeskServiceEditor $editor, string $code): JsonResponse|RedirectResponse
{
$row = $editor->publish($request->user(), $code, $request->all());
return $request->expectsJson() ? response()->json(['id' => $row->id, 'version' => $row->version], 201)
: back()->with('success', 'Nova versão publicada para pedidos futuros. Os pedidos existentes conservam a sua versão.');
}
public function history(Request $request, DeskServiceEditor $editor, string $code): JsonResponse
{
$data = $request->validate(['page' => ['nullable', 'integer', 'min:1']]);
return response()->json($editor->history($code, (int) ($data['page'] ?? 1)));
}
}
@@ -0,0 +1,182 @@
<?php
namespace App\Http\Controllers;
use App\Models\Sector;
use App\Services\DocumentManagementAccess;
use App\Services\FundingProgrammeReferences;
use App\Services\FundingProgrammes;
use App\Services\InvestmentGeography;
use App\Services\Procurement;
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
{
return tap(Inertia::render('funding/reference', ['reference' => $references->preview($request->user(), $code)])->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
{
$data = $request->validate(['hash' => 'required|string|size:64']);
$result = $references->prepare($request->user(), $code, $data['hash']);
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 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])->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']),
'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
{
$service->publish($request->user(), $id, $request->all());
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:fund,programme,incentive',
'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 !== '');
$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]));
}
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, 'kinds' => FundingProgrammes::KINDS,
'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
{
$record = $service->published($id);
$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])
->toResponse($request), static fn (\Symfony\Component\HttpFoundation\Response $response) => $response->headers->set('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);
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Http\Controllers;
use App\Services\InvestmentOpportunityAlerts;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
final class InvestmentAlertPreferencesController extends Controller
{
public function save(Request $request, InvestmentOpportunityAlerts $alerts): JsonResponse|RedirectResponse
{
$preferences = $alerts->save($request->user(), $request->all());
return $request->expectsJson() ? response()->json($preferences) : back()->with('success', $preferences['enabled']
? 'Preferências guardadas. O acompanhamento considera novas disponibilizações nos catálogos escolhidos.'
: 'Alertas dos catálogos cancelados. As notificações anteriores foram conservadas.');
}
}
@@ -11,6 +11,7 @@ use App\Services\InvestmentComplements;
use App\Services\InvestmentGeography;
use App\Services\InvestmentZones;
use App\Services\OpportunityCorridorExplorer;
use App\Services\OpportunityTextSearch;
use App\Services\SectorInvestment;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\JsonResponse;
@@ -29,12 +30,20 @@ class InvestmentDiscoveryController extends Controller
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)),
];
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' => $catalogue->query($filters)->paginate(12)->withQueryString()->through(fn ($record) => $catalogue->presentLocation($record)),
'results' => $results, 'relatedSearch' => $related,
'favourites' => $request->user() ? DB::table('opportunity_favourites')->where('user_id', $request->user()->id)->pluck('opportunity_id') : [],
'territorial' => $this->territorial($filters)]);
}
@@ -3,10 +3,17 @@
namespace App\Http\Controllers;
use App\Models\InvestmentSubmission;
use App\Models\Opportunity;
use App\Models\Sector;
use App\Models\User;
use App\Services\CountryCatalogue;
use App\Services\FundingProgrammes;
use App\Services\InvestmentGeography;
use App\Services\InvestmentOpportunityAlerts;
use App\Services\InvestmentWorkspace;
use App\Services\OpportunityManagement;
use App\Services\OrganizationCountry;
use App\Services\Procurement;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -119,14 +126,38 @@ class InvestmentWorkspaceController extends Controller
public function notifications(Request $request): Response|JsonResponse
{
$query = DB::table('investment_notifications')->where('user_id', $request->user()->id);
if (! $request->user()->can('manage-portal')) {
$alerts = app(InvestmentOpportunityAlerts::class);
$available = $alerts->installed();
if ($available) {
$query->where(function ($sources) use ($request, $alerts) {
$sources->whereIn('opportunity_id', Opportunity::published()->select('id'))
->orWhere(function ($legacy) use ($request) {
$legacy->whereNotNull('submission_id');
if (! $request->user()->can('manage-portal')) {
$legacy->whereIn('submission_id', InvestmentSubmission::where('user_id', $request->user()->id)->select('id'));
}
});
if ($alerts->tendersAvailable()) {
$sources->orWhereIn('tender_version_id', app(Procurement::class)->readableVersions()->select('id'));
}
if ($alerts->fundingAvailable()) {
$sources->orWhereIn('funding_version_id', app(FundingProgrammes::class)->readableVersions()->select('id'));
}
});
} elseif (! $request->user()->can('manage-portal')) {
$query->whereIn('submission_id', InvestmentSubmission::where('user_id', $request->user()->id)->select('id'));
}
$data = ['unread' => (clone $query)->whereNull('read_at')->count(), 'notifications' => $query->orderByDesc('created_at')->paginate(20)->through(function ($row) {
$row->payload = json_decode(Crypt::decryptString($row->payload), true, 512, JSON_THROW_ON_ERROR);
$data = ['alertPreferences' => $available ? $alerts->preferences($request->user()->id) : null,
'alertOptions' => $available ? ['frequencies' => InvestmentOpportunityAlerts::FREQUENCIES,
'sectors' => Sector::choices()->whereNotNull('published_at')->orderBy('name')->get(['slug', 'name']),
'provinces' => app(InvestmentGeography::class)->publicReferences()->where('location_type', 'province')->orderBy('name')->get(['id', 'name']),
'categories' => OpportunityManagement::CATEGORIES, 'tendersAvailable' => $alerts->tendersAvailable(), 'fundingAvailable' => $alerts->fundingAvailable(),
'tenderRegimes' => config('procurement.regimes'), 'tenderActs' => config('procurement.acts')] : null,
'unread' => (clone $query)->whereNull('read_at')->count(), 'notifications' => $query->orderByDesc('created_at')->orderByDesc('id')->paginate(20)->through(function ($row) {
$row->payload = json_decode(Crypt::decryptString($row->payload), true, 512, JSON_THROW_ON_ERROR);
return $row;
})];
return $row;
})];
return $request->expectsJson() ? response()->json($data) : Inertia::render('investment/notifications', $data);
}
@@ -135,7 +166,17 @@ class InvestmentWorkspaceController extends Controller
{
$row = DB::table('investment_notifications')->where('id', $id)->where('user_id', $request->user()->id)->first();
abort_unless($row !== null, 404);
$this->submission($request, $row->submission_id);
if ($row->opportunity_id ?? null) {
abort_unless(Opportunity::published()->whereKey($row->opportunity_id)->exists(), 404);
} elseif ($row->tender_version_id ?? null) {
abort_unless(app(InvestmentOpportunityAlerts::class)->tendersAvailable()
&& app(Procurement::class)->readableVersions()->where('id', $row->tender_version_id)->exists(), 404);
} elseif ($row->funding_version_id ?? null) {
abort_unless(app(InvestmentOpportunityAlerts::class)->fundingAvailable()
&& app(FundingProgrammes::class)->readableVersions()->where('id', $row->funding_version_id)->exists(), 404);
} else {
$this->submission($request, $row->submission_id);
}
DB::table('investment_notifications')->where('id', $id)->whereNull('read_at')->update(['read_at' => now()]);
return $request->expectsJson() ? response()->json(['read' => true]) : back();
+2 -23
View File
@@ -13,37 +13,16 @@ use Symfony\Component\HttpFoundation\Response;
class ProcurementController extends Controller
{
private const STATE_SQL = "CASE WHEN v.payload->>'procedural_state' IN ('cancelled','results') THEN v.payload->>'procedural_state' WHEN ((v.payload->>'deadline')::timestamp AT TIME ZONE (v.payload->>'timezone'))<=CURRENT_TIMESTAMP THEN 'deadline_ended' WHEN v.payload->>'extended'='true' THEN 'extended' ELSE 'open' END";
private function query(Procurement $service): Builder
{
abort_unless($service->available(), 404);
return DB::table('portal_tenders as t')->join('portal_tender_versions as v', fn ($q) => $q->on('v.id', '=', 't.published_version_id')->on('v.tender_id', '=', 't.id'))->whereNotNull('v.published_at');
return $service->publicQuery();
}
public function index(Request $request, Procurement $service): Response
{
$filters = $request->validate(['q' => 'nullable|string|max:150', 'entity' => 'nullable|uuid', 'sector' => 'nullable|integer', 'territory' => 'nullable|uuid', 'modality' => 'nullable|string|max:200', 'financier' => 'nullable|string|max:200', 'state' => 'nullable|in:open,extended,deadline_ended,cancelled,results', 'deadline_before' => 'nullable|date_format:Y-m-d', 'regime' => 'nullable|in:state,external,private,other']);
$filters = array_filter($filters, fn ($value) => $value !== null && $value !== '');
$query = $this->query($service);
if (! empty($filters['q'])) {
$query->where(fn ($q) => $q->whereRaw("v.payload->>'title' ILIKE ?", ['%'.$filters['q'].'%'])->orWhereRaw("v.payload->>'object' ILIKE ?", ['%'.$filters['q'].'%'])->orWhereRaw("v.payload->>'reference' ILIKE ?", ['%'.$filters['q'].'%']));
}
foreach (['entity' => 'organization_id', 'sector' => 'sector_id', 'modality' => 'modality', 'financier' => 'financier', 'regime' => 'regime'] as $filter => $field) {
if (! empty($filters[$filter])) {
$query->whereRaw('v.payload->>? = ?', [$field, (string) $filters[$filter]]);
}
}
if (! empty($filters['territory'])) {
$query->whereRaw("v.payload->>'territory_id' IN (WITH RECURSIVE scope(id,depth) AS (SELECT id,0 FROM geo_locations WHERE id=? AND deleted_at IS NULL UNION ALL SELECT c.id,s.depth+1 FROM geo_locations c JOIN scope s ON c.parent_id=s.id WHERE c.deleted_at IS NULL AND s.depth<4) SELECT id::text FROM scope)", [$filters['territory']]);
}
if (! empty($filters['state'])) {
$query->whereRaw(self::STATE_SQL.' = ?', [$filters['state']]);
}
if (! empty($filters['deadline_before'])) {
$query->whereRaw("left(v.payload->>'deadline',10)<=?", [$filters['deadline_before']]);
}
$query = $service->publicQuery($filters);
$rows = $query->select('t.id', 'v.id as vid')->orderByDesc('v.published_at')->paginate(20)->withQueryString();
$rows->through(fn ($r) => ['id' => $r->id, ...$this->summary($service, $r->vid)]);
$facets = $this->query($service)->get(['v.payload'])->map(fn ($r) => json_decode($r->payload, true));
@@ -87,8 +87,17 @@ class PublicBusinessOpportunityController extends Controller
}
$returnFilters = $this->filters($request);
$similarProvince = $record->getAttribute('similar_province_id');
if ($similarProvince !== null && ! app(InvestmentGeography::class)->publicReferences()->where('geo_locations.id', $similarProvince)->exists()) {
$similarProvince = null;
}
return Inertia::render('investment/business-opportunity', ['record' => $resource->resolve($request),
'similarCatalogueUrl' => '/negocios?'.http_build_query(array_filter([
'modality' => $record->modality,
'sector' => $record->sector_id,
'province_id' => $similarProvince,
], fn ($value) => $value !== null && $value !== '')),
'returnUrl' => $returnFilters === [] ? '/negocios' : '/negocios?'.http_build_query($returnFilters),
'interestUrl' => '/negocios/'.rawurlencode($slug).'/manifestar-interesse'.($returnFilters === [] ? '' : '?'.http_build_query($returnFilters))]);
}
@@ -116,7 +125,7 @@ class PublicBusinessOpportunityController extends Controller
->leftJoin('geo_locations as locality', 'locality.id', '=', 'locations.locality_id')
->where('locations.is_public', true)->where('locations.is_primary', true)
->whereNull('province.deleted_at')->whereNull('district.deleted_at')->whereNull('locality.deleted_at')
->select('locations.existing_business_id', 'province.name as province')
->select('locations.existing_business_id', 'province.name as province', 'province.id as similar_province_id')
->selectRaw("CASE WHEN locations.precision_level IN ('district','admin_post','locality','exact') THEN district.name ELSE NULL END AS district")
->selectRaw('NULL::text AS locality');
@@ -127,6 +136,6 @@ class PublicBusinessOpportunityController extends Controller
'existing_businesses.currency', 'existing_businesses.amount_min', 'existing_businesses.amount_max',
'existing_businesses.available_percentage', 'existing_businesses.published_at', 'existing_businesses.sector_id',
'existing_businesses.owner_user_id', 'existing_businesses.organization_id', 'existing_businesses.version_no', 'existing_businesses.expires_at', 'existing_businesses.status', 'existing_businesses.modality',
'public_location.province', 'public_location.district', 'public_location.locality']);
'public_location.province', 'public_location.district', 'public_location.locality', 'public_location.similar_province_id']);
}
}
@@ -152,7 +152,7 @@ class VirtualDeskController extends Controller
'serviceSource' => json_decode(DB::table('desk_service_versions')->where('id', $row->service_version_id)->value('source'), true),
'owner' => $desk->owns($request->user(), $case), 'manager' => app(ScopedPortalAdministration::class)->deskDecision($request->user(), $row), 'analyst' => (int) $row->analyst_id === $request->user()->id,
'fieldsEditable' => in_array($row->status, ['draft', 'returned'], true), 'states' => config('virtual-desk.request_states'), 'transitions' => config('virtual-desk.transitions.'.$row->status, []),
'findings' => $desk->findings($payload), 'ai' => config('virtual-desk.ai'), 'documents' => $this->documents($case->id, $request->user())->filter(fn ($d) => ! $d->request_id || $d->request_id === $id)->values(),
'findings' => $desk->findings($payload, $definition), 'ai' => config('virtual-desk.ai'), 'documents' => $this->documents($case->id, $request->user())->filter(fn ($d) => ! $d->request_id || $d->request_id === $id)->values(),
'messages' => DB::table('desk_messages as m')->join('users as u', 'u.id', '=', 'm.actor_id')->where('request_id', $id)->orderBy('m.created_at')->get(['m.id', 'm.actor_id', 'm.body', 'm.created_at', 'u.name'])->map(fn ($m) => [...(array) $m, 'body' => Crypt::decryptString($m->body),
'kind' => $messageKinds[$m->id] ?? 'historical_unclassified']),
'events' => $this->events($case->id, $desk, $request->user())->filter(fn ($e) => $e['request_id'] === $id)->values(),
@@ -245,7 +245,10 @@ class VirtualDeskController extends Controller
$this->auditRead($request, 'desk_requests', $id, 'receipt_downloaded');
if ($format === 'html') {
return response()->view('desk.receipt', ['record' => $row, 'snapshot' => $snapshot, 'definition' => $desk->definition($row)], 200, ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']);
$definition = $desk->definition($row);
return response()->view('desk.receipt', ['record' => $row, 'snapshot' => $snapshot, 'definition' => $definition,
'applicableFields' => $desk->applicableFields($snapshot['payload'] ?? [], $definition)], 200, ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']);
}
return response()->streamDownload(function () use ($row, $snapshot) {