fix: resolve reconciliation conflicts preserving catalogue behaviour
This commit is contained in:
@@ -7620,7 +7620,6 @@ Cobertura: identificação/período, 27 campos manuais IVA e 24 campos manuais M
|
||||
Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência.
|
||||
|
||||
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
|
||||
@@ -9072,5 +9071,12 @@ Validação e diagnósticos:29 Feature/364 finais, com seis novos de reutilizaç
|
||||
Rastreabilidade dos executores: o recibo anterior organization_country_editor_increment_20261103 conservava na lista tooling_sha256 nomes de helpers da etapa workspace, por uma cópia de metadados. Mantêm-se esse recibo e todas as suas provas exactamente como estavam. Este incremento acrescenta prior_tooling_supplement_sha256 para os ficheiros efectivamente usados e retidos de check-organization-country-editor-qa, verify-organization-country-editor-concurrency e record-organization-country-editor, e liga os próprios executores actuais pelos caminhos correctos, incluindo este recorder. A suplementação de rastreabilidade não altera os resultados, fontes, observações de concorrência ou estado de instalação anteriores.
|
||||
|
||||
Preservação e próxima acção:79 tabelas de teste vazias, lock/identidade/owner/RLS/FORCE conferidos e fixture de falha revertida.89 recibos/411 provas anteriores preservados e16 novas provas arquivadas. Nenhuma migração/escrita activa ou comunicação externa. AUD-COUNTRY-02 e AUD-PROFILE-25 continuam parciais: clientes legados sem chave mantêm semântica anterior; restam outros consumidores/perfis, retoma durável/expiração, visual/móvel/browser, preenchimento real autorizado onde falta país, backup/restauro do candidato e disponibilização autenticada. A fonte aprovada não volta a ser submetida a uma validação genérica nem se inventam representantes. Próxima acção: confrontar os consumidores restantes e a recuperação de preparações com a matriz, reutilizando os mecanismos agora comuns.
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
|
||||
|
||||
### 04/11/2026 — resolução dos conflitos gravados na reconciliação
|
||||
|
||||
Pedido expresso da Tempo: resolver os conflitos de `codex/investemoz-reconciliation-20261029`. Base preservada em `backup/reconciliation-before-conflict-fix-20261104` (`4a881252`). Resolvidos oito blocos em quatro ficheiros, sem integrar na main ou instalar no portal.
|
||||
|
||||
O catálogo conserva o layout lateral/responsivo e os filtros expansíveis, a selecção validada com recuperação de falha do armazenamento, as acções de limpar nos corredores e na lista, os resultados relacionados distintos da pesquisa, a paginação bloqueada durante pesquisa/falha e o mapa único com camadas e limites administrativos pelo território aplicado. Não atribui localização distrital nem coordenadas a uma indicação provincial. Manual e matriz conservam os registos de ambos os lados; matriz com 3.555 requisitos únicos e 49 colunas. As provas aceites e pendências visuais mantêm-se; esta resolução não constitui aceitação visual ou disponibilização activa.
|
||||
|
||||
Verificação do delta: 25 testes frontend dirigidos aprovados; TypeScript e compilação QA aprovados; lint dos dois ficheiros React sem erros após normalizar o título do sector; pesquisa de marcadores e `git diff --check` limpos. Compilação apenas em `public/build-qa-opportunities`, com aviso de dimensão de chunks. Sem alterações de schema, base de dados, permissões, serviços ou outros worktrees. Recibos locais: `outputs/reconciliation-conflicts-20261104/`.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -231,122 +231,6 @@ export default function OpportunitySearchFilters({
|
||||
}}
|
||||
aria-label="Pesquisar oportunidades"
|
||||
>
|
||||
<<<<<<< HEAD
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Field label="O que procura">
|
||||
<input
|
||||
className={input}
|
||||
value={String(data.q)}
|
||||
placeholder="Palavras do título ou da descrição"
|
||||
onChange={(e) => change({ q: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
{select('perspective', [
|
||||
{ value: 'exists', label: 'O que existe' },
|
||||
{ value: 'needs', label: 'Necessidades identificadas' },
|
||||
{ value: 'ideas', label: 'Ideias de investimento' },
|
||||
{
|
||||
value: 'opportunities',
|
||||
label: 'Oportunidades cadastradas — disponibilidade na ficha',
|
||||
},
|
||||
])}
|
||||
{select('sector', choicesFor('sector'))}
|
||||
{select('category', choicesFor('category'))}
|
||||
</div>
|
||||
<fieldset className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<legend className="mb-2 font-semibold">Localização</legend>
|
||||
<p className="text-sm sm:col-span-2 lg:col-span-4">
|
||||
A província inclui as oportunidades dos seus distritos,
|
||||
postos administrativos e localidades. Não é necessário
|
||||
escolher todos os níveis. O contexto provincial de um
|
||||
corredor é identificado nos resultados; não confirma uma
|
||||
implantação distrital.
|
||||
{path.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 underline"
|
||||
onClick={() => territory(path.slice(0, 1))}
|
||||
>
|
||||
Ver toda a província de {path[0].name}
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
{(
|
||||
[
|
||||
'province_id',
|
||||
'district_id',
|
||||
'admin_post_id',
|
||||
'locality_id',
|
||||
] as const
|
||||
).map((key, index) => (
|
||||
<TerritoryLevel
|
||||
key={`${key}-${path[index - 1]?.id ?? ''}`}
|
||||
index={index}
|
||||
parent={path[index - 1]?.id ?? ''}
|
||||
selected={path[index]}
|
||||
endpoint="/api/v1/investment/territory-options"
|
||||
onChange={(value) =>
|
||||
territory([
|
||||
...path.slice(0, index),
|
||||
...(value ? [value] : []),
|
||||
])
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</fieldset>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Field label="Estado de preparação">
|
||||
<select
|
||||
className={input}
|
||||
value={String(data.preparation ?? '')}
|
||||
onChange={(e) =>
|
||||
change({
|
||||
preparation: e.target.value,
|
||||
maturity_level: '',
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Todos os estados</option>
|
||||
{[
|
||||
'Etapas de preparação',
|
||||
'Classificações históricas',
|
||||
].map((group) => (
|
||||
<optgroup key={group} label={group}>
|
||||
{(references.preparationOptions ?? [])
|
||||
.filter((o) => o.group === group)
|
||||
.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-sm text-slate-600">
|
||||
Os registos históricos são apresentados separadamente e
|
||||
conservam a classificação indicada na ficha.
|
||||
</p>
|
||||
</Field>
|
||||
{(['min_amount', 'max_amount'] as const).map((key) => (
|
||||
<Field key={key} label={labels[key]}>
|
||||
<input
|
||||
className={input}
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={String(data[key])}
|
||||
onChange={(e) => change({ [key]: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
))}
|
||||
{select(
|
||||
'currency',
|
||||
['MZN', 'USD', 'EUR', 'ZAR'].map((c) => ({
|
||||
value: c,
|
||||
label: c,
|
||||
})),
|
||||
'Seleccione uma moeda para comparar montantes. Não se efectuam conversões cambiais.',
|
||||
=======
|
||||
<button
|
||||
type="button"
|
||||
className="catalogue-filters-toggle"
|
||||
@@ -360,7 +244,6 @@ export default function OpportunitySearchFilters({
|
||||
<span className="catalogue-count">
|
||||
{active.length + path.length}
|
||||
</span>
|
||||
>>>>>>> origin/main
|
||||
)}
|
||||
<ChevronDown size={18} aria-hidden="true" />
|
||||
</button>
|
||||
@@ -401,7 +284,11 @@ export default function OpportunitySearchFilters({
|
||||
corredor é identificado nos resultados; não confirma uma
|
||||
implantação distrital.
|
||||
{path.length > 1 && (
|
||||
<button type="button" className="ml-2 underline" onClick={() => territory(path.slice(0, 1))}>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-2 underline"
|
||||
onClick={() => territory(path.slice(0, 1))}
|
||||
>
|
||||
Ver toda a província de {path[0].name}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -76,7 +76,6 @@ export default function Catalogue({
|
||||
const page = usePage<{ auth: { user: unknown } }>();
|
||||
const { auth } = page.props;
|
||||
const [investmentSelection, setInvestmentSelection] = useState<number[]>(
|
||||
<<<<<<< HEAD
|
||||
readOpportunitySelection,
|
||||
);
|
||||
const [selectionStorageUnavailable, setSelectionStorageUnavailable] =
|
||||
@@ -85,33 +84,11 @@ export default function Catalogue({
|
||||
setInvestmentSelection(next);
|
||||
setSelectionStorageUnavailable(!persistOpportunitySelection(next));
|
||||
}
|
||||
=======
|
||||
() => {
|
||||
try {
|
||||
return JSON.parse(
|
||||
sessionStorage.getItem(
|
||||
'investment-opportunity-selection',
|
||||
) ?? '[]',
|
||||
);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
);
|
||||
>>>>>>> origin/main
|
||||
function selectInvestment(id: number, checked: boolean) {
|
||||
const next = checked
|
||||
? [...new Set([...investmentSelection, id])]
|
||||
: investmentSelection.filter((x) => x !== id);
|
||||
<<<<<<< HEAD
|
||||
updateInvestmentSelection(next);
|
||||
=======
|
||||
setInvestmentSelection(next);
|
||||
sessionStorage.setItem(
|
||||
'investment-opportunity-selection',
|
||||
JSON.stringify(next),
|
||||
);
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
const form = useForm({
|
||||
perspective: String(filters.perspective ?? ''),
|
||||
@@ -420,208 +397,6 @@ export default function Catalogue({
|
||||
Combine os filtros para encontrar oportunidades publicadas.
|
||||
Nenhum critério é alargado automaticamente.
|
||||
</p>
|
||||
<<<<<<< HEAD
|
||||
<OpportunitySearchFilters
|
||||
data={form.data}
|
||||
references={references}
|
||||
path={territoryPath}
|
||||
change={(patch) => form.setData((d) => ({ ...d, ...patch }))}
|
||||
territory={changeTerritory}
|
||||
submit={() => runSearch()}
|
||||
errors={form.errors}
|
||||
clear={() => {
|
||||
setTerritoryPath([]);
|
||||
form.setData(
|
||||
(d) =>
|
||||
Object.fromEntries(
|
||||
Object.entries(d).map(([key, value]) => [
|
||||
key,
|
||||
key === 'view'
|
||||
? 'grid'
|
||||
: key === 'sort'
|
||||
? 'recent'
|
||||
: key === 'territory_basis'
|
||||
? 'all'
|
||||
: Array.isArray(value)
|
||||
? []
|
||||
: typeof value === 'boolean'
|
||||
? false
|
||||
: '',
|
||||
]),
|
||||
) as unknown as typeof d,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Oportunidades publicadas
|
||||
{filters.sector &&
|
||||
` · ${inputText(references.sectors.find((sector) => sector.slug === filters.sector)?.name ?? filters.sector)}`}
|
||||
{territorySelection.length > 0 &&
|
||||
` · ${territorySelection.map((territory) => territory.name).join(' / ')}`}
|
||||
</h2>
|
||||
<SearchState
|
||||
loading={searchLoading}
|
||||
error={searchError}
|
||||
total={results.total}
|
||||
retry={() => runSearch()}
|
||||
/>
|
||||
<OpportunityRelatedResults
|
||||
related={relatedSearch}
|
||||
loading={searchLoading}
|
||||
error={searchError}
|
||||
total={results.total}
|
||||
/>
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
results.total === 0 &&
|
||||
selectedStructures.length > 0 && (
|
||||
<section
|
||||
className={card}
|
||||
aria-label="Estrutura seleccionada"
|
||||
>
|
||||
<p className="font-semibold">
|
||||
Sem oportunidades publicadas associadas com estes
|
||||
filtros.
|
||||
</p>
|
||||
<p>
|
||||
A estrutura continua disponível para consulta. A sua
|
||||
selecção não confirma benefícios nem disponibilidade
|
||||
para investimento.
|
||||
</p>
|
||||
{selectedStructures.map((record) =>
|
||||
record.url ? (
|
||||
<Link
|
||||
key={record.id}
|
||||
className="block underline"
|
||||
href={searchTerritoryHref(
|
||||
record.url,
|
||||
page.url,
|
||||
)}
|
||||
>
|
||||
Consultar ficha: {record.name}
|
||||
</Link>
|
||||
) : (
|
||||
<p key={record.id}>
|
||||
{record.name} — sem ficha pública detalhada
|
||||
disponível.
|
||||
</p>
|
||||
),
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
results.total === 0 &&
|
||||
(form.data.province_id ||
|
||||
form.data.district_id ||
|
||||
form.data.admin_post_id ||
|
||||
form.data.locality_id) && (
|
||||
<button
|
||||
type="button"
|
||||
className="underline"
|
||||
onClick={() => {
|
||||
setTerritoryPath([]);
|
||||
form.setData((data) => ({
|
||||
...data,
|
||||
province_id: '',
|
||||
district_id: '',
|
||||
admin_post_id: '',
|
||||
locality_id: '',
|
||||
province: '',
|
||||
district: '',
|
||||
geo_location_id: '',
|
||||
}));
|
||||
}}
|
||||
>
|
||||
Pesquisar noutras províncias (remover localização)
|
||||
</button>
|
||||
)}
|
||||
{['grid', 'list', 'map', 'combined', 'corridors'].map(
|
||||
(value, i) => (
|
||||
<button
|
||||
key={value}
|
||||
disabled={searchLoading || !!searchError}
|
||||
aria-pressed={view === value}
|
||||
className="rounded border px-4 py-2"
|
||||
onClick={() => {
|
||||
const params = new URLSearchParams(query());
|
||||
params.set('view', value);
|
||||
router.get(
|
||||
`${catalogueUrl}?${params}`,
|
||||
{},
|
||||
{ preserveScroll: true },
|
||||
);
|
||||
}}
|
||||
>
|
||||
{
|
||||
[
|
||||
'Grelha',
|
||||
'Lista',
|
||||
'Mapa',
|
||||
'Lista e mapa',
|
||||
'Por corredor',
|
||||
][i]
|
||||
}
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
<a
|
||||
className="underline"
|
||||
aria-disabled={searchLoading || !!searchError}
|
||||
tabIndex={searchLoading || searchError ? -1 : undefined}
|
||||
onClick={(event) => {
|
||||
if (searchLoading || searchError)
|
||||
event.preventDefault();
|
||||
}}
|
||||
href={'/api/v1/investment/export?' + query()}
|
||||
>
|
||||
Exportar CSV (até 1.000)
|
||||
</a>
|
||||
<Link className="underline" href="/investidor/manifestacoes">
|
||||
Favoritos e pedidos
|
||||
</Link>
|
||||
</div>
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) && (
|
||||
<div role={error ? 'alert' : 'status'} aria-live="polite">
|
||||
{error ? (
|
||||
<>
|
||||
Não foi possível carregar as oportunidades no
|
||||
mapa. A lista continua disponível.{' '}
|
||||
<button
|
||||
className="underline"
|
||||
onClick={() =>
|
||||
setMapAttempt((value) => value + 1)
|
||||
}
|
||||
>
|
||||
Tentar novamente
|
||||
</button>
|
||||
</>
|
||||
) : points === null ? (
|
||||
'A carregar o mapa…'
|
||||
) : (
|
||||
`${points.length} ${points.length === 1 ? 'local' : 'locais'} no mapa; ${unlocated.length} ${unlocated.length === 1 ? 'oportunidade' : 'oportunidades'} sem coordenadas confirmadas.`
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) && (
|
||||
<Map
|
||||
points={error ? [] : points}
|
||||
areas={areas}
|
||||
territoryId={
|
||||
String(
|
||||
filters.locality_id ||
|
||||
filters.admin_post_id ||
|
||||
filters.district_id ||
|
||||
filters.province_id ||
|
||||
'',
|
||||
) || undefined
|
||||
}
|
||||
=======
|
||||
<div className="catalogue-workspace">
|
||||
<aside aria-label="Filtros do catálogo">
|
||||
<OpportunitySearchFilters
|
||||
@@ -660,7 +435,6 @@ export default function Catalogue({
|
||||
) as unknown as typeof d,
|
||||
);
|
||||
}}
|
||||
>>>>>>> origin/main
|
||||
/>
|
||||
</aside>
|
||||
<section
|
||||
@@ -672,138 +446,11 @@ export default function Catalogue({
|
||||
<header className="catalogue-results-heading">
|
||||
<h2>
|
||||
Oportunidades publicadas
|
||||
{filters.sector && ` · ${references.sectors.find((sector) => sector.slug === filters.sector)?.name ?? filters.sector}`}
|
||||
{territorySelection.length > 0 && ` · ${territorySelection.map((territory) => territory.name).join(' / ')}`}
|
||||
{filters.sector &&
|
||||
` · ${inputText(references.sectors.find((sector) => sector.slug === filters.sector)?.name ?? filters.sector)}`}
|
||||
{territorySelection.length > 0 &&
|
||||
` · ${territorySelection.map((territory) => territory.name).join(' / ')}`}
|
||||
</h2>
|
||||
<<<<<<< HEAD
|
||||
<p>
|
||||
Estas fichas continuam consultáveis
|
||||
independentemente de terem coordenadas. A lista não
|
||||
atribui implantação às oportunidades nem cria pontos
|
||||
no mapa. Até 30 fichas por conjunto; refine a
|
||||
localização para pesquisar.
|
||||
</p>
|
||||
<ul>
|
||||
{mapContext.map((record) => (
|
||||
<li key={record.id}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={record.url}
|
||||
>
|
||||
{record.kind}: {record.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) &&
|
||||
truncated && (
|
||||
<p role="alert">
|
||||
O mapa atingiu o limite de 1.000 registos; refine os
|
||||
filtros. A cobertura não está completa.
|
||||
</p>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
view === 'map' &&
|
||||
unlocated.length > 0 && (
|
||||
<section
|
||||
className={card}
|
||||
aria-label="Investimentos a georreferenciar"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
{unlocated.length} investimentos sem coordenadas de
|
||||
local confirmadas
|
||||
</h2>
|
||||
<p>
|
||||
Continuam nos resultados. Seleccionar uma província
|
||||
não determina a posição exacta do investimento.
|
||||
</p>
|
||||
<ul>
|
||||
{unlocated.map((record) => (
|
||||
<li key={record.id}>
|
||||
<Link
|
||||
href={record.url}
|
||||
className="underline"
|
||||
>
|
||||
{record.title}
|
||||
</Link>{' '}
|
||||
—{' '}
|
||||
{record.location ||
|
||||
'Localização por completar'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
{view === 'corridors' && !searchLoading && !searchError && (
|
||||
<OpportunityCorridorGroups
|
||||
groups={corridorGroups}
|
||||
selected={investmentSelection}
|
||||
onSelect={selectInvestment}
|
||||
onClear={() => {
|
||||
updateInvestmentSelection([]);
|
||||
}}
|
||||
onCompare={(ids) =>
|
||||
void api(
|
||||
'/api/v1/investment/compare?' +
|
||||
ids.map((id) => 'ids[]=' + id).join('&'),
|
||||
)
|
||||
.then((data) => setComparison(data.records))
|
||||
.catch((e) => setError(e.message))
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{view !== 'corridors' &&
|
||||
!searchLoading &&
|
||||
!searchError &&
|
||||
(view !== 'map' || error || points?.length === 0) && (
|
||||
<div
|
||||
className={
|
||||
view === 'grid'
|
||||
? 'grid gap-5 md:grid-cols-2 xl:grid-cols-3'
|
||||
: 'space-y-4'
|
||||
}
|
||||
>
|
||||
{results.data.map((r) => (
|
||||
<article key={r.id} className={card + ' space-y-3'}>
|
||||
<p className="text-sm">
|
||||
{r.sector?.name} ·{' '}
|
||||
{r.territorial_summary ||
|
||||
[r.province, r.district]
|
||||
.filter(Boolean)
|
||||
.join(' / ') ||
|
||||
'Localização não indicada'}
|
||||
</p>
|
||||
<h2 className="text-xl font-bold">
|
||||
<Link
|
||||
href={`/oportunidades/${r.slug}${query() ? `?${query()}` : ''}`}
|
||||
>
|
||||
{r.title}
|
||||
</Link>
|
||||
</h2>
|
||||
<p>{r.summary}</p>
|
||||
<p className="font-semibold">{money(r)}</p>
|
||||
<p>
|
||||
{r.proactive
|
||||
? r.proactive.group === 'ideas'
|
||||
? 'Ideia em preparação'
|
||||
: r.proactive.group === 'needs'
|
||||
? 'Necessidade identificada'
|
||||
: 'O que existe'
|
||||
: r.maturity_label}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/oportunidades/${r.slug}${query() ? `?${query()}` : ''}`}
|
||||
>
|
||||
Ver ficha
|
||||
</Link>
|
||||
=======
|
||||
<SearchState
|
||||
loading={searchLoading}
|
||||
error={searchError}
|
||||
@@ -811,6 +458,12 @@ export default function Catalogue({
|
||||
retry={() => runSearch()}
|
||||
/>
|
||||
</header>
|
||||
<OpportunityRelatedResults
|
||||
related={relatedSearch}
|
||||
loading={searchLoading}
|
||||
error={searchError}
|
||||
total={results.total}
|
||||
/>
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
results.total === 0 &&
|
||||
@@ -875,7 +528,8 @@ export default function Catalogue({
|
||||
}));
|
||||
}}
|
||||
>
|
||||
Pesquisar noutras províncias (remover localização)
|
||||
Pesquisar noutras províncias (remover
|
||||
localização)
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
@@ -898,7 +552,6 @@ export default function Catalogue({
|
||||
Route,
|
||||
][i];
|
||||
return (
|
||||
>>>>>>> origin/main
|
||||
<button
|
||||
key={value}
|
||||
disabled={
|
||||
@@ -929,187 +582,6 @@ export default function Catalogue({
|
||||
][i]
|
||||
}
|
||||
</button>
|
||||
<<<<<<< HEAD
|
||||
<label className="flex gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={investmentSelection.includes(
|
||||
r.id,
|
||||
)}
|
||||
onChange={(e) =>
|
||||
selectInvestment(
|
||||
r.id,
|
||||
e.target.checked,
|
||||
)
|
||||
}
|
||||
/>
|
||||
Escolher para preparar investimento
|
||||
</label>
|
||||
<label className="flex gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.includes(r.id)}
|
||||
disabled={
|
||||
!selected.includes(r.id) &&
|
||||
selected.length >= 4
|
||||
}
|
||||
onChange={(e) =>
|
||||
setSelected(
|
||||
e.target.checked
|
||||
? [...selected, r.id]
|
||||
: selected.filter(
|
||||
(id) =>
|
||||
id !== r.id,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
Comparar
|
||||
</label>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{!searchLoading && !searchError && !results.total && (
|
||||
<p className={card}>
|
||||
Não há oportunidades publicadas com estes filtros no
|
||||
cadastro consultado. A informação disponível pode ser
|
||||
incompleta. Pode{' '}
|
||||
<Link href="/apoio" className="underline">
|
||||
peça apoio
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
<nav
|
||||
aria-label="Paginação"
|
||||
className={view === 'corridors' ? 'hidden' : 'flex gap-6'}
|
||||
>
|
||||
{!searchLoading && !searchError && results.prev_page_url && (
|
||||
<Link href={results.prev_page_url}>Anterior</Link>
|
||||
)}
|
||||
{!searchLoading && !searchError && results.next_page_url && (
|
||||
<Link href={results.next_page_url}>Seguinte</Link>
|
||||
)}
|
||||
</nav>
|
||||
{selected.length > 0 && (
|
||||
<button
|
||||
className={button}
|
||||
onClick={() =>
|
||||
api(
|
||||
'/api/v1/investment/compare?' +
|
||||
selected.map((id) => 'ids[]=' + id).join('&'),
|
||||
)
|
||||
.then((data) => setComparison(data.records))
|
||||
.catch((e) => setError(e.message))
|
||||
}
|
||||
>
|
||||
Comparar {selected.length} oportunidades
|
||||
</button>
|
||||
)}
|
||||
{selectionStorageUnavailable && (
|
||||
<p
|
||||
role="status"
|
||||
className="my-3 rounded border border-amber-300 p-3 text-sm"
|
||||
>
|
||||
O navegador não permitiu guardar a escolha. A selecção
|
||||
actual continua disponível nesta página; ao sair ou
|
||||
actualizar, pode recuperar uma escolha anterior ou perdê-la.
|
||||
</p>
|
||||
)}
|
||||
{view !== 'corridors' && !!investmentSelection.length && (
|
||||
<section className={card}>
|
||||
<p>
|
||||
{investmentSelection.length} oportunidades escolhidas,
|
||||
{selectionStorageUnavailable
|
||||
? ' disponíveis nesta página.'
|
||||
: ' conservadas ao mudar de página.'}
|
||||
</p>
|
||||
<Link
|
||||
className={button}
|
||||
href={
|
||||
'/investimentos/iniciar?' +
|
||||
investmentSelection
|
||||
.map((id) => 'opportunities[]=' + id)
|
||||
.join('&')
|
||||
}
|
||||
>
|
||||
Preparar investimento com as oportunidades escolhidas
|
||||
</Link>
|
||||
<button
|
||||
className="ml-4 underline"
|
||||
onClick={() => {
|
||||
updateInvestmentSelection([]);
|
||||
}}
|
||||
>
|
||||
Limpar escolha
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
{comparison.length > 0 && (
|
||||
<div ref={comparisonRef} className="overflow-x-auto">
|
||||
<table className="w-full border-collapse text-left">
|
||||
<caption className="p-3 text-left font-bold">
|
||||
Comparação — montantes nas moedas de origem
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="border p-3">Campo</th>
|
||||
{comparison.map(({ record: r }) => (
|
||||
<th key={r.id} className="border p-3">
|
||||
{r.title}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
'Sector',
|
||||
'Província',
|
||||
'Investimento',
|
||||
'Maturidade',
|
||||
'Parceria',
|
||||
'Impacto',
|
||||
].map((field, i) => (
|
||||
<tr key={field}>
|
||||
<th className="border p-3">{field}</th>
|
||||
{comparison.map(({ record: r }) => (
|
||||
<td className="border p-3" key={r.id}>
|
||||
{[
|
||||
r.sector?.name,
|
||||
r.province,
|
||||
money(r),
|
||||
r.maturity_label,
|
||||
r.financing_modality,
|
||||
r.expected_impact,
|
||||
][i] || 'Não indicado'}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
{territorial.length > 0 && (
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-2xl font-bold">
|
||||
Contexto territorial e projectos âncora
|
||||
</h2>
|
||||
<p>
|
||||
Fichas do documento de referência; montantes e metas
|
||||
conservam a natureza declarada na fonte.
|
||||
</p>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{territorial.map((d) => (
|
||||
<Link
|
||||
href={searchTerritoryHref(
|
||||
`/carteira/${d.id}`,
|
||||
page.url,
|
||||
)}
|
||||
key={d.id}
|
||||
=======
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -1169,19 +641,20 @@ export default function Catalogue({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) &&
|
||||
!error && <Map points={points} areas={areas} />}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
['map', 'combined'].includes(view) && (
|
||||
<TerritoryGeometryPreview
|
||||
<Map
|
||||
points={error ? [] : points}
|
||||
areas={areas}
|
||||
territoryId={
|
||||
form.data.locality_id ||
|
||||
form.data.admin_post_id ||
|
||||
form.data.district_id ||
|
||||
form.data.province_id
|
||||
String(
|
||||
filters.locality_id ||
|
||||
filters.admin_post_id ||
|
||||
filters.district_id ||
|
||||
filters.province_id ||
|
||||
'',
|
||||
) || undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -1191,7 +664,6 @@ export default function Catalogue({
|
||||
!error &&
|
||||
mapContext.length > 0 && (
|
||||
<section
|
||||
>>>>>>> origin/main
|
||||
className={card}
|
||||
aria-label="Estruturas territoriais consultáveis"
|
||||
>
|
||||
@@ -1270,10 +742,7 @@ export default function Catalogue({
|
||||
selected={investmentSelection}
|
||||
onSelect={selectInvestment}
|
||||
onClear={() => {
|
||||
setInvestmentSelection([]);
|
||||
sessionStorage.removeItem(
|
||||
'investment-opportunity-selection',
|
||||
);
|
||||
updateInvestmentSelection([]);
|
||||
}}
|
||||
onCompare={(ids) =>
|
||||
void api(
|
||||
@@ -1313,7 +782,10 @@ export default function Catalogue({
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>
|
||||
{r.territorial_summary || [r.province, r.district].filter(Boolean).join(' / ') ||
|
||||
{r.territorial_summary ||
|
||||
[r.province, r.district]
|
||||
.filter(Boolean)
|
||||
.join(' / ') ||
|
||||
'Localização não indicada'}
|
||||
</span>
|
||||
</p>
|
||||
@@ -1435,8 +907,9 @@ export default function Catalogue({
|
||||
)}
|
||||
{!searchLoading && !searchError && !results.total && (
|
||||
<p className={card}>
|
||||
Não há oportunidades publicadas com estes filtros no cadastro consultado.
|
||||
A informação disponível pode ser incompleta. Pode{' '}
|
||||
Não há oportunidades publicadas com estes filtros no
|
||||
cadastro consultado. A informação disponível pode
|
||||
ser incompleta. Pode{' '}
|
||||
<Link href="/apoio" className="underline">
|
||||
pedir apoio
|
||||
</Link>
|
||||
@@ -1451,12 +924,20 @@ export default function Catalogue({
|
||||
: 'catalogue-pagination'
|
||||
}
|
||||
>
|
||||
{results.prev_page_url && (
|
||||
<Link href={results.prev_page_url}>Anterior</Link>
|
||||
)}
|
||||
{results.next_page_url && (
|
||||
<Link href={results.next_page_url}>Seguinte</Link>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
results.prev_page_url && (
|
||||
<Link href={results.prev_page_url}>
|
||||
Anterior
|
||||
</Link>
|
||||
)}
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
results.next_page_url && (
|
||||
<Link href={results.next_page_url}>
|
||||
Seguinte
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
{selected.length > 0 && (
|
||||
<button
|
||||
@@ -1475,11 +956,25 @@ export default function Catalogue({
|
||||
Comparar {selected.length} oportunidades
|
||||
</button>
|
||||
)}
|
||||
{selectionStorageUnavailable && (
|
||||
<p
|
||||
role="status"
|
||||
className="my-3 rounded border border-amber-300 p-3 text-sm"
|
||||
>
|
||||
O navegador não permitiu guardar a escolha. A
|
||||
selecção actual continua disponível nesta página; ao
|
||||
sair ou actualizar, pode recuperar uma escolha
|
||||
anterior ou perdê-la.
|
||||
</p>
|
||||
)}
|
||||
{view !== 'corridors' && !!investmentSelection.length && (
|
||||
<section className={card}>
|
||||
<p>
|
||||
{investmentSelection.length} oportunidades
|
||||
escolhidas, conservadas ao mudar de página.
|
||||
escolhidas,
|
||||
{selectionStorageUnavailable
|
||||
? ' disponíveis nesta página.'
|
||||
: ' conservadas ao mudar de página.'}
|
||||
</p>
|
||||
<Link
|
||||
className={button}
|
||||
@@ -1496,10 +991,7 @@ export default function Catalogue({
|
||||
<button
|
||||
className="ml-4 underline"
|
||||
onClick={() => {
|
||||
setInvestmentSelection([]);
|
||||
sessionStorage.removeItem(
|
||||
'investment-opportunity-selection',
|
||||
);
|
||||
updateInvestmentSelection([]);
|
||||
}}
|
||||
>
|
||||
Limpar escolha
|
||||
|
||||
Reference in New Issue
Block a user