fix: resolve reconciliation conflicts preserving catalogue behaviour

This commit is contained in:
2026-11-04 09:07:23 +02:00
parent 4a881252fc
commit f17e807ce1
4 changed files with 76 additions and 695 deletions
@@ -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>
)}
+60 -568
View File
@@ -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