146 lines
9.3 KiB
PHP
146 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers;
|
|
|
|
use App\Services\Procurement;
|
|
use Illuminate\Database\Query\Builder;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Inertia\Inertia;
|
|
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');
|
|
}
|
|
|
|
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']]);
|
|
}
|
|
$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));
|
|
$options = [];
|
|
foreach (['entity' => ['organization_id', 'organization_name'], 'sector' => ['sector_id', 'sector_name'], 'territory' => ['territory_id', 'territory_name'], 'modality' => ['modality', 'modality'], 'financier' => ['financier', 'financier']] as $name => [$value,$label]) {
|
|
$options[$name] = $facets->filter(fn ($p) => ! empty($p[$value]))->map(fn ($p) => ['id' => (string) $p[$value], 'name' => $p[$label]])->unique('id')->values();
|
|
}
|
|
|
|
return tap(Inertia::render('procurement/index', ['items' => $rows, 'filters' => $filters, 'options' => $options, 'regimes' => config('procurement.regimes'), 'management' => $service->access($request->user())])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
/**
|
|
* @return array<array-key, mixed>
|
|
*/
|
|
private function summary(Procurement $service, string $vid): array
|
|
{
|
|
$v = $service->publishedVersion($vid);
|
|
|
|
return ['publication' => $v, 'state' => $service->state($v)];
|
|
}
|
|
|
|
public function show(Request $request, string $id, Procurement $service): Response
|
|
{
|
|
return $this->detail($request, $id, $service, false);
|
|
}
|
|
|
|
public function print(Request $request, string $id, Procurement $service): Response
|
|
{
|
|
return $this->detail($request, $id, $service, true);
|
|
}
|
|
|
|
private function detail(Request $request, string $id, Procurement $service, bool $print): Response
|
|
{
|
|
$row = $this->query($service)->where('t.id', $id)->firstOrFail(['t.id', 'v.id as vid']);
|
|
$filters = $request->only(['q', 'entity', 'sector', 'territory', 'modality', 'financier', 'state', 'deadline_before', 'regime', 'page']);
|
|
|
|
return tap(Inertia::render('procurement/show', ['id' => $id, ...$this->summary($service, $row->vid), 'printMode' => $print, 'backUrl' => '/concursos'.($filters ? '?'.http_build_query($filters) : ''),
|
|
'history' => DB::table('portal_tender_versions')->where('tender_id', $id)->whereNotNull('published_at')->orderBy('version')->pluck('id')->map(fn ($vid) => $service->publishedVersion($vid)),
|
|
'watching' => $request->user() && DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->exists()])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function manage(Request $request, Procurement $service): Response
|
|
{
|
|
$service->guard($request->user());
|
|
|
|
return tap(Inertia::render('procurement/manage', ['items' => DB::table('portal_tenders')->orderByDesc('updated_at')->get(['id', 'draft', 'version', 'published_version_id'])->map(fn ($r) => ['id' => $r->id, 'title' => json_decode($r->draft, true)['title'] ?? 'Rascunho sem título', 'version' => $r->version, 'published' => (bool) $r->published_version_id])])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function edit(Request $request, Procurement $service, ?string $id = null): Response
|
|
{
|
|
$service->guard($request->user());
|
|
$r = $id ? $service->record($id) : null;
|
|
|
|
return tap(Inertia::render('procurement/edit', ['record' => $r ? ['id' => $id, 'version' => $r->version, 'draft' => json_decode($r->draft, true), 'published' => (bool) $r->published_version_id] : null,
|
|
'references' => $service->references($request->user()), 'history' => $id ? DB::table('portal_tender_versions')->where('tender_id', $id)->orderByDesc('version')->get(['version', 'act', 'reason', 'actor_id', 'published_at', 'created_at']) : []])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
|
|
public function save(Request $request, Procurement $service, ?string $id = null): RedirectResponse
|
|
{
|
|
$id = $service->save($request->user(), $id, $request->all());
|
|
|
|
return redirect('/management/concursos/'.$id)->with('success', 'Rascunho guardado. A publicação anterior permanece intacta.');
|
|
}
|
|
|
|
public function preview(Request $request, string $id, Procurement $service): JsonResponse
|
|
{
|
|
return response()->json($service->preview($request->user(), $id, (string) $request->input('act')))->header('Cache-Control', 'private, no-store');
|
|
}
|
|
|
|
public function publish(Request $request, string $id, Procurement $service): RedirectResponse
|
|
{
|
|
$service->publish($request->user(), $id, $request->all());
|
|
|
|
return redirect('/management/concursos/'.$id)->with('success', 'Acto publicado, com versão e auditoria. Nenhuma proposta recebida pelo portal.');
|
|
}
|
|
|
|
public function watch(Request $request, string $id, Procurement $service): RedirectResponse
|
|
{
|
|
$row = $this->query($service)->where('t.id', $id)->firstOrFail(['v.id']);
|
|
DB::table('portal_tender_watches')->updateOrInsert(['tender_id' => $id, 'user_id' => $request->user()->id], ['seen_version_id' => $row->id, 'created_at' => now(), 'updated_at' => now()]);
|
|
|
|
return back()->with('success', 'Concurso guardado para acompanhamento. Isto não apresenta uma proposta.');
|
|
}
|
|
|
|
public function unwatch(Request $request, string $id, Procurement $service): RedirectResponse
|
|
{
|
|
abort_unless($service->available(), 404);
|
|
DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->delete();
|
|
|
|
return back();
|
|
}
|
|
|
|
public function watched(Request $request, Procurement $service): Response
|
|
{
|
|
$rows = $this->query($service)->join('portal_tender_watches as w', 'w.tender_id', '=', 't.id')->where('w.user_id', $request->user()->id)->get(['t.id', 'v.id as vid', 'w.seen_version_id'])->map(fn ($r) => ['id' => $r->id, 'changed' => $r->seen_version_id !== $r->vid, ...$this->summary($service, $r->vid)]);
|
|
|
|
return tap(Inertia::render('procurement/manage', ['watched' => true, 'items' => $rows])->toResponse($request), static fn (Response $response) => $response->headers->set('Cache-Control', 'private, no-store'));
|
|
}
|
|
}
|