diff --git a/app/Http/Controllers/InvestorContextController.php b/app/Http/Controllers/InvestorContextController.php index cd648ede..12b289d1 100644 --- a/app/Http/Controllers/InvestorContextController.php +++ b/app/Http/Controllers/InvestorContextController.php @@ -11,6 +11,19 @@ use Inertia\Inertia; final class InvestorContextController extends Controller { + private function entryChoices(Request $r, InvestorContext $context): array + { + $data = $r->validate(['source' => 'nullable|string|max:100', 'from_dossier' => 'nullable|uuid', 'opportunities' => 'sometimes|array|max:100', 'opportunities.*' => 'integer|distinct|min:1', + 'province_id' => 'nullable|uuid', 'district_id' => 'nullable|uuid', 'admin_post_id' => 'nullable|uuid', 'locality_id' => 'nullable|uuid']); + $source = !empty($data['source']) ? $context->reference($data['source']) : null; + $ids = $data['opportunities'] ?? (($source['kind'] ?? '') === 'opportunity' ? [$source['id']] : []); + abort_if(!empty($data['from_dossier']) && !$ids, 422, 'Escolha a oportunidade associada à ficha da carteira.'); + $path = app(InvestmentGeography::class)->selection($data); + $input = array_map(fn($id) => ['source' => 'opportunity:'.$id, ...($path ? ['territory' => 'territory:'.$path[array_key_last($path)]->id] : [])], $ids); + if (!empty($data['from_dossier'])) $input[0]['portfolio_origins'] = [$data['from_dossier']]; + return app(\App\Services\InvestmentOpportunityChoices::class)->resolve($input, [], true); + } + public function current(Request $r, InvestorContext $context) { return response()->json($context->current($r->user())); @@ -51,22 +64,27 @@ final class InvestorContextController extends Controller $data = $context->view($r->user(), $id); abort_unless($data['can_write'], 403); - $ids = $r->validate(['opportunities' => 'sometimes|array|max:100', 'opportunities.*' => 'integer|distinct|min:1'])['opportunities'] ?? []; - $choices = $ids ? app(\App\Services\InvestmentOpportunityChoices::class)->resolve(array_map(fn ($id) => ['source' => 'opportunity:'.$id], $ids)) : null; + $entries = $this->entryChoices($r, $context); + $choices = $entries ?: null; $source = $r->query('source') ? $context->reference($r->validate(['source' => 'required|string|max:100'])['source']) : null; if ($choices === null && ($source['kind'] ?? '') === 'opportunity') $choices = app(\App\Services\InvestmentOpportunityChoices::class)->resolve([['source' => $source['key']]]); $explicitChoices=$choices; $selectionKey=$r->validate(['selection_key'=>'nullable|uuid'])['selection_key']??null; $entryKey=fn($c)=>$c['opportunity']['key'].($selectionKey?'@'.$selectionKey:''); $entrySources=array_map($entryKey,$explicitChoices??[]); - if ($choices !== null) $choices = collect([...($data['context']['opportunity_choices'] ?? []), ...$choices])->unique(fn ($choice) => $choice['opportunity']['key'])->values()->all(); + if ($choices !== null) $choices = app(\App\Services\InvestmentOpportunityChoices::class)->mergeEntries($data['context']['opportunity_choices'] ?? [], $choices); $draft = $context->draft($r->user(), $id); if ($draft && isset($draft['opportunity_choices'])) { $recovered = app(\App\Services\InvestmentOpportunityChoices::class)->resolve($draft['opportunity_choices'], $data['context']['opportunity_choices'] ?? [], true); $new=array_filter($explicitChoices??[],fn($c)=>!in_array($entryKey($c),$draft['entry_sources']??[],true)); - $choices = collect([...$recovered, ...$new])->unique(fn($c)=>$c['opportunity']['key'])->values()->all(); + $choices = app(\App\Services\InvestmentOpportunityChoices::class)->mergeEntries($recovered, array_values($new)); } - return Inertia::render('investor-path/context', ['entrySources'=>$entrySources,'draft' => $draft, 'requestedChoices' => $choices, 'initial' => $data, 'options' => $context->options(), 'source' => $source, 'territorySelection' => isset($data['context']['selection']['territory']) ? app(InvestmentGeography::class)->path($data['context']['selection']['territory']['id']) : []]); + $differences = []; + foreach ($explicitChoices ?? [] as $entry) { + $kept = collect($choices ?? [])->firstWhere('opportunity.key', $entry['opportunity']['key']); + if ($kept && ($kept['territory']['key'] ?? null) !== ($entry['territory']['key'] ?? null)) $differences[] = ['title'=>$entry['opportunity']['title'],'saved'=>$kept['territory']['title']??'Por definir','requested'=>$entry['territory']['title']??'Por definir']; + } + return Inertia::render('investor-path/context', ['entryLocationDifferences'=>$differences,'entrySources'=>$entrySources,'draft' => $draft, 'requestedChoices' => $choices, 'initial' => $data, 'options' => $context->options(), 'source' => $source, 'territorySelection' => isset($data['context']['selection']['territory']) ? app(InvestmentGeography::class)->path($data['context']['selection']['territory']['id']) : []]); } public function preview(Request $r, string $id, InvestorContext $context) @@ -84,9 +102,8 @@ final class InvestorContextController extends Controller $d = $r->validate(['source' => 'nullable|string|max:100', 'step' => ['nullable', 'regex:/^S(0[1-9]|1[0-9])$/']]); $ref = isset($d['source']) ? $context->reference($d['source']) : null; - $ids = $r->validate(['opportunities' => 'sometimes|array|max:100', 'opportunities.*' => 'integer|distinct|min:1'])['opportunities'] ?? []; - $choices = $ids ? app(\App\Services\InvestmentOpportunityChoices::class)->resolve(array_map(fn ($id) => ['source' => 'opportunity:'.$id], $ids)) : []; - return Inertia::render('investor-path/context-start', ['opportunityChoices' => $choices, 'contexts' => $path->contexts($r->user()), 'source' => $ref, 'step' => $d['step'] ?? null]); + $choices = $this->entryChoices($r, $context); + return Inertia::render('investor-path/context-start', ['entryQuery' => $r->only(['from_dossier', 'province_id', 'district_id', 'admin_post_id', 'locality_id']), 'opportunityChoices' => $choices, 'contexts' => $path->contexts($r->user()), 'source' => $ref, 'step' => $d['step'] ?? null]); } public function installation(Request $r, InvestorContextInstallation $install) diff --git a/app/Services/InvestmentOpportunityChoices.php b/app/Services/InvestmentOpportunityChoices.php index 807b89ab..1e2f05d9 100644 --- a/app/Services/InvestmentOpportunityChoices.php +++ b/app/Services/InvestmentOpportunityChoices.php @@ -2,11 +2,41 @@ namespace App\Services; +use App\Http\Controllers\InvestmentPortfolioController; use App\Models\Opportunity; +use Illuminate\Support\Facades\DB; /** Multiple identified references in the existing encrypted investment context. */ final class InvestmentOpportunityChoices { + public function portfolioOrigins(array $ids, int $opportunity): array + { + $origins = []; + foreach (array_unique($ids) as $id) { + $record = InvestmentPortfolioController::visible()->addSelect('version_no')->findOrFail($id); + $kind = null; + foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) { + if ($record->$foreign && DB::table('opportunity_'.$relation)->where('opportunity_id', $opportunity)->where($foreign, $record->$foreign)->exists()) $kind = rtrim($relation, 's'); + } + abort_unless($kind, 422, 'A associação escolhida não pertence a esta oportunidade.'); + $origins[] = ['id' => $record->id, 'title' => $record->title, 'kind' => $kind, 'href' => '/carteira/'.$record->id, + 'version' => $record->version_no, 'sha256' => hash('sha256', json_encode([$record->title, $record->source, $record->content], JSON_THROW_ON_ERROR))]; + } + return $origins; + } + + /** Add explicitly consulted associations without resetting saved geography or deliberate answers. */ + public function mergeEntries(array $saved, array $entries): array + { + foreach ($entries as $entry) { + $index = array_search($entry['opportunity']['key'], array_column(array_column($saved, 'opportunity'), 'key'), true); + if ($index === false) { $saved[] = $entry; continue; } + $origins = collect([...($saved[$index]['portfolio_origins'] ?? []), ...($entry['portfolio_origins'] ?? [])])->unique('id')->values()->all(); + if ($origins) $saved[$index]['portfolio_origins'] = $origins; + } + return $saved; + } + public function resolve(array $choices, array $previous = [], bool $pending = false): array { $result = []; @@ -15,6 +45,7 @@ final class InvestmentOpportunityChoices abort_unless($ref['kind'] === 'opportunity', 422, 'Seleccione uma ficha de oportunidade.'); $record = Opportunity::findOrFail($ref['id']); $old = collect($previous)->first(fn ($row) => $row['opportunity']['key'] === $ref['key']); + $origins = $this->portfolioOrigins($choice['portfolio_origins'] ?? array_column($old['portfolio_origins'] ?? [], 'id'), (int) $record->id); $corridors = app(InvestmentCorridorContext::class); $corridor = $choice['corridor_key'] ?? $old['corridor_key'] ?? $corridors->key($record); $provinces = $corridor ? $corridors->provinces($corridor) : []; @@ -33,7 +64,7 @@ final class InvestmentOpportunityChoices } $location = $territory ? app(InvestorContext::class)->reference($territory) : null; abort_unless($location === null || $location['kind'] === 'territory', 422); - $result[] = ['opportunity' => $ref, 'territory' => $location, + $result[] = ['opportunity' => $ref, 'territory' => $location, ...($origins ? ['portfolio_origins' => $origins] : []), 'corridor_key' => $corridor, 'corridor_name' => $corridor ? config('investment_corridors.'.$corridor.'.name') : null, 'allowed_provinces' => array_map(fn($p)=>(array)$p, $provinces), 'territory_path' => $location ? array_map(fn ($node) => \Illuminate\Support\Arr::only((array) $node, ['id', 'name', 'parent_id', 'location_type']), app(InvestmentGeography::class)->path($location['id'], true)) : [], diff --git a/app/Services/InvestorContext.php b/app/Services/InvestorContext.php index cdeb8c09..bf201328 100644 --- a/app/Services/InvestorContext.php +++ b/app/Services/InvestorContext.php @@ -52,6 +52,7 @@ final class InvestorContext $ref = $this->reference($choice['source']); abort_unless($ref['kind'] === 'opportunity', 422); $record = Opportunity::findOrFail($ref['id']); + app(InvestmentOpportunityChoices::class)->portfolioOrigins($choice['portfolio_origins'] ?? [], (int) $record->id); $key = $choice['corridor_key'] ?? app(InvestmentCorridorContext::class)->key($record); if (!empty($choice['territory'])) { $location = $this->reference($choice['territory']); @@ -118,6 +119,7 @@ final class InvestorContext public function dependencyHash(array $selection): string { + if (isset($selection['opportunity_choices'])) $selection['opportunity_choices'] = array_map(fn($choice)=>Arr::except($choice,['portfolio_origins']),$selection['opportunity_choices']); return hash('sha256', json_encode(Arr::sortRecursive($selection), JSON_THROW_ON_ERROR)); } @@ -339,7 +341,8 @@ final class InvestorContext } return Validator::make($input, [...$inputRules, - 'opportunity_choices' => 'sometimes|array|max:100', 'opportunity_choices.*' => 'array:source,territory,location_reason,confirm_location_change,activities,corridor_key', + 'opportunity_choices' => 'sometimes|array|max:100', 'opportunity_choices.*' => 'array:source,territory,location_reason,confirm_location_change,activities,corridor_key,portfolio_origins', + 'opportunity_choices.*.portfolio_origins' => 'sometimes|array|max:100', 'opportunity_choices.*.portfolio_origins.*' => 'uuid', 'opportunity_choices.*.corridor_key' => ['sometimes', 'nullable', Rule::in(array_keys(config('investment_corridors')))], 'opportunity_choices.*.activities' => 'sometimes|array', 'opportunity_choices.*.activities.*' => 'string|distinct|in:production,processing,trade,transport,water,forestry,animal', 'opportunity_choices.*.source' => 'required|string|distinct|max:100', 'opportunity_choices.*.territory' => 'nullable|string|max:100', @@ -473,11 +476,13 @@ final class InvestorContext } $changedFields = array_column($replaced, 'field'); $choicesChanged = $choices !== ($old['opportunity_choices'] ?? []); - if ($choicesChanged) $changed = array_values(array_unique([...$changed, 'opportunity', 'territory'])); + $choiceInputsChanged = array_map(fn($c)=>Arr::except($c,['portfolio_origins']),$choices) !== array_map(fn($c)=>Arr::except($c,['portfolio_origins']),$old['opportunity_choices']??[]); + if ($choiceInputsChanged) $changed = array_values(array_unique([...$changed, 'opportunity', 'territory'])); + elseif ($choicesChanged) $changed[] = 'portfolio_origins'; $conditionsChanged = (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary'])); - $dependencyChanged = $choicesChanged || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged; + $dependencyChanged = $choiceInputsChanged || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged; $privateConditionsChanged = (bool) array_intersect($changedFields, PrivateInvestmentPreparation::FIELDS); - $calculationInputsChanged = $choicesChanged || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])])); + $calculationInputsChanged = $choiceInputsChanged || $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])])); $payload = $this->desk->decode($case->payload); $results = $payload['investor_path']['results'] ?? []; $requests = DB::table('desk_requests')->where('case_id', $id)->orderBy('id')->get(['id', 'status', 'protocol', 'version'])->all(); diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 13f48ea4..057da369 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -1,5 +1,27 @@ # Rastreabilidade, utilização e formação +## Continuidade carteira → oportunidade → dossier → simulador — incremento QA, 23/10/2026 + +Origem: pedido expresso da Tempo de conservar oportunidade, localização e associações escolhidas no mesmo percurso, sem repetir perguntas ou sobrescrever edições deliberadas. Complementa a entrega QA `6948ed62`; não instala esse conjunto nem este incremento na homologação. + +Os dois botões Preparar investimento da oportunidade passam a transportar a origem `from_dossier` já validada e os filtros territoriais escolhidos. A entrada mostra a oportunidade, localização proposta e associação da carteira antes de escolher/criar o dossier. As mesmas referências atravessam autenticação, rascunho por actor, recuperação, prévia, confirmação e a referência guardada do simulador. Permanecem no payload cifrado existente: sem migrations, papéis ou percurso paralelo. Posto/localidade não são inferidos quando faltam. + +Cada oportunidade conserva as suas próprias associações escolhidas (`portfolio_origins`), com identificador, título, versão, ligação e hash documental. O servidor só aceita fichas publicadas da carteira com ligação cadastrada à oportunidade. Não cria relações por semelhança de nome, corredor ou proximidade e não publica fichas privadas. É possível retirar uma associação pela interface sem retirar a oportunidade. Uma mesma associação pode acompanhar oportunidades diferentes, mantendo cada identidade e localização. + +Guia: abra uma oportunidade associada a partir da carteira e escolha Preparar investimento. Na entrada, confirme visualmente a oportunidade, localização recebida e associação; continue um dossier existente ou crie um novo. Reveja e guarde no editor existente. A confirmação retira da URL a intenção de entrada já consumida, para que actualizar recupere o contexto guardado, incluindo associações retiradas, em vez de reaplicar essa entrada. Abra Plano e simulador no mesmo dossier; os dados e referências continuam disponíveis, com os controlos existentes para editar divergências e exportar. + +Ao retomar uma oportunidade já existente num dossier, acrescentam-se as associações recebidas sem substituir localização, corredor ou actividades guardadas. Uma localização recebida diferente é apresentada como divergência, mantendo a anterior para revisão explícita. Confirmar associações gera a revisão/auditoria do contexto existente; alterar apenas a referência documental não reabre etapas nem invalida cálculos se as entradas efectivas não mudarem. A alteração de localização, actividade ou outros pressupostos conserva os controlos de impacto existentes. + +No simulador, o componente comum mostra as oportunidades, localizações e associações do dossier. Os campos recuperados continuam a usar os resumos editáveis da entrega anterior; respostas deliberadas do simulador não são substituídas. Relatórios e CSV do plano conservam cada oportunidade, o respectivo território e as associações escolhidas, com origem identificável, sem inferir incentivos ou autorização de execução. + +Verificação do delta e regressão directamente afectada: `InvestmentPortfolioContinuityTest` e `InvestmentOpportunityChoicesTest`, seis testes/151 asserções em base QA isolada com transacções revertidas; incluem retoma após autenticação, selecção múltipla, associação partilhada entre fichas, preservação de título/localização/actividades, rejeição de origem não relacionada/privada, confirmação e recuperação no rascunho do simulador, e alteração só de referência sem invalidar cálculos. O teste de transporte dos 34 usa agora o mesmo agrupamento `selection_labels` do navegador, sem alargar os campos aceites pela API. Frontend: cinco testes territoriais/entrada e um teste de continuidade/CSV/impressão. Types e build aprovados. As provas dos incrementos anteriores mantêm-se; não se voltou a importar, publicar ou conferir os 20 artigos, 34 oportunidades, 408 limites ou 29 fichas na homologação. + +Navegador QA: fixture identificada como SYNTHETIC QA, com oportunidade, âncora/associação e dossier próprios, sem atribuir a dados reais as associações do teste. Conferidos os parâmetros dos dois botões públicos, a recepção na entrada, escolha do dossier existente, rascunho guardado, prévia com Sofala/associação, confirmação e recuperação no simulador com a mesma referência. Os registos reais QA não foram reclassificados nem associados artificialmente para esta prova visual. Captura local `outputs/preservation-inventory-20261023/portfolio-dossier-simulator-continuity-QA.png`. Nenhuma escrita remota ou publicação real. + +**Causa e prevenção da perda anterior:** mantém-se o diagnóstico documental de `e65cb269` e o manifesto nominal da reposição auditada. A mudança do importador para rascunhos deixou de reconciliar a divulgação anteriormente aprovada noutro ambiente; não se conhece a data exacta da perda num endereço. Nas próximas entregas, conservar a visibilidade/aprovação dos exemplares existentes e conferir os IDs/títulos/estados contra o inventário aceite, antes/depois do transporte; um transporte privado não revoga a divulgação anterior. Não restaurar a base inteira nem reimportar/publicar automaticamente conteúdos novos para corrigir visibilidade. Esta é prevenção técnica conforme a instrução de preservação da Tempo, não um novo aprovador. A presença visual não substitui o manifesto, a proveniência e o recibo de reposição existentes. + +Saldo: visibilidade das 29 fichas confirmada no destino na passagem anterior; continuidade implementada neste recorte QA para associações efectivamente cadastradas. Fichas sem oportunidades associadas continuam a indicar essa lacuna; não foram preenchidas com relações presumidas. Homologação inalterada nesta passagem; aplicação transversal permanece parcial. + ## Entrega QA reconciliada — preparação adaptativa, 23/10/2026 Conjunto funcional único, na ordem Git já existente: `7ca94967` → `f2150b19` → `5bda74ae`. A reconciliação documental `7d798579` conserva a evidência da carteira. Não fazer três instalações independentes, reaplicar patches ou substituir ficheiros por exemplares de cada candidata. Preservar estes commits e integrar o conjunto pelo processo Git de revisão existente. @@ -339,7 +361,7 @@ Incremento de apresentação: o dossier conserva as 19 etapas e o histórico, ma | Início | `PortalInvestidor` | Sectores, oportunidades, recursos publicados | `/`, APIs existentes de homepage | Visitante; contagens do cadastro | `PublicPortalNavigationTest`, navegador | | Catálogo / Educação | `investment/catalogue` | Filtros, educação, pivots, facetas | `/oportunidades`, `/educacao`; `/api/v1/investment/search` | Visitante; filtros em intersecção, montantes com moeda | `InvestmentUnificationTest` | | Cluster / Corredor / Âncora | `portfolio/index`, `portfolio/show` | Dossiers, relações, cadeias e oportunidades | `/clusters`, `/corredores`, `/projectos-ancora`, `/carteira/{id}` | Visitante; dossier publicado; cadeia pública | `InvestmentPortfolioTest`, testes territoriais | -| Continuidade carteira → oportunidade → dossier → simulador | Componentes de carteira, `investment/detail`, contexto existente | Origem documental e associações existentes, sem inferência | `from_dossier` na ficha de oportunidade; arranque do investimento existente | Mesmo percurso integrado; reposição não equivale a continuidade completa | Visibilidade: 29 IDs/títulos conferidos contra manifesto em 23/10. Lacuna concreta: `from_dossier` não acompanha a ligação Preparar investimento; continuidade privada por concluir | +| Continuidade carteira → oportunidade → dossier → simulador | Componentes de carteira, `investment/detail`, contexto existente | Origem documental por oportunidade no payload cifrado; associações existentes, sem inferência | `from_dossier` e território no arranque; rascunho, prévia, confirmação e simulador existentes | Mesmo percurso integrado; origem relacionada/publicada; respostas deliberadas conservadas | Visibilidade: 29 IDs/títulos conferidos anteriormente. Delta QA: seis testes/151 asserções API, cinco testes de entrada territorial, um de relatório; instalação na homologação pendente | | Ficha | `investment/detail` | Oportunidade, contexto, relações, fonte | `/oportunidades/{slug}`; `/api/v1/investment/opportunities/{slug}` | Visitante; rascunhos não acessíveis | `PublicPortalNavigationTest`, `InvestmentUnificationTest` | | Mapa | Componente `Map` em catálogo | `geo_locations`, `geo_geometries`, `opportunity_locations` | `/api/v1/investment/map` | Visitante; dois níveis de classificação pública | Teste PostGIS e estados vazios | | Comparar / exportar / sugerir | `investment/catalogue` | IDs publicados, dados de catálogo | `/compare`, `/export`, `/suggest` sob prefixo da API | Visitante; 4 comparações, 1.000 linhas/pontos; CSV protegido | Teste de publicação e fórmula CSV | diff --git a/resources/js/components/dossier-prefill.tsx b/resources/js/components/dossier-prefill.tsx index 38e28388..05594e3c 100644 --- a/resources/js/components/dossier-prefill.tsx +++ b/resources/js/components/dossier-prefill.tsx @@ -7,6 +7,7 @@ import { type ContextView, } from '@/lib/investor-context'; import { inheritCompatibleFields } from '@/lib/context-inheritance'; +import OpportunityChoices from './investment-opportunity-choices'; type Props = { contextId?: string | null; consumer: 'simulator' | 'land' | 'desk' | 'submission'; @@ -143,8 +144,10 @@ export function DossierPrefill(props: Props) {
)} {message &&{message}
} - {Object.values(view.context.selection).map( - (r) => + {consumer === 'simulator' && !!view.context.opportunity_choices?.length &&diff --git a/resources/js/components/investment-opportunity-choices.tsx b/resources/js/components/investment-opportunity-choices.tsx index 7085d1b5..c13df87e 100644 --- a/resources/js/components/investment-opportunity-choices.tsx +++ b/resources/js/components/investment-opportunity-choices.tsx @@ -34,6 +34,7 @@ function ChoiceCard({c,i,corridors,disabled,update,remove}:{c:OpportunityChoice; {!remove&&c.coverage&&
Abrangência: {c.coverage}
} {notice&&{notice}
}Localização: {c.territory?.title??'Por escolher'}
+ {!!c.portfolio_origins?.length &&Escolha o corredor e confirme onde pretende realizar o investimento.
+{onChange?'Escolha o corredor e confirme onde pretende realizar o investimento.':'Escolhas guardadas no dossier; cada oportunidade conserva a sua localização e associações.'}
{choices.map((c,i)=>Em preparação. Consulte a oportunidade e prepare o seu investimento. A execução depende das condições e autorizações aplicáveis ao projecto.
} Iniciar ou continuar investimento com esta oportunidade @@ -90,7 +90,7 @@ export default function Detail({ )}Confirme a localização de cada ficha na comparação do dossier.
Associação escolhida: {origin.title}
)}Confirme a localização de cada ficha na comparação do dossier. Ao continuar um dossier, as escolhas já guardadas conservam-se.
Ideia, oportunidade, sector e localização convergem no mesmo dossier. A referência é para preparação e não confirma direitos ou benefícios.
Confirme as oportunidades e a localização, complete os dados disponíveis e reveja antes de guardar.
+ {!!entryLocationDifferences.length && } {error && (