fix: preserve territorial search across investment catalogues
This commit is contained in:
@@ -2151,3 +2151,30 @@ Autorização expressa da Tempo executada. Main passou de `57fc9148` por avanço
|
||||
**Estado fiscal preservado:** nenhuma publicação/activação de nova regra e nenhuma operação na base activa. O consumidor IVA geral previamente autorizado manteve a mesma disponibilidade, cobertura2026 e mensagem antes/depois. As exclusões sectoriais, forma indirecta/dimensão monetária não determinada, benefícios territoriais e apuramento fiscal completo continuam explícitos e não bloqueiam o lote procedimental concluído.
|
||||
|
||||
**Suspensão/recuperação:** desligar somente a chave do novo percurso e reconstruir cache/reiniciar graciosamente os workers mantém dossiers, documentos, respostas, versões, relatórios e históricos. Para reverter configuração, usar o valor anterior apenas depois de conferir o hash actual e preservar alterações posteriores; não substituir todo o .env sem conferir concorrência. A reversão de código/build segue o procedimento de releases já existente, sem restaurar dados antigos. A release anterior foi efectivamente conservada na primeira tentativa recusada. Recibo completo local em `outputs/investmoz-adaptive-path-delivery-20261022/delivery-receipt.json`; a presente actualização documental fica na branch de registo, sem novo push main ou nova disponibilização desnecessária.
|
||||
|
||||
|
||||
## Revis?o das 30 fichas de Inhambane e cat?logo vazio ? 22/10/2026
|
||||
|
||||
Leitura remota autenticada, transac??o READ ONLY, base teste_investidor: 30 oportunidades em draft; zero publicadas em toda a base. As consultas independentes de Cabo Delgado e Inhambane e a consulta exacta Inhambane/Vilankulo devolveram zero sem falha de consulta. A alega??o de oportunidades publicadas de Cabo Delgado n?o se confirma neste destino; n?o inventar associa??es para preencher a lista.
|
||||
|
||||
As 30 localiza??es permanecem nos recursos privados opportunity_location_drafts, referidos no cadastro can?nico; os campos e v?nculos p?blicos ainda n?o s?o a projec??o dessas fichas. A inexist?ncia desses v?nculos p?blicos em rascunhos n?o prova perda da localiza??o. Na publica??o, projectar somente a abrang?ncia documentada e conferir a pesquisa territorial.
|
||||
|
||||
Decis?o Tempo nesta conversa: incompletude e autoriza??es necess?rias ? execu??o n?o s?o impedimentos autom?ticos ? divulga??o como oportunidade em prepara??o. Proposta: divulgar resumos das 30 fichas em prepara??o com fonte, edi??o, data-base e lacunas vis?veis; n?o declarar financiamento, terreno, licen?as, promotor autorizado ou execu??o confirmados. Valores hist?ricos, agregados e divergentes devem ser identificados sem somas indevidas. Nenhum impedimento concreto de divulga??o do resumo foi demonstrado nesta leitura. Isto n?o declara o conte?do privado integral public?vel: originais, contactos e evid?ncias continuam separados. N?o houve publica??o, altera??o de pap?is ou escrita remota.
|
||||
|
||||
Prepara??o por ficha: outputs/investmoz-territory-recovery-20261022/publication-review.json (30 identidades reais, localiza??o privada, lacunas e valores). Evid?ncia: read-catalogue-remote.py e remote-read.log no mesmo direct?rio. A tabela hist?rica de lacunas de prepara??o ? reutilizada, sem a converter em bloqueios legais. A prepara??o t?cnica do fluxo de publica??o ainda deve reconciliar essas condi??es com o contrato existente; n?o marcar publica??o como pronta ou executada.
|
||||
|
||||
O URL recebido ? do cat?logo/iniciar por territ?rio, n?o do dossier afectado. N?o permite ler o contexto inicial, a altera??o e a ?ltima ac??o desse processo. A falha da pr?via territorial e do t?rmino sem resposta continua aberta e ainda n?o foi reproduzida no dossier real; n?o encerrar pela disponibiliza??o anterior ou por esta leitura de cat?logo.
|
||||
<!-- search-territory-recovery-20261022 -->
|
||||
## Continuidade territorial da pesquisa — correcção em QA, 22/10/2026
|
||||
|
||||
Esclarecimento da Tempo: o percurso reportado foi navegação pública entre carteira e catálogo, sem clicar em iniciar/continuar ideia. Não existe evidência de alteração de um dossier pessoal neste caso. Retira-se a exigência de URL do dossier e de confirmação de impacto; a alteração é de filtros de pesquisa.
|
||||
|
||||
Causa verificada: ligações da carteira para oportunidades e zonas e ligações dos menus públicos usavam destinos sem província/distrito. O contexto transportado para detalhes por `catalogue_query` também não acompanhava as ligações cruzadas. No ecrã de zonas, os parâmetros de exportação/detalhe omitiam a hierarquia territorial.
|
||||
|
||||
Correcção: `searchTerritoryHref` transporta exclusivamente os quatro níveis territoriais na URL entre carteira, oportunidades, zonas e educação, incluindo os respectivos detalhes. Reutiliza o `catalogue_query` existente e preserva os filtros próprios do destino. Uma selecção explícita de outro ascendente não herda descendentes incompatíveis. Sem armazenamento global, mistura de contas ou alteração de dossiers. Menus, navegação relacionada, breadcrumbs e ligações cruzadas usam a mesma função. O selector partilhado mantém a limpeza dos descendentes; não há confirmação adicional para pesquisar.
|
||||
|
||||
Verificação dirigida: seis testes Node (incluindo o selector partilhado), TypeScript e build isolado. No navegador QA: carteira Cabo Delgado → selecção Inhambane/Vilankulo → pesquisar → catálogo com ambos os campos preenchidos; recarregamento e regresso preservam a selecção; zonas herdam os dois níveis; mudar para Cabo Delgado limpa Vilankulo. Contagem zero, filtros activos e mensagem de ausência coerentes na base QA consultada. Não foram repetidos cálculos ou exportadores fiscais nem publicados registos para produzir resultados artificiais.
|
||||
|
||||
Provas: `outputs/investmoz-territory-recovery-20261022/before-carteira.png` e `after-catalogue.png` no workspace de entrega; testes em `tests/frontend/search-territory.test.mjs` e `territory-path.test.mjs`; logs QA `outputs/territory-types.log` e `territory-build.log`. O router privado QA selecciona o build isolado; não integra a candidata.
|
||||
|
||||
Entrega limitada a branch/QA. A homologação não foi alterada por esta correcção. Publicação das 30 fichas de Inhambane permanece separada: revisão por ficha preparada, nenhuma publicação executada; ainda por reconciliar a preparação técnica da divulgação em preparação com o fluxo existente. A correcção da pesquisa não depende dessa publicação.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,4 +1,5 @@
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
export default function CatalogueNavigation({
|
||||
dimensionUrl,
|
||||
}: {
|
||||
@@ -21,7 +22,7 @@ export default function CatalogueNavigation({
|
||||
)
|
||||
return dimensionUrl(u.searchParams.get('dimension')!);
|
||||
if (context) u.searchParams.set('context', context);
|
||||
return u.pathname + u.search;
|
||||
return searchTerritoryHref(u.pathname + u.search, url);
|
||||
};
|
||||
const sections = {
|
||||
'Enquadramento e filtros': [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import PortalProductIdentity from '@/components/portal-product-identity';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
import {
|
||||
ChevronDown,
|
||||
@@ -110,7 +111,7 @@ export function PortalGroupedMenu() {
|
||||
className="tempo-menu-item"
|
||||
>
|
||||
<Link
|
||||
href={item.href!}
|
||||
href={searchTerritoryHref(item.href!, url)}
|
||||
aria-current={
|
||||
currentItem === item.id ? 'page' : undefined
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import SearchFeedback from '@/components/search-feedback';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { PortalGroupedMenu } from '@/components/portal-grouped-menu';
|
||||
@@ -148,7 +149,7 @@ export default function PortalNavigation({
|
||||
{parent && (
|
||||
<>
|
||||
<Link
|
||||
href={parent}
|
||||
href={searchTerritoryHref(parent, url)}
|
||||
className="underline"
|
||||
>
|
||||
{names[parent]}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { searchTerritoryHref } from './search-territory';
|
||||
/** Only a query string is carried; the return destination is always local. */
|
||||
export function catalogueDetailHref(path: string, query: string): string {
|
||||
return query
|
||||
@@ -8,5 +9,5 @@ export function catalogueReturnHref(base: string, pageUrl: string): string {
|
||||
const query = new URL(pageUrl, 'https://portal.invalid').searchParams.get(
|
||||
'catalogue_query',
|
||||
);
|
||||
return base + (query ? `?${new URLSearchParams(query).toString()}` : '');
|
||||
return searchTerritoryHref(base + (query ? `?${new URLSearchParams(query).toString()}` : ''), pageUrl);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const levels = ['province_id', 'district_id', 'admin_post_id', 'locality_id'];
|
||||
const searchRoutes = ['/carteira', '/oportunidades', '/zonas', '/educacao'];
|
||||
|
||||
/** URL-scoped search context, never a saved investment or another browser tab. */
|
||||
export function searchTerritoryHref(href: string, pageUrl: string): string {
|
||||
const base = 'https://portal.invalid';
|
||||
const source = new URL(pageUrl, base);
|
||||
const target = new URL(href, base);
|
||||
const supported = (path: string) => searchRoutes.some((route) => path === route || path.startsWith(`${route}/`));
|
||||
if (target.origin !== base || !supported(source.pathname) || !supported(target.pathname)) return href;
|
||||
const inherited = source.searchParams.has('catalogue_query')
|
||||
? new URLSearchParams(source.searchParams.get('catalogue_query')!)
|
||||
: source.searchParams;
|
||||
// An explicit ancestor, including an explicit empty value, overrides its descendants.
|
||||
let changed = false;
|
||||
for (const field of levels) {
|
||||
if (target.searchParams.has(field)) {
|
||||
changed ||= target.searchParams.get(field) !== inherited.get(field);
|
||||
} else if (!changed && inherited.get(field)) {
|
||||
target.searchParams.set(field, inherited.get(field)!);
|
||||
}
|
||||
}
|
||||
return target.pathname + target.search + target.hash;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import EducationalGuides from '@/components/educational-guides';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import type { EducationalArticle } from '@/components/educational-article-card';
|
||||
import OpportunitySearchFilters from '@/components/opportunity-search-filters';
|
||||
import SearchState from '@/components/search-state';
|
||||
@@ -46,7 +47,8 @@ export default function Catalogue({
|
||||
corridor_id: string | null;
|
||||
}[];
|
||||
}) {
|
||||
const { auth } = usePage<{ auth: { user: unknown } }>().props;
|
||||
const page = usePage<{ auth: { user: unknown } }>();
|
||||
const { auth } = page.props;
|
||||
const form = useForm({
|
||||
association_type: String(filters.association_type ?? ''),
|
||||
association_target: String(filters.association_target ?? ''),
|
||||
@@ -696,7 +698,7 @@ export default function Catalogue({
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{territorial.map((d) => (
|
||||
<Link
|
||||
href={`/carteira/${d.id}`}
|
||||
href={searchTerritoryHref(`/carteira/${d.id}`, page.url)}
|
||||
key={d.id}
|
||||
className={card}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import EducationalGuides from '@/components/educational-guides';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import type { EducationalArticle } from '@/components/educational-article-card';
|
||||
import { ClassificationSummary } from '@/components/opportunity-classification';
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
@@ -45,7 +46,7 @@ export default function Detail({
|
||||
<p>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/carteira/${dossierOrigin.id}`}
|
||||
href={searchTerritoryHref(`/carteira/${dossierOrigin.id}`, page.url)}
|
||||
>
|
||||
Voltar ao dossier: {dossierOrigin.title}
|
||||
</Link>
|
||||
@@ -253,7 +254,7 @@ export default function Detail({
|
||||
<span key={dossier.id}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/carteira/${dossier.id}`}
|
||||
href={searchTerritoryHref(`/carteira/${dossier.id}`, page.url)}
|
||||
>
|
||||
Consultar dossier: {dossier.title}
|
||||
</Link>
|
||||
|
||||
@@ -5,6 +5,7 @@ import ParkDetailsEditor, {
|
||||
} from '@/components/investment-zone-operational-details';
|
||||
import { Link, router, useForm, usePage } from '@inertiajs/react';
|
||||
import { catalogueReturnHref } from '@/lib/catalogue-context';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import InvestmentTerritory from '@/components/investment-territory';
|
||||
import InvestmentMapPanel from '@/components/investment-map-panel';
|
||||
import {
|
||||
@@ -1772,7 +1773,7 @@ function Editor({
|
||||
<p key={o.id}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/oportunidades/${o.slug}?zone_id=${z.id}`}
|
||||
href={searchTerritoryHref(`/oportunidades/${o.slug}?zone_id=${z.id}`, pageUrl)}
|
||||
>
|
||||
{o.title}
|
||||
</Link>
|
||||
@@ -1829,7 +1830,7 @@ function Editor({
|
||||
<p key={r.id}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/carteira/${r.id}`}
|
||||
href={searchTerritoryHref(`/carteira/${r.id}`, pageUrl)}
|
||||
>
|
||||
{r.title}
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import InvestmentZoneInstallation, {
|
||||
type ParkInstallationPlan,
|
||||
} from '@/components/investment-zone-installation';
|
||||
import { Link, router } from '@inertiajs/react';
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import { useState } from 'react';
|
||||
import { catalogueDetailHref } from '@/lib/catalogue-context';
|
||||
import {
|
||||
@@ -90,9 +91,13 @@ export default function Zones({
|
||||
zoneExtensionsAvailable?: boolean;
|
||||
territorySelection?: TerritoryOption[];
|
||||
}) {
|
||||
const pageUrl = usePage().url;
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const exportParams = new URLSearchParams();
|
||||
['province_id', 'district_id', 'admin_post_id', 'locality_id'].forEach((field, index) => {
|
||||
if (territorySelection[index]) exportParams.set(field, territorySelection[index].id);
|
||||
});
|
||||
for (const key of [
|
||||
'q',
|
||||
'category',
|
||||
@@ -148,9 +153,9 @@ export default function Zones({
|
||||
>
|
||||
<Link href="/clusters">Por Cluster</Link>
|
||||
<Link href="/corredores">Por Corredor</Link>
|
||||
<Link href="/zonas?category=zee">Por ZEE</Link>
|
||||
<Link href="/zonas?category=zfi">Por ZFI</Link>
|
||||
<Link href="/zonas?category=industrial_park">
|
||||
<Link href={searchTerritoryHref('/zonas?category=zee', pageUrl)}>Por ZEE</Link>
|
||||
<Link href={searchTerritoryHref('/zonas?category=zfi', pageUrl)}>Por ZFI</Link>
|
||||
<Link href={searchTerritoryHref('/zonas?category=industrial_park', pageUrl)}>
|
||||
Parques industriais
|
||||
</Link>
|
||||
</nav>
|
||||
@@ -343,7 +348,7 @@ export default function Zones({
|
||||
{!management && (
|
||||
<Link
|
||||
className="ml-4 underline"
|
||||
href={`/oportunidades?${new URLSearchParams({ [zone.category === 'zee' ? 'zee_id' : zone.category === 'zfi' ? 'zfi_id' : 'park_id']: zone.id })}`}
|
||||
href={searchTerritoryHref(`/oportunidades?${new URLSearchParams({ [zone.category === 'zee' ? 'zee_id' : zone.category === 'zfi' ? 'zfi_id' : 'park_id']: zone.id })}`, pageUrl)}
|
||||
>
|
||||
Ver oportunidades associadas
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link, useForm } from '@inertiajs/react';
|
||||
import { Link, useForm, usePage } from '@inertiajs/react';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import { catalogueDetailHref } from '@/lib/catalogue-context';
|
||||
import { Management } from '../management/shared';
|
||||
import type { Paginated } from '../management/shared';
|
||||
@@ -35,6 +36,7 @@ export default function Index({
|
||||
locality_id: filters.locality_id ?? '',
|
||||
});
|
||||
const base = management ? '/management/portfolio' : '/carteira';
|
||||
const { url } = usePage();
|
||||
const Frame = management ? Management : Layout;
|
||||
return (
|
||||
<Frame title="Corredores, clusters e projectos âncora">
|
||||
@@ -151,7 +153,7 @@ export default function Index({
|
||||
(r.corridor_id || r.cluster_id || r.project_id) && (
|
||||
<Link
|
||||
className="ml-4 underline"
|
||||
href={`/oportunidades?${new URLSearchParams(r.corridor_id ? { corridor_id: r.corridor_id } : r.cluster_id ? { cluster_id: r.cluster_id } : { project_id: r.project_id! })}`}
|
||||
href={searchTerritoryHref(`/oportunidades?${new URLSearchParams(r.corridor_id ? { corridor_id: r.corridor_id } : r.cluster_id ? { cluster_id: r.cluster_id } : { project_id: r.project_id! })}`, url)}
|
||||
>
|
||||
Ver oportunidades associadas
|
||||
</Link>
|
||||
@@ -174,10 +176,10 @@ export default function Index({
|
||||
aria-label="Alternativas de exploração"
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
<Link className="underline" href="/oportunidades">
|
||||
<Link className="underline" href={searchTerritoryHref('/oportunidades', url)}>
|
||||
Ver oportunidades publicadas
|
||||
</Link>
|
||||
<Link className="underline" href="/zonas">
|
||||
<Link className="underline" href={searchTerritoryHref('/zonas', url)}>
|
||||
Consultar ZEE, ZFI e parques
|
||||
</Link>
|
||||
<Link className="underline" href="/Simulador">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link, useForm, router, usePage } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import { catalogueReturnHref } from '@/lib/catalogue-context';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import InvestmentMapPanel from '@/components/investment-map-panel';
|
||||
import StructureGeographyPanel, {
|
||||
type StructureGeography,
|
||||
@@ -190,7 +191,7 @@ function Editor({
|
||||
</h2>
|
||||
{zones.map((z) => (
|
||||
<p key={z.id}>
|
||||
<Link className="underline" href={`/zonas/${z.id}`}>
|
||||
<Link className="underline" href={searchTerritoryHref(`/zonas/${z.id}`, pageUrl)}>
|
||||
{z.name}
|
||||
</Link>{' '}
|
||||
· {z.association_type} · Fonte: {z.source_reference}
|
||||
@@ -297,7 +298,7 @@ function Editor({
|
||||
<article key={opportunity.id}>
|
||||
<Link
|
||||
className="font-semibold underline"
|
||||
href={`/oportunidades/${opportunity.slug}?from_dossier=${record.id}`}
|
||||
href={searchTerritoryHref(`/oportunidades/${opportunity.slug}?from_dossier=${record.id}`, pageUrl)}
|
||||
>
|
||||
{opportunity.title}
|
||||
</Link>
|
||||
@@ -485,7 +486,7 @@ function Editor({
|
||||
{kind(r)}:{' '}
|
||||
<Link
|
||||
className="underline"
|
||||
href={`${base}/${r.id}${management ? '' : `?from=${record.id}`}`}
|
||||
href={searchTerritoryHref(`${base}/${r.id}${management ? '' : `?from=${record.id}`}`, pageUrl)}
|
||||
>
|
||||
{r.title}
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { searchTerritoryHref } from '../../resources/js/lib/search-territory.ts';
|
||||
|
||||
test('portfolio to catalogue to related screen retains only territorial search filters', () => {
|
||||
const start = '/carteira?province_id=inh&district_id=vil&q=airport&kind=project';
|
||||
const catalogue = searchTerritoryHref('/oportunidades?view=grid', start);
|
||||
assert.equal(catalogue, '/oportunidades?view=grid&province_id=inh&district_id=vil');
|
||||
const detail = searchTerritoryHref('/carteira/id', catalogue);
|
||||
assert.equal(searchTerritoryHref('/carteira', detail), '/carteira?province_id=inh&district_id=vil');
|
||||
assert.equal(searchTerritoryHref('/zonas?category=zee', start), '/zonas?category=zee&province_id=inh&district_id=vil');
|
||||
});
|
||||
test('nested historical return query restores territory without importing unrelated filters', () => {
|
||||
assert.equal(searchTerritoryHref('/oportunidades', '/carteira/id?catalogue_query='+encodeURIComponent('province_id=inh&district_id=vil&q=airport')), '/oportunidades?province_id=inh&district_id=vil');
|
||||
});
|
||||
test('explicit parent changes or clearing never import incompatible descendants', () => {
|
||||
const current = '/oportunidades?province_id=inh&district_id=vil&admin_post_id=post&locality_id=loc';
|
||||
assert.equal(searchTerritoryHref('/carteira?province_id=cabo', current), '/carteira?province_id=cabo');
|
||||
assert.equal(searchTerritoryHref('/carteira?district_id=other', current), '/carteira?district_id=other&province_id=inh');
|
||||
assert.equal(searchTerritoryHref('/carteira?province_id=', current), '/carteira?province_id=');
|
||||
});
|
||||
test('no cross-tab, project, account, external or unrelated route inheritance', () => {
|
||||
assert.equal(searchTerritoryHref('/oportunidades','/carteira'), '/oportunidades');
|
||||
assert.equal(searchTerritoryHref('/oportunidades','/investimentos/id?province_id=private'), '/oportunidades');
|
||||
assert.equal(searchTerritoryHref('/investimentos/id','/carteira?province_id=inh'), '/investimentos/id');
|
||||
assert.equal(searchTerritoryHref('https://other.test/carteira','/carteira?province_id=inh'), 'https://other.test/carteira');
|
||||
});
|
||||
Reference in New Issue
Block a user