validate(['q' => ['nullable', 'string', 'max:150']]); $model = $this->model($structure); $query = $model::query(); if ($term = $filters['q'] ?? null) { $query->where(function ($q) use ($term): void { $q->whereLike('name', "%{$term}%")->orWhereLike('code', "%{$term}%"); }); } return Inertia::render('structures/index', ['structure' => $structure, 'filters' => $filters, 'records' => $query->orderBy('name')->orderBy('id')->paginate(20)->withQueryString()]); } public function edit(Request $request, string $structure, ?string $id = null): Response { $model = $this->model($structure); $record = $id ? $model::findOrFail($id) : null; return Inertia::render('structures/editor', ['structure' => $structure, 'record' => $record, 'projects' => $structure === 'clusters' && $id ? Project::orderBy('name')->get(['id', 'name', 'code']) : [], 'chains' => $structure === 'clusters' && $id ? ValueChain::where('cluster_id', $id)->orderBy('code')->get() : [], 'gaps' => $structure === 'clusters' && $id ? InvestmentGap::where('cluster_id', $id)->orderBy('created_at')->get() : [], 'chainLinks' => $structure === 'clusters' && $id ? DB::table('value_chain_links') ->join('value_chains as origins', 'origins.id', '=', 'value_chain_links.from_value_chain_id') ->join('value_chains as destinations', 'destinations.id', '=', 'value_chain_links.to_value_chain_id') ->where('origins.cluster_id', $id)->where('destinations.cluster_id', $id) ->whereNull('origins.deleted_at')->whereNull('destinations.deleted_at')->whereNull('value_chain_links.deleted_at') ->orderBy('origins.code')->orderBy('destinations.code') ->get(['value_chain_links.id', 'origins.name as origin', 'destinations.name as destination', 'link_type']) : [], 'competitiveness' => $structure === 'clusters' && $id ? app(ClusterCompetitiveness::class)->management($id) : null, 'viewerId' => $request->user()->id, 'memberships' => $structure === 'clusters' && $id ? DB::table('cluster_projects') ->join('projects', 'projects.id', '=', 'cluster_projects.project_id') ->where('cluster_id', $id)->whereNull('cluster_projects.deleted_at')->whereNull('projects.deleted_at') ->orderBy('projects.name')->get(['cluster_projects.id', 'cluster_projects.project_id', 'cluster_projects.role', 'cluster_projects.association_type', 'cluster_projects.source_reference', 'cluster_projects.evidence_reference', 'cluster_projects.valid_from', 'cluster_projects.valid_until', 'cluster_projects.validation_status', 'cluster_projects.version_no', 'cluster_projects.editor_id', 'projects.name']) : [], ]); } public function save(Request $request, string $structure, ?string $id = null): RedirectResponse { $isProject = $structure === 'projects'; $isCluster = $structure === 'clusters'; $isCorridor = $structure === 'corridors'; $model = $this->model($structure); $record = $id ? $model::findOrFail($id) : new $model; $rules = [ 'code' => ['required', 'string', 'max:100', Rule::unique($record->getTable(), 'code')->whereNull('deleted_at')->ignore($record->getKey())], 'name' => ['required', 'string', 'max:200'], 'status' => ['required', Rule::in($isProject ? ['draft', 'planned', 'active', 'suspended', 'completed', 'cancelled'] : ($isCluster ? ['planned', 'active', 'inactive'] : ['proposed', 'active', 'closed']))], ]; if ($isProject || $isCorridor) { $rules['description'] = ['nullable', 'string', 'max:10000']; } if ($isProject) { $rules += [ 'budget_amount' => ['nullable', 'required_with:budget_currency', 'regex:/^\d{1,18}(\.\d{1,2})?$/'], 'budget_currency' => ['nullable', 'required_with:budget_amount', 'regex:/^[A-Z]{3}$/'], 'start_date' => ['nullable', 'date_format:Y-m-d'], 'end_date' => ['nullable', 'date_format:Y-m-d', ...($request->filled('start_date') ? ['after_or_equal:start_date'] : [])]]; } elseif ($isCluster) { $rules['sector'] = ['required', 'string', 'max:200']; } if (! $id) { foreach (InvestmentGeography::FIELDS as $field => $type) { $rules[$field] = ['nullable', 'uuid']; } } $data = $request->validate($rules); if (! $id && $request->has('province_id')) { $path = app(InvestmentGeography::class)->selection($data, true); $data = array_diff_key($data, InvestmentGeography::FIELDS); $data['primary_territory_id'] = $path === [] ? null : $path[array_key_last($path)]->id; $data['territory_version'] = $path === [] ? 0 : 1; } PortalAudit::save($record, $data, $request->user()->id); return redirect('/management/'.$structure.'/'.$record->getKey().'/edit')->with('success', 'Registo guardado.'); } /** @return class-string */ private function model(string $structure): string { return match ($structure) { 'projects' => Project::class, 'clusters' => EconomicCluster::class, 'corridors' => DevelopmentCorridor::class, default => abort(404), }; } public function linkProject(Request $request, EconomicCluster $cluster): RedirectResponse { $data = $request->validate(['project_id' => ['required', 'uuid', Rule::exists('projects', 'id')->whereNull('deleted_at')], 'version_no' => ['required', 'integer', 'min:0'], 'role' => ['required', Rule::in(['anchor', 'complementary', 'supporting'])], 'association_type' => ['required', Rule::in(['official', 'influence', 'proximity', 'potential', 'historical'])], 'source_reference' => ['required', 'string', 'min:10', 'max:2000'], 'evidence_reference' => ['required', 'string', 'min:3', 'max:2000'], 'valid_from' => ['required', 'date_format:Y-m-d'], 'valid_until' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:valid_from']]); DB::transaction(function () use ($cluster, $data, $request) { EconomicCluster::whereKey($cluster->id)->lockForUpdate()->firstOrFail(); $link = ClusterProject::where('cluster_id', $cluster->id)->where('project_id', $data['project_id'])->whereNull('deleted_at')->lockForUpdate()->first(); if ($link) { abort_if((int) $link->version_no !== (int) $data['version_no'], 409, 'Versao da associacao alterada.'); $changed = false; foreach (['role', 'association_type', 'source_reference', 'evidence_reference', 'valid_from', 'valid_until'] as $field) { $current = $link->getAttribute($field); $current = $current instanceof \DateTimeInterface ? $current->format('Y-m-d') : $current; if (($current ?: null) !== (($data[$field] ?? null) ?: null)) { $changed = true; break; } } if (! $changed) { return; } } else { abort_if((int) $data['version_no'] !== 0, 409, 'A associacao foi alterada. Actualize a pagina.'); $link = new ClusterProject; } unset($data['version_no']); $nextVersion = $link->exists ? ((int) $link->version_no + 1) : 1; PortalAudit::save($link, [...$data, 'cluster_id' => $cluster->id, 'validation_status' => 'pending', 'editor_id' => $request->user()->id, 'validator_id' => null, 'version_no' => $nextVersion], $request->user()->id); }); return back()->with('success', 'Associação ao projecto guardada.'); } public function saveChain(Request $request, EconomicCluster $cluster, ?ValueChain $chain = null): RedirectResponse { abort_if($chain && $chain->cluster_id !== $cluster->id, 404); $data = $request->validate(['code' => ['required', 'string', 'max:100', Rule::unique('value_chains', 'code')->where('cluster_id', $cluster->id)->whereNull('deleted_at')->ignore($chain?->id)], 'name' => ['required', 'string', 'max:200'], 'stage' => ['required', Rule::in(['input', 'production', 'processing', 'logistics', 'market'])]]); PortalAudit::save($chain ?? new ValueChain, [...$data, 'cluster_id' => $cluster->id], $request->user()->id); return back()->with('success', 'Etapa da cadeia de valor guardada.'); } }