Unify institution profiles and reduce primary menu to six groups
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
## Uniformização pública e menu — decisão expressa do proprietário,11/11/2026
|
||||
|
||||
Lista e detalhe das instituições financeiras devem usar a mesma apresentação:descrição única,categoria/tipo sem repetição,produtos/serviços,contactos e moradas úteis apenas quando disponíveis. Não apresentar secções vazias,avisos genéricos repetidos,notas de publicação/importação,preços antigos por reconfirmar ou a mesma informação várias vezes. Deduplicar apenas a apresentação,conservando dados e fontes no editor. Usar Quorus e Moza Banco como exemplos de conferência.
|
||||
Menu principal:Início,Descoberta,Realização,Suporte,Sobre nós,Mais. Descoberta:Concursos e anúncios,Localizações,Instituições financeiras,Catálogos relacionados. Realização:Iniciar investimento,Licenciamento,Acesso à terra. Suporte:Biblioteca,Legislação,Serviços ao investidor,Acompanhamento do projecto,Apoio ao investidor. Preservar conteúdo deSobre nós/Mais,contexto dos dossiers e permissões. Escolha de apresentação:acessos de trabalho antes no nível principal passam paraMais,sem novas concessões.
|
||||
|
||||
## Comunicação pública — orientação expressa do proprietário,11/11/2026
|
||||
|
||||
Apresentar conteúdos públicos com linguagem profissional,clara e apelativa,centrada na entidade,oportunidade,produtos e serviços úteis ao investidor. Não usar descrições de importação,cadastro,fichas,ficheiros ou documentos geradores como apresentação comercial. Preservar as fontes,versões e rastreabilidade no cadastro/editor;ligações institucionais e datas úteis podem ser consultadas separadamente. Não inventar ofertas,condições,disponibilidade,cobertura ou garantias para preencher lacunas. Esta orientação aplica-se transversalmente aos conteúdos do portal.
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
## 11/11/2026 — uniformização Quorus/Moza e menu de seis grupos em QA
|
||||
|
||||
Correcção orientada pelos exemplos expressos do proprietário:lista e detalhe reutilizam agora **o mesmo componente público**. Descrição única,categoria/tipo numa linha,produtos/serviços uma vez,contactos e moradas apenas quando disponíveis. Quorus:descrição de corretagem eSeguros uma vez;sem secções vazias ou avisos genéricos. Moza:descrição paraPME,CréditoPME/instrumentos uma vez,umtelefone e umamorada;sem notas de publicação,preços antigos,comissões/garantias por reconfirmar,entidade responsável repetida,sectores/cobertura desconhecidos ou referências institucionais duplicadas. Não inventa dados nem altera condições guardadas. Ligações dos produtos/contactos permanecem acessíveis uma vez;moradas diferentes são preservadas.
|
||||
|
||||
Menu principal exactamente **Início–Descoberta–Realização–Suporte–Sobre nós–Mais**. Concursos movido paraDescoberta;Biblioteca eInstituições financeiras com os nomes pedidos;conteúdos deSobre nós/Mais preservados. Acesso de trabalho deslocado paraMais,sem perder permissões;conta/contexto/licenciamentoS08 e filtros preservados. Instruções permanentes actualizadas com o padrão pedido.
|
||||
|
||||
**QA:**33verificações dirigidas aprovadas,com três casos novos:exemplos públicos exactos nas duas páginas,deduplicação conservadora e cabeçalho real com seis entradas em quatro combinações(público/autenticado,móvel/computador). Reutilizadas provas dos fluxos de interesse/dossier/editorial e das permissões,não reexecutadas na base. Fontes de270instituições intactas,sem migração/importação/republicação. Artefactos `outputs/funding-uniform-menu-20261111/verification.json` eHTMLs reais emSSR. Não é conferência visual de navegador;tipos/lint/compilação aprovados;pacote Git com aplicação/reversão conferidas no fecho.
|
||||
|
||||
**Estado:** implementado em QA,não instalado;homologação mantém `a59142ca` e270instituições publicadas. Restam instalação e conferência visual;não requer seleccionar/publicar novamente instituições. Restante saldo de módulos e fiscal preservado. Próximo trabalho:entregar este conjunto compatível e continuar os resíduos de implementação acordados.
|
||||
|
||||
## 11/11/2026 — apresentação financeira profissional instalada ema59142ca
|
||||
|
||||
Instalação expressamente autorizada,executada pelo processo Git existente e conferida às11:38:54Africa/Johannesburg(09:38:54UTC): `a59142caa2fc277975be82fb550223ff7d949594`. O directório `/fundos` e o detalhe apresentam descrição e produtos/serviços conhecidos,sem usar texto de importação,fichas ou documento gerador como apresentação. Descrições específicas são preservadas;resumo genérico exacto passa a uma descrição pela natureza publicada. Não são inventadas ofertas para instituições sem produtos específicos identificados. Metadados/fontes/histórico mantidos no editor;ligações institucionais e datas úteis acessíveis separadamente. Orientação transversal registada emAGENTS.
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
## 11/11/2026 — uniformização Quorus/Moza e menu de seis grupos em QA
|
||||
|
||||
Correcção orientada pelos exemplos expressos do proprietário:lista e detalhe reutilizam agora **o mesmo componente público**. Descrição única,categoria/tipo numa linha,produtos/serviços uma vez,contactos e moradas apenas quando disponíveis. Quorus:descrição de corretagem eSeguros uma vez;sem secções vazias ou avisos genéricos. Moza:descrição paraPME,CréditoPME/instrumentos uma vez,umtelefone e umamorada;sem notas de publicação,preços antigos,comissões/garantias por reconfirmar,entidade responsável repetida,sectores/cobertura desconhecidos ou referências institucionais duplicadas. Não inventa dados nem altera condições guardadas. Ligações dos produtos/contactos permanecem acessíveis uma vez;moradas diferentes são preservadas.
|
||||
|
||||
Menu principal exactamente **Início–Descoberta–Realização–Suporte–Sobre nós–Mais**. Concursos movido paraDescoberta;Biblioteca eInstituições financeiras com os nomes pedidos;conteúdos deSobre nós/Mais preservados. Acesso de trabalho deslocado paraMais,sem perder permissões;conta/contexto/licenciamentoS08 e filtros preservados. Instruções permanentes actualizadas com o padrão pedido.
|
||||
|
||||
**QA:**33verificações dirigidas aprovadas,com três casos novos:exemplos públicos exactos nas duas páginas,deduplicação conservadora e cabeçalho real com seis entradas em quatro combinações(público/autenticado,móvel/computador). Reutilizadas provas dos fluxos de interesse/dossier/editorial e das permissões,não reexecutadas na base. Fontes de270instituições intactas,sem migração/importação/republicação. Artefactos `outputs/funding-uniform-menu-20261111/verification.json` eHTMLs reais emSSR. Não é conferência visual de navegador;tipos/lint/compilação aprovados;pacote Git com aplicação/reversão conferidas no fecho.
|
||||
|
||||
**Estado:** implementado em QA,não instalado;homologação mantém `a59142ca` e270instituições publicadas. Restam instalação e conferência visual;não requer seleccionar/publicar novamente instituições. Restante saldo de módulos e fiscal preservado. Próximo trabalho:entregar este conjunto compatível e continuar os resíduos de implementação acordados.
|
||||
|
||||
## 11/11/2026 — apresentação financeira profissional instalada ema59142ca
|
||||
|
||||
Instalação expressamente autorizada,executada pelo processo Git existente e conferida às11:38:54Africa/Johannesburg(09:38:54UTC): `a59142caa2fc277975be82fb550223ff7d949594`. O directório `/fundos` e o detalhe apresentam descrição e produtos/serviços conhecidos,sem usar texto de importação,fichas ou documento gerador como apresentação. Descrições específicas são preservadas;resumo genérico exacto passa a uma descrição pela natureza publicada. Não são inventadas ofertas para instituições sem produtos específicos identificados. Metadados/fontes/histórico mantidos no editor;ligações institucionais e datas úteis acessíveis separadamente. Orientação transversal registada emAGENTS.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,3 +1,13 @@
|
||||
## 11/11/2026 — uniformização Quorus/Moza e menu de seis grupos em QA
|
||||
|
||||
Correcção orientada pelos exemplos expressos do proprietário:lista e detalhe reutilizam agora **o mesmo componente público**. Descrição única,categoria/tipo numa linha,produtos/serviços uma vez,contactos e moradas apenas quando disponíveis. Quorus:descrição de corretagem eSeguros uma vez;sem secções vazias ou avisos genéricos. Moza:descrição paraPME,CréditoPME/instrumentos uma vez,umtelefone e umamorada;sem notas de publicação,preços antigos,comissões/garantias por reconfirmar,entidade responsável repetida,sectores/cobertura desconhecidos ou referências institucionais duplicadas. Não inventa dados nem altera condições guardadas. Ligações dos produtos/contactos permanecem acessíveis uma vez;moradas diferentes são preservadas.
|
||||
|
||||
Menu principal exactamente **Início–Descoberta–Realização–Suporte–Sobre nós–Mais**. Concursos movido paraDescoberta;Biblioteca eInstituições financeiras com os nomes pedidos;conteúdos deSobre nós/Mais preservados. Acesso de trabalho deslocado paraMais,sem perder permissões;conta/contexto/licenciamentoS08 e filtros preservados. Instruções permanentes actualizadas com o padrão pedido.
|
||||
|
||||
**QA:**33verificações dirigidas aprovadas,com três casos novos:exemplos públicos exactos nas duas páginas,deduplicação conservadora e cabeçalho real com seis entradas em quatro combinações(público/autenticado,móvel/computador). Reutilizadas provas dos fluxos de interesse/dossier/editorial e das permissões,não reexecutadas na base. Fontes de270instituições intactas,sem migração/importação/republicação. Artefactos `outputs/funding-uniform-menu-20261111/verification.json` eHTMLs reais emSSR. Não é conferência visual de navegador;tipos/lint/compilação aprovados;pacote Git com aplicação/reversão conferidas no fecho.
|
||||
|
||||
**Estado:** implementado em QA,não instalado;homologação mantém `a59142ca` e270instituições publicadas. Restam instalação e conferência visual;não requer seleccionar/publicar novamente instituições. Restante saldo de módulos e fiscal preservado. Próximo trabalho:entregar este conjunto compatível e continuar os resíduos de implementação acordados.
|
||||
|
||||
## 11/11/2026 — apresentação financeira profissional instalada ema59142ca
|
||||
|
||||
Instalação expressamente autorizada,executada pelo processo Git existente e conferida às11:38:54Africa/Johannesburg(09:38:54UTC): `a59142caa2fc277975be82fb550223ff7d949594`. O directório `/fundos` e o detalhe apresentam descrição e produtos/serviços conhecidos,sem usar texto de importação,fichas ou documento gerador como apresentação. Descrições específicas são preservadas;resumo genérico exacto passa a uma descrição pela natureza publicada. Não são inventadas ofertas para instituições sem produtos específicos identificados. Metadados/fontes/histórico mantidos no editor;ligações institucionais e datas úteis acessíveis separadamente. Orientação transversal registada emAGENTS.
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import {
|
||||
ClassificationFields,
|
||||
ClassificationSummary,
|
||||
evidenceLabels,
|
||||
type Classification,
|
||||
} from '@/components/funding-classification';
|
||||
import {fundingPublicPresentation} from '@/lib/funding-public-presentation';
|
||||
import {
|
||||
fundingPublicPresentation,
|
||||
publicInstitutionContacts,
|
||||
publicInstitutionLocations,
|
||||
} from '@/lib/funding-public-presentation';
|
||||
import {
|
||||
financialCategories,
|
||||
financialTypes,
|
||||
} from '@/components/funding-classification';
|
||||
import { card, input, button } from '@/pages/investment/shared';
|
||||
|
||||
export type FundingSource = {
|
||||
@@ -552,135 +559,158 @@ export function InstitutionFields({
|
||||
</section>
|
||||
);
|
||||
}
|
||||
function Source({value}:{value:FundingSource}){return <>{value.source_url&&<details><summary>Informação institucional</summary><a className="underline" href={value.source_url} target="_blank" rel="noopener noreferrer">Consultar informação da entidade</a><p>Consulta em {value.checked_on}.</p></details>}{value.evidence_status==='historical'&&<p>Informação histórica.</p>}{value.evidence_status==='conflict'&&<p>Informação por esclarecer junto da instituição.</p>}</>;}
|
||||
export function InstitutionView({ value }: { value: InstitutionDetails }) {
|
||||
export function InstitutionView({
|
||||
value,
|
||||
compact = false,
|
||||
}: {
|
||||
value: InstitutionDetails & { title?: string; summary?: string };
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const presentation = fundingPublicPresentation({
|
||||
...value,
|
||||
title: value.title ?? '',
|
||||
summary: value.summary ?? '',
|
||||
kind: 'institution',
|
||||
}),
|
||||
contacts = publicInstitutionContacts(value.contacts),
|
||||
locations = publicInstitutionLocations(value.locations);
|
||||
const category = (value.institution_categories ?? [])
|
||||
.map((k) => financialCategories[k])
|
||||
.filter(Boolean)
|
||||
.join(' · '),
|
||||
types = (value.institution_types ?? [])
|
||||
.map((k) => financialTypes[k])
|
||||
.filter(Boolean)
|
||||
.join(' · ');
|
||||
const urls = new Set<string>(
|
||||
compact
|
||||
? []
|
||||
: presentation.products
|
||||
.map((p) => p.url)
|
||||
.filter((url): url is string => !!url),
|
||||
);
|
||||
const sites = contacts
|
||||
.filter((c) => c.kind === 'website')
|
||||
.filter((c) => {
|
||||
if (urls.has(c.value)) return false;
|
||||
urls.add(c.value);
|
||||
return true;
|
||||
});
|
||||
return (
|
||||
<section className={card}>
|
||||
<h2>Instituição financeira</h2>
|
||||
<ClassificationSummary value={value} />
|
||||
{!value.institution_types?.length && (
|
||||
<section className="space-y-4" aria-label="Informação da instituição">
|
||||
{!compact && (
|
||||
<h2 className="text-lg font-semibold">
|
||||
Instituição financeira
|
||||
</h2>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-semibold">Descrição</h3>
|
||||
<p>{presentation.description}</p>
|
||||
</div>
|
||||
{(category || types) && (
|
||||
<p>
|
||||
Natureza:{' '}
|
||||
{
|
||||
(
|
||||
{
|
||||
bank: 'Banco',
|
||||
credit: 'Instituição de crédito',
|
||||
microfinance: 'Microfinanças',
|
||||
investment_fund: 'Fundo de investimento',
|
||||
development:
|
||||
'Agência de desenvolvimento/financiamento',
|
||||
other: 'Outra natureza documentada',
|
||||
unknown: 'Por confirmar',
|
||||
} as Record<string, string>
|
||||
)[value.institution_nature]
|
||||
}
|
||||
{category}
|
||||
{category && types ? ': ' : ''}
|
||||
{types}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<h3>Contactos</h3>
|
||||
{value.contacts.length === 0 && <p>Contactos por confirmar.</p>}
|
||||
{value.contacts.map((c, i) => (
|
||||
<article key={i}>
|
||||
<p>
|
||||
{c.kind === 'website' ? (
|
||||
<a
|
||||
className="underline"
|
||||
href={c.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{c.value}
|
||||
</a>
|
||||
) : (
|
||||
c.value
|
||||
)}
|
||||
</p>
|
||||
<Source value={c} />
|
||||
</article>
|
||||
))}
|
||||
<h3>Sede e balcões — presença física</h3>
|
||||
{value.locations.length === 0 && (
|
||||
<p>Localização física por confirmar.</p>
|
||||
{['historical', 'merged'].includes(
|
||||
value.institution_status ?? '',
|
||||
) && <p>Instituição histórica ou integrada noutra entidade.</p>}
|
||||
<div>
|
||||
<h3 className="font-semibold">Produtos e serviços</h3>
|
||||
{presentation.products.length ? (
|
||||
<ul className="list-disc space-y-2 pl-5">
|
||||
{presentation.products.map((p) => (
|
||||
<li key={p.title}>
|
||||
{!compact && p.url ? (
|
||||
<a
|
||||
className="font-semibold underline"
|
||||
href={p.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{p.title}
|
||||
</a>
|
||||
) : (
|
||||
<strong>{p.title}</strong>
|
||||
)}
|
||||
{p.detail ? ': ' + p.detail : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : presentation.services.length ? (
|
||||
<p>{presentation.services.join(' · ')}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{!compact &&
|
||||
(sites.length > 0 ||
|
||||
contacts.some((c) => c.kind !== 'website')) && (
|
||||
<div>
|
||||
<h3 className="font-semibold">Contactos</h3>
|
||||
<ul className="space-y-1">
|
||||
{sites.map((c) => (
|
||||
<li key={c.value}>
|
||||
<a
|
||||
className="underline"
|
||||
href={c.value}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Website
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{contacts
|
||||
.filter((c) => c.kind !== 'website')
|
||||
.map((c) => (
|
||||
<li key={c.kind + c.value}>
|
||||
{c.kind === 'email' ? (
|
||||
<a
|
||||
className="underline"
|
||||
href={'mailto:' + c.value}
|
||||
>
|
||||
{c.value}
|
||||
</a>
|
||||
) : c.kind === 'phone' ? (
|
||||
<a
|
||||
className="underline"
|
||||
href={
|
||||
'tel:' +
|
||||
c.value.replace(
|
||||
/[^+0-9]/g,
|
||||
'',
|
||||
)
|
||||
}
|
||||
>
|
||||
{c.value}
|
||||
</a>
|
||||
) : (
|
||||
c.value
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{!compact && locations.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-semibold">Localização</h3>
|
||||
<ul className="space-y-1">
|
||||
{locations.map((l, i) => (
|
||||
<li key={i}>
|
||||
{l.role === 'headquarters'
|
||||
? 'Sede: '
|
||||
: l.role === 'branch'
|
||||
? 'Balcão: '
|
||||
: ''}
|
||||
{l.address}
|
||||
{l.hours ? ' · ' + l.hours : ''}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{value.locations.map((l, i) => (
|
||||
<article key={i}>
|
||||
<p>
|
||||
{l.role === 'headquarters'
|
||||
? 'Sede'
|
||||
: l.role === 'branch'
|
||||
? 'Balcão'
|
||||
: 'Ponto de contacto institucional'}
|
||||
: {l.label && !/^(Morada|Localização).*(documento|fonte)/i.test(l.label) ? l.label+' · ' : ''}{l.address}
|
||||
</p>
|
||||
<p>
|
||||
{l.territory_path?.map((n) => n.name).join(' / ') ||
|
||||
'Localização detalhada a confirmar'}
|
||||
</p>
|
||||
{l.location_text && (
|
||||
<p>Local indicado: {l.location_text}</p>
|
||||
)}
|
||||
{l.hours && (
|
||||
<p>
|
||||
Horário de atendimento:{' '}
|
||||
{l.hours}
|
||||
</p>
|
||||
)}
|
||||
<Source value={l} />
|
||||
</article>
|
||||
))}
|
||||
<h3>Produtos e serviços</h3>
|
||||
{value.products.length === 0 && (
|
||||
<p>
|
||||
{fundingPublicPresentation({...value,title:"",kind:"institution",summary:""}).services.join(" · ") || "Consulte as soluções disponíveis junto da instituição."}
|
||||
</p>
|
||||
)}
|
||||
{value.products.map((p) => (
|
||||
<article className={card} key={p.code}>
|
||||
<h4>{p.title}</h4>
|
||||
<p>{p.instrument}</p>
|
||||
<p>
|
||||
Cobertura do financiamento:{' '}
|
||||
{p.coverage === 'national'
|
||||
? 'Nacional, segundo a fonte'
|
||||
: p.coverage === 'territorial'
|
||||
? 'Locais documentados'
|
||||
: 'Por confirmar'}{' '}
|
||||
· {p.coverage_text}
|
||||
</p>
|
||||
{p.territory_paths?.map((path, i) => (
|
||||
<p key={i}>{path.map((n) => n.name).join(' / ')}</p>
|
||||
))}
|
||||
<p>Condições: {p.conditions || 'Por confirmar'}</p>
|
||||
<p>
|
||||
Montantes/moeda/prazos: {p.amounts || 'Por confirmar'}
|
||||
</p>
|
||||
<p>
|
||||
Disponibilidade:{' '}
|
||||
{p.availability === 'unknown'
|
||||
? 'Por confirmar'
|
||||
: p.availability === 'closed'
|
||||
? 'Encerrada segundo a fonte'
|
||||
: 'Oferta identificada na fonte; confirmar condições actuais com a instituição'}
|
||||
</p>
|
||||
{p.contact_url && (
|
||||
<a
|
||||
href={p.contact_url}
|
||||
className="underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Contactar/consultar o produto
|
||||
</a>
|
||||
)}
|
||||
<Source value={p} />
|
||||
</article>
|
||||
))}
|
||||
<p>
|
||||
Uma referência não é elegibilidade confirmada, aprovação nem
|
||||
garantia de financiamento. O portal não envia pedidos ou dados à
|
||||
instituição.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -200,11 +200,9 @@ export function PortalGroupedMenu() {
|
||||
{groups
|
||||
.filter((group) =>
|
||||
[
|
||||
'procurement-area',
|
||||
'discovery',
|
||||
'realization',
|
||||
'support',
|
||||
'work',
|
||||
].includes(group.id),
|
||||
)
|
||||
.map((group) => (
|
||||
@@ -260,7 +258,9 @@ export function PortalGroupedMenu() {
|
||||
<DropdownMenuTrigger
|
||||
className="tempo-nav-trigger"
|
||||
data-current-group={
|
||||
institutionalCurrent || undefined
|
||||
institutionalCurrent ||
|
||||
current === 'work' ||
|
||||
undefined
|
||||
}
|
||||
>
|
||||
Mais
|
||||
@@ -310,6 +310,17 @@ export function PortalGroupedMenu() {
|
||||
</DropdownMenuItem>
|
||||
),
|
||||
)}
|
||||
{groups
|
||||
.filter((group) => group.id === 'work')
|
||||
.map((group) => (
|
||||
<Fragment key={group.id}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuLabel>
|
||||
{t(group.label)}
|
||||
</DropdownMenuLabel>
|
||||
{items(group.children ?? [])}
|
||||
</Fragment>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
type PublicFunding = {
|
||||
export type PublicFunding = {
|
||||
title: string;
|
||||
kind: string;
|
||||
summary: string;
|
||||
@@ -8,6 +8,8 @@ type PublicFunding = {
|
||||
title: string;
|
||||
instrument?: string | null;
|
||||
availability?: string;
|
||||
contact_url?: string | null;
|
||||
source_url?: string | null;
|
||||
}[];
|
||||
};
|
||||
const descriptions: Record<string, string> = {
|
||||
@@ -45,9 +47,10 @@ const services: Record<string, string> = {
|
||||
};
|
||||
export function fundingPublicPresentation(p: PublicFunding) {
|
||||
const generic =
|
||||
!p.summary.trim() ||
|
||||
p.summary.trim() ===
|
||||
'Instituição identificada no documento fornecido. Categoria, localização e contactos são informativos; produtos, disponibilidade e condições não documentados permanecem por confirmar.';
|
||||
const description = generic
|
||||
'Instituição identificada no documento fornecido. Categoria, localização e contactos são informativos; produtos, disponibilidade e condições não documentados permanecem por confirmar.';
|
||||
let description = generic
|
||||
? (descriptions[p.institution_types?.[0] ?? ''] ??
|
||||
'Entidade do sector financeiro.')
|
||||
: p.summary
|
||||
@@ -58,9 +61,31 @@ export function fundingPublicPresentation(p: PublicFunding) {
|
||||
.replace(/Público\/finalidade:/g, 'Soluções para')
|
||||
.replace(/\.\./g, '.')
|
||||
.trim();
|
||||
const repeatedInstrument = (p.products ?? []).some(
|
||||
(product) =>
|
||||
product.instrument?.trim() &&
|
||||
description.startsWith(product.instrument.trim()),
|
||||
);
|
||||
if (repeatedInstrument) {
|
||||
const audience = description.match(
|
||||
/Soluções para\s+(.+?)(?:\.|$)/i,
|
||||
)?.[1];
|
||||
description = audience
|
||||
? 'Soluções financeiras para ' +
|
||||
audience.charAt(0).toLowerCase() +
|
||||
audience.slice(1) +
|
||||
'.'
|
||||
: (descriptions[p.institution_types?.[0] ?? ''] ?? description);
|
||||
}
|
||||
const products = (p.products ?? [])
|
||||
.filter((x) => x.availability !== 'closed')
|
||||
.map((x) => ({ title: x.title, detail: x.instrument?.trim() ?? '' }));
|
||||
.map((x) => ({
|
||||
title: x.title.startsWith(p.title + ' — ')
|
||||
? x.title.slice(p.title.length + 3)
|
||||
: x.title,
|
||||
detail: x.instrument?.trim() ?? '',
|
||||
url: x.contact_url || x.source_url || null,
|
||||
}));
|
||||
return {
|
||||
description,
|
||||
products,
|
||||
@@ -73,3 +98,74 @@ export function fundingPublicPresentation(p: PublicFunding) {
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const normal = (s: string) =>
|
||||
s
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.trim();
|
||||
export function publicInstitutionContacts<
|
||||
T extends { kind: string; value: string },
|
||||
>(rows: T[]) {
|
||||
const seen = new Set<string>();
|
||||
return rows.flatMap((row) => {
|
||||
const value =
|
||||
row.kind === 'phone'
|
||||
? row.value.replace(/\s*Não encontrado\.?$/i, '').trim()
|
||||
: row.value.trim();
|
||||
if (!value) return [];
|
||||
const key =
|
||||
row.kind +
|
||||
':' +
|
||||
(row.kind === 'phone'
|
||||
? value.replace(/\D/g, '')
|
||||
: value.toLowerCase().replace(/\/$/, ''));
|
||||
if (seen.has(key)) return [];
|
||||
seen.add(key);
|
||||
return [{ ...row, value }];
|
||||
});
|
||||
}
|
||||
export function publicInstitutionLocations<
|
||||
T extends {
|
||||
role: string;
|
||||
address?: string | null;
|
||||
location_text?: string;
|
||||
territory_path?: { name: string }[];
|
||||
},
|
||||
>(rows: T[]) {
|
||||
const result: T[] = [];
|
||||
for (const row of [...rows].sort(
|
||||
(a, b) =>
|
||||
(a.role === 'headquarters' ? 0 : 1) -
|
||||
(b.role === 'headquarters' ? 0 : 1),
|
||||
)) {
|
||||
if (!row.address?.trim()) continue;
|
||||
const address = normal(row.address);
|
||||
const duplicate = result.some((previous) => {
|
||||
const other = normal(previous.address ?? '');
|
||||
if (address === other) return true;
|
||||
const short = address.length < other.length ? address : other,
|
||||
long = address.length < other.length ? other : address;
|
||||
if (!long.startsWith(short + ' ')) return false;
|
||||
const rest = long.slice(short.length).trim();
|
||||
const localities = [
|
||||
row.location_text ?? '',
|
||||
previous.location_text ?? '',
|
||||
...(row.territory_path ?? []).map((x) => x.name),
|
||||
...(previous.territory_path ?? []).map((x) => x.name),
|
||||
].map(normal);
|
||||
return (
|
||||
localities.includes(rest) ||
|
||||
localities.some(
|
||||
(x) =>
|
||||
x.replace(/^cidade de /, '') ===
|
||||
rest.replace(/^cidade de /, ''),
|
||||
)
|
||||
);
|
||||
});
|
||||
if (!duplicate) result.push(row);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -478,40 +478,41 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
|
||||
: null)
|
||||
: null;
|
||||
return uniqueNavigationDestinations([
|
||||
...(access.procurement
|
||||
? [
|
||||
{
|
||||
id: 'procurement-area',
|
||||
label: 'Concursos e anúncios',
|
||||
children: [
|
||||
link(
|
||||
'procurement',
|
||||
'Consultar concursos e anúncios',
|
||||
'/concursos',
|
||||
),
|
||||
...(access.authenticated
|
||||
? [
|
||||
link(
|
||||
'procurement-watched',
|
||||
'Os concursos que acompanho',
|
||||
'/concursos/acompanhar',
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
id: 'discovery',
|
||||
label: 'Descoberta',
|
||||
children: [
|
||||
...(access.procurement
|
||||
? [
|
||||
{
|
||||
id: 'procurement-area',
|
||||
label: 'Concursos e anúncios',
|
||||
children: [
|
||||
link(
|
||||
'procurement',
|
||||
'Consultar concursos e anúncios',
|
||||
'/concursos',
|
||||
),
|
||||
...(access.authenticated
|
||||
? [
|
||||
link(
|
||||
'procurement-watched',
|
||||
'Os concursos que acompanho',
|
||||
'/concursos/acompanhar',
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
link('locations', 'Localizações', '/oportunidades?view=map'),
|
||||
...(access.funding
|
||||
? [
|
||||
link(
|
||||
'funding',
|
||||
'Instituições financeiras e seguros',
|
||||
'Instituições financeiras',
|
||||
'/fundos',
|
||||
),
|
||||
...(access.authenticated
|
||||
@@ -580,7 +581,7 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
|
||||
id: 'support',
|
||||
label: 'Suporte',
|
||||
children: [
|
||||
link('documents', 'Biblioteca de documentos', '/documentos'),
|
||||
link('documents', 'Biblioteca', '/documentos'),
|
||||
link('legislation', 'Legislação', '/legislacao'),
|
||||
link(
|
||||
'services',
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import {fundingPublicPresentation} from '@/lib/funding-public-presentation';
|
||||
import {
|
||||
InstitutionView,
|
||||
type InstitutionDetails,
|
||||
} from '@/components/funding-institution';
|
||||
import {
|
||||
financialCategories,
|
||||
financialTypes,
|
||||
@@ -68,8 +71,9 @@ export default function Index({
|
||||
A minha selecção
|
||||
</Link>
|
||||
<p>
|
||||
Encontre bancos, seguradoras, fundos e outros parceiros financeiros.
|
||||
Explore as soluções e os contactos adequados ao seu investimento.
|
||||
Encontre bancos, seguradoras, fundos e outros parceiros
|
||||
financeiros. Explore as soluções e os contactos adequados ao seu
|
||||
investimento.
|
||||
</p>
|
||||
<form
|
||||
className={`${card} flex flex-wrap gap-4`}
|
||||
@@ -253,10 +257,9 @@ export default function Index({
|
||||
</label>
|
||||
</form>
|
||||
<p className="text-sm">
|
||||
Sector e local filtram o âmbito geral do programa. Cada chamada
|
||||
pode ter uma cobertura mais restrita. O estado indica que existe
|
||||
pelo menos uma janela nesse estado; não confirma elegibilidade
|
||||
nem candidatura aberta no local escolhido.
|
||||
Explore as instituições e soluções adequadas ao seu
|
||||
investimento. Use os filtros para encontrar a informação de que
|
||||
precisa.
|
||||
</p>
|
||||
{items.data.length === 0 && (
|
||||
<p>
|
||||
@@ -274,27 +277,33 @@ export default function Index({
|
||||
{item.payload.title}
|
||||
</Link>
|
||||
</h2>
|
||||
<p>{fundingPublicPresentation(item.payload).description}</p>
|
||||
{item.payload.kind==='institution' && <div><h3 className="font-semibold">Produtos e serviços</h3>{fundingPublicPresentation(item.payload).products.length ? <ul>{fundingPublicPresentation(item.payload).products.map(p=><li key={p.title}>{p.title}{p.detail ? ': '+p.detail : ''}</li>)}</ul> : <p>{fundingPublicPresentation(item.payload).services.join(' · ') || 'Consulte as soluções disponíveis junto da instituição.'}</p>}</div>}
|
||||
<p>
|
||||
{(item.payload.institution_categories ?? [])
|
||||
.map((k) => financialCategories[k] ?? k)
|
||||
.join(' · ')}{' '}
|
||||
·{' '}
|
||||
{(item.payload.institution_types ?? [])
|
||||
.map((k) => financialTypes[k] ?? k)
|
||||
.join(' · ')}
|
||||
</p>
|
||||
{item.payload.kind!=='institution' && <p>Entidade gestora: {item.payload.manager_name}</p>}
|
||||
{Object.entries(item.filter_explanation ?? {}).map(
|
||||
([key, message]) => (
|
||||
<p key={key}>{message}</p>
|
||||
),
|
||||
{item.payload.kind === 'institution' ? (
|
||||
<InstitutionView
|
||||
compact
|
||||
value={
|
||||
item.payload as InstitutionDetails & {
|
||||
title: string;
|
||||
summary: string;
|
||||
}
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<p>{item.payload.summary}</p>
|
||||
<p>Entidade gestora: {item.payload.manager_name}</p>
|
||||
{Object.entries(item.filter_explanation ?? {}).map(
|
||||
([key, message]) => (
|
||||
<p key={key}>{message}</p>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{item.payload.kind==='institution' && item.payload.windows.length===0 ? null : item.payload.windows.length === 0 ? (
|
||||
{item.payload.kind === 'institution' &&
|
||||
item.payload.windows.length === 0 ? null : item.payload
|
||||
.windows.length === 0 ? (
|
||||
<p>
|
||||
Não existe uma janela de candidatura confirmada
|
||||
para este programa.
|
||||
Não existe uma janela de candidatura confirmada para
|
||||
este programa.
|
||||
</p>
|
||||
) : (
|
||||
<ul>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {fundingPublicPresentation} from '@/lib/funding-public-presentation';
|
||||
import { fundingPublicPresentation } from '@/lib/funding-public-presentation';
|
||||
import {
|
||||
InstitutionView,
|
||||
type InstitutionDetails,
|
||||
@@ -41,9 +41,20 @@ export default function Show({
|
||||
>
|
||||
Voltar aos fundos e programas
|
||||
</Link>
|
||||
<section><h2 className="text-xl font-semibold">Descrição</h2><p>{fundingPublicPresentation(payload).description}</p></section>
|
||||
{payload.kind === 'institution' && (
|
||||
<InstitutionView value={payload as InstitutionDetails} />
|
||||
{payload.kind === 'institution' ? (
|
||||
<InstitutionView
|
||||
value={
|
||||
payload as InstitutionDetails & {
|
||||
title: string;
|
||||
summary: string;
|
||||
}
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<section>
|
||||
<h2 className="text-xl font-semibold">Descrição</h2>
|
||||
<p>{fundingPublicPresentation(payload).description}</p>
|
||||
</section>
|
||||
)}
|
||||
{authenticated && (
|
||||
<FundingDossierChoice
|
||||
@@ -55,90 +66,121 @@ export default function Show({
|
||||
destinations={destinations}
|
||||
/>
|
||||
)}
|
||||
<p>
|
||||
Entidade responsável: {payload.manager_name}.
|
||||
</p>
|
||||
{(payload.territory_paths ?? []).map((path, index) => (
|
||||
<p key={index}>
|
||||
Local abrangido: {path.map((node) => node.name).join(' / ')}
|
||||
</p>
|
||||
))}
|
||||
<p>
|
||||
Sectores:{' '}
|
||||
{payload.sectors_text ||
|
||||
(payload.sectors_scope === 'all'
|
||||
? 'Todos, segundo a fonte'
|
||||
: payload.sectors_scope === 'unknown'
|
||||
? 'Por confirmar'
|
||||
: (payload.sector_snapshots ?? [])
|
||||
.map((sector) => sector.name)
|
||||
.join(', '))}
|
||||
</p>
|
||||
<p>
|
||||
{payload.coverage === 'national'
|
||||
? 'Cobertura nacional'
|
||||
: payload.coverage === 'territorial'
|
||||
? 'Cobertura territorial'
|
||||
: 'Cobertura por confirmar'}
|
||||
{payload.coverage_text ? ` — ${payload.coverage_text}` : ''}
|
||||
</p>
|
||||
{payload.source_url && <details><summary>Informação institucional</summary><a className="underline" href={payload.source_url} target="_blank" rel="noopener noreferrer">Consultar informação da entidade</a><p>Actualização consultada em {payload.source_checked_on}.</p></details>}
|
||||
{(payload.kind!=='institution'||payload.windows.length>0) && ( <section className={card}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Janelas de candidatura
|
||||
</h2>
|
||||
{payload.windows.length === 0 && (
|
||||
<p>
|
||||
Não há uma janela de candidatura confirmada para este programa.
|
||||
</p>
|
||||
)}
|
||||
{payload.windows.map((item) => (
|
||||
<article key={item.code} className="my-4">
|
||||
<h3 className="font-semibold">{item.title}</h3>
|
||||
<p>{windowLabels[item.state]}</p>
|
||||
{item.start_on && <p>Abertura: {item.start_on}</p>}
|
||||
{item.end_on && <p>Data-limite: {item.end_on}</p>}
|
||||
{item.coverage_text && <p>{item.coverage_text}</p>}
|
||||
<p>
|
||||
{item.coverage === 'programme'
|
||||
? 'Mesma cobertura do programa'
|
||||
: item.coverage === 'national'
|
||||
? 'Chamada nacional'
|
||||
: item.coverage === 'unknown'
|
||||
? 'Cobertura da chamada por confirmar'
|
||||
: 'Chamada territorial'}
|
||||
{payload.kind !== 'institution' && (
|
||||
<>
|
||||
{' '}
|
||||
<p>Entidade responsável: {payload.manager_name}.</p>
|
||||
{(payload.territory_paths ?? []).map((path, index) => (
|
||||
<p key={index}>
|
||||
Local abrangido:{' '}
|
||||
{path.map((node) => node.name).join(' / ')}
|
||||
</p>
|
||||
{(item.territory_paths ?? []).map((path, index) => (
|
||||
<p key={index}>
|
||||
{path.map((node) => node.name).join(' / ')}
|
||||
</p>
|
||||
))}
|
||||
<a
|
||||
className="underline"
|
||||
href={item.source_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{item.source_reference}
|
||||
</a>
|
||||
<p>Fonte consultada em {item.checked_on}.</p>
|
||||
{item.application_url && (
|
||||
))}
|
||||
<p>
|
||||
Sectores:{' '}
|
||||
{payload.sectors_text ||
|
||||
(payload.sectors_scope === 'all'
|
||||
? 'Todos, segundo a fonte'
|
||||
: payload.sectors_scope === 'unknown'
|
||||
? 'Por confirmar'
|
||||
: (payload.sector_snapshots ?? [])
|
||||
.map((sector) => sector.name)
|
||||
.join(', '))}
|
||||
</p>
|
||||
<p>
|
||||
{payload.coverage === 'national'
|
||||
? 'Cobertura nacional'
|
||||
: payload.coverage === 'territorial'
|
||||
? 'Cobertura territorial'
|
||||
: 'Cobertura por confirmar'}
|
||||
{payload.coverage_text
|
||||
? ` — ${payload.coverage_text}`
|
||||
: ''}
|
||||
</p>
|
||||
{payload.source_url && (
|
||||
<details>
|
||||
<summary>Informação institucional</summary>
|
||||
<a
|
||||
className="underline"
|
||||
href={item.application_url}
|
||||
href={payload.source_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Consultar o canal indicado pela entidade
|
||||
Consultar informação da entidade
|
||||
</a>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</section>)}
|
||||
{(payload.kind!=='institution'||payload.criteria.length>0||payload.windows.some(window=>window.criteria.length>0)) && ( <FundingCriteriaAssessment
|
||||
key={record.id + ':' + record.version_id}
|
||||
record={record}
|
||||
/>)}
|
||||
<p>
|
||||
Actualização consultada em{' '}
|
||||
{payload.source_checked_on}.
|
||||
</p>
|
||||
</details>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{(payload.kind !== 'institution' || payload.windows.length > 0) && (
|
||||
<section className={card}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Janelas de candidatura
|
||||
</h2>
|
||||
{payload.windows.length === 0 && (
|
||||
<p>
|
||||
Não há uma janela de candidatura confirmada para
|
||||
este programa.
|
||||
</p>
|
||||
)}
|
||||
{payload.windows.map((item) => (
|
||||
<article key={item.code} className="my-4">
|
||||
<h3 className="font-semibold">{item.title}</h3>
|
||||
<p>{windowLabels[item.state]}</p>
|
||||
{item.start_on && <p>Abertura: {item.start_on}</p>}
|
||||
{item.end_on && <p>Data-limite: {item.end_on}</p>}
|
||||
{item.coverage_text && <p>{item.coverage_text}</p>}
|
||||
<p>
|
||||
{item.coverage === 'programme'
|
||||
? 'Mesma cobertura do programa'
|
||||
: item.coverage === 'national'
|
||||
? 'Chamada nacional'
|
||||
: item.coverage === 'unknown'
|
||||
? 'Cobertura da chamada por confirmar'
|
||||
: 'Chamada territorial'}
|
||||
</p>
|
||||
{(item.territory_paths ?? []).map((path, index) => (
|
||||
<p key={index}>
|
||||
{path.map((node) => node.name).join(' / ')}
|
||||
</p>
|
||||
))}
|
||||
<a
|
||||
className="underline"
|
||||
href={item.source_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{item.source_reference}
|
||||
</a>
|
||||
<p>Fonte consultada em {item.checked_on}.</p>
|
||||
{item.application_url && (
|
||||
<a
|
||||
className="underline"
|
||||
href={item.application_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Consultar o canal indicado pela entidade
|
||||
</a>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
{(payload.kind !== 'institution' ||
|
||||
payload.criteria.length > 0 ||
|
||||
payload.windows.some(
|
||||
(window) => window.criteria.length > 0,
|
||||
)) && (
|
||||
<FundingCriteriaAssessment
|
||||
key={record.id + ':' + record.version_id}
|
||||
record={record}
|
||||
/>
|
||||
)}
|
||||
{payload.documents.length > 0 && (
|
||||
<section className={card}>
|
||||
<h2>Documentos associados</h2>
|
||||
@@ -162,10 +204,7 @@ export default function Show({
|
||||
)}
|
||||
<section className={card}>
|
||||
<h2>Guardar interesse</h2>
|
||||
<p>
|
||||
Guarde esta opção para retomar a preparação do investimento. Não envia uma
|
||||
candidatura nem contacta a entidade.
|
||||
</p>
|
||||
|
||||
{saved ? (
|
||||
<>
|
||||
<p>
|
||||
|
||||
@@ -84,14 +84,11 @@ test("actual institution view keeps physical address separate from product cover
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(InstitutionView, { value: details }),
|
||||
);
|
||||
assert.match(html, /Sede: SYNTHETIC Maputo office/);
|
||||
assert.match(html, /Nacional, segundo a fonte/);
|
||||
assert.match(html, /Condições: Por confirmar/);
|
||||
assert.match(html, /Disponibilidade: Por confirmar/);
|
||||
assert.match(html, /Sede: SYNTHETIC address/);
|
||||
assert.match(html, /SYNTHETIC QA investment/);
|
||||
assert.match(html, /https:\/\/example.invalid\/contact/);
|
||||
assert.match(html, /https:\/\/example\.invalid\/source/);
|
||||
assert.match(html, /Consultar informação da entidade/);
|
||||
assert.match(html, /não é elegibilidade confirmada/);
|
||||
assert.doesNotMatch(html,/Por confirmar|não é elegibilidade|SHA-256/);
|
||||
|
||||
});
|
||||
test("adding a physical location does not fill product financing coverage", () => {
|
||||
const { InstitutionFields } = load("components/funding-institution.tsx");
|
||||
|
||||
@@ -60,16 +60,26 @@ function load(file) {
|
||||
}
|
||||
: name === './shared'
|
||||
? { windowLabels: {} }
|
||||
: name === '@/lib/portal-api' ? {portalRequest:()=>{throw Error('Read-only rendering must not send requests');}} : name === '@/components/funding-classification'
|
||||
? load('components/funding-classification.tsx')
|
||||
: name === '@/components/funding-institution'
|
||||
? load('components/funding-institution.tsx')
|
||||
: name === '@/components/funding-dossier-choice'
|
||||
? { FundingDossierChoice: () => null }
|
||||
: name ===
|
||||
'@/components/funding-criteria-assessment'
|
||||
? load('components/funding-criteria-assessment.tsx')
|
||||
: require(name),
|
||||
: name === '@/lib/portal-api'
|
||||
? {
|
||||
portalRequest: () => {
|
||||
throw Error(
|
||||
'Read-only rendering must not send requests',
|
||||
);
|
||||
},
|
||||
}
|
||||
: name === '@/components/funding-classification'
|
||||
? load('components/funding-classification.tsx')
|
||||
: name === '@/components/funding-institution'
|
||||
? load('components/funding-institution.tsx')
|
||||
: name === '@/components/funding-dossier-choice'
|
||||
? { FundingDossierChoice: () => null }
|
||||
: name ===
|
||||
'@/components/funding-criteria-assessment'
|
||||
? load(
|
||||
'components/funding-criteria-assessment.tsx',
|
||||
)
|
||||
: require(name),
|
||||
module,
|
||||
module.exports,
|
||||
);
|
||||
@@ -146,8 +156,143 @@ test('actual public cards and detail lead with description/products and keep imp
|
||||
new RegExp(raw.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
|
||||
);
|
||||
}
|
||||
const out=new URL('../../outputs/funding-public-editorial-20261111/',import.meta.url);fs.mkdirSync(out,{recursive:true});const name=p.products.length?'bank-with-products':'investment-manager';fs.writeFileSync(new URL(name+'-index.html',out),index);fs.writeFileSync(new URL(name+'-detail.html',out),detail);fs.writeFileSync(new URL('verification.json',out),JSON.stringify({publishedSourceRecords:270,descriptionsPreserved:true,productsNotFabricated:true,actualPublicComponents:true,metadataRemovedFromPresentation:true,privateEditorialFieldsUnchanged:true,activeDataWrites:0,visualBrowserReview:false},null,2));
|
||||
const out = new URL(
|
||||
'../../outputs/funding-public-editorial-20261111/',
|
||||
import.meta.url,
|
||||
);
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const name = p.products.length
|
||||
? 'bank-with-products'
|
||||
: 'investment-manager';
|
||||
fs.writeFileSync(new URL(name + '-index.html', out), index);
|
||||
fs.writeFileSync(new URL(name + '-detail.html', out), detail);
|
||||
fs.writeFileSync(
|
||||
new URL('verification.json', out),
|
||||
JSON.stringify(
|
||||
{
|
||||
publishedSourceRecords: 270,
|
||||
descriptionsPreserved: true,
|
||||
productsNotFabricated: true,
|
||||
actualPublicComponents: true,
|
||||
metadataRemovedFromPresentation: true,
|
||||
privateEditorialFieldsUnchanged: true,
|
||||
activeDataWrites: 0,
|
||||
visualBrowserReview: false,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
assert.match(detail, /Descrição/);
|
||||
if (p.products.length) assert.match(detail, /Soluções PME/);
|
||||
}
|
||||
});
|
||||
|
||||
test('Quorus and Moza use one shared clean profile in both list and detail', () => {
|
||||
const Index = load('pages/funding/index.tsx').default,
|
||||
Show = load('pages/funding/show.tsx').default;
|
||||
const out = new URL(
|
||||
'../../outputs/funding-uniform-menu-20261111/',
|
||||
import.meta.url,
|
||||
);
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
for (const title of ['Quorus', 'Moza Banco']) {
|
||||
const raw = records.find((p) => p.title === title);
|
||||
assert.ok(raw, title);
|
||||
const p = { ...raw, windows: [], documents: [] },
|
||||
item = {
|
||||
id: 'synthetic',
|
||||
version_id: 'synthetic-version',
|
||||
version: 1,
|
||||
payload: p,
|
||||
};
|
||||
const index = renderToStaticMarkup(
|
||||
React.createElement(Index, {
|
||||
items: { data: [item], links: [] },
|
||||
filters: {},
|
||||
kinds: {},
|
||||
options: { territories: [], sectors: [] },
|
||||
management: false,
|
||||
}),
|
||||
);
|
||||
const detail = renderToStaticMarkup(
|
||||
React.createElement(Show, {
|
||||
record: item,
|
||||
authenticated: false,
|
||||
saved: null,
|
||||
}),
|
||||
);
|
||||
for (const html of [
|
||||
index.match(/<article class="card">([\s\S]*?)<\/article>/)?.[0] ??
|
||||
index,
|
||||
detail,
|
||||
]) {
|
||||
assert.doesNotMatch(
|
||||
html,
|
||||
/Por confirmar|por confirmar|correspondência confirmada|Instituição identificada|Entidade responsável|Sectores:|Cobertura|garantia de financiamento|Não encontrado|ficha|Sede ou contacto indicado|Informação institucional/,
|
||||
);
|
||||
assert.equal((html.match(/Produtos e serviços/g) || []).length, 1);
|
||||
assert.equal((html.match(/Descrição/g) || []).length, 1);
|
||||
}
|
||||
if (title === 'Quorus') {
|
||||
assert.equal(
|
||||
(detail.match(/Entidade de corretagem de seguros/g) || [])
|
||||
.length,
|
||||
1,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
detail,
|
||||
/<h3[^>]*>Contactos|<h3[^>]*>Localização/,
|
||||
);
|
||||
} else {
|
||||
assert.equal((detail.match(/21 342 000/g) || []).length, 1);
|
||||
assert.equal(
|
||||
(detail.match(/Rua dos Desportistas/g) || []).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
detail.match(
|
||||
/Tesouraria, investimento, construção e equipamento/g,
|
||||
) || []
|
||||
).length,
|
||||
1,
|
||||
);
|
||||
assert.doesNotMatch(detail, /500\.000|30\.000\.000|Prime|spread/);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
new URL(title.replaceAll(' ', '-') + '-index.html', out),
|
||||
index,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
new URL(title.replaceAll(' ', '-') + '-detail.html', out),
|
||||
detail,
|
||||
);
|
||||
}
|
||||
});
|
||||
test('contact and location deduplication preserves different branches and original records', () => {
|
||||
const moza = records.find((p) => p.title === 'Moza Banco'),
|
||||
original = JSON.stringify(moza);
|
||||
assert.equal(
|
||||
presentation
|
||||
.publicInstitutionContacts(moza.contacts)
|
||||
.filter((c) => c.kind === 'phone').length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
presentation.publicInstitutionLocations(moza.locations).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
presentation.publicInstitutionLocations([
|
||||
...moza.locations,
|
||||
{
|
||||
...moza.locations[0],
|
||||
role: 'branch',
|
||||
address: 'Rua dos Desportistas, número 714',
|
||||
},
|
||||
]).length,
|
||||
2,
|
||||
);
|
||||
assert.equal(JSON.stringify(moza), original);
|
||||
});
|
||||
|
||||
@@ -417,7 +417,7 @@ void test('installed concours has its own primary group and preserves public and
|
||||
authenticated,
|
||||
procurement: true,
|
||||
});
|
||||
const group = groups.find((g) => g.id === 'procurement-area');
|
||||
const group = groups.find((g) => g.id === 'discovery').children.find(g=>g.id==='procurement-area');
|
||||
assert.equal(group.label, 'Concursos e anúncios');
|
||||
assert.equal(
|
||||
destinations(groups).filter((h) => h === '/concursos').length,
|
||||
@@ -427,15 +427,15 @@ void test('installed concours has its own primary group and preserves public and
|
||||
JSON.stringify(groups.find((g) => g.id === 'discovery')).includes(
|
||||
'/concursos',
|
||||
),
|
||||
false,
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
currentNavigationGroup(groups, '/concursos?q=obras'),
|
||||
'procurement-area',
|
||||
'discovery',
|
||||
);
|
||||
assert.equal(
|
||||
currentNavigationGroup(groups, '/concursos/fixture'),
|
||||
'procurement-area',
|
||||
'discovery',
|
||||
);
|
||||
assert.equal(
|
||||
destinations(group.children).includes('/concursos/acompanhar'),
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { compileFunction } from 'node:vm';
|
||||
import ts from 'typescript';
|
||||
import React from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import * as navigation from '../../resources/js/lib/portal-navigation.ts';
|
||||
import { searchTerritoryHref } from '../../resources/js/lib/search-territory.ts';
|
||||
const require = createRequire(import.meta.url);
|
||||
let state, mobile;
|
||||
const primitive =
|
||||
(tag) =>
|
||||
({ children, asChild, className, ...props }) =>
|
||||
asChild
|
||||
? React.cloneElement(children, { className })
|
||||
: React.createElement(
|
||||
tag,
|
||||
{
|
||||
className,
|
||||
...Object.fromEntries(
|
||||
Object.entries(props).filter(
|
||||
([k]) =>
|
||||
k.startsWith('aria-') ||
|
||||
k.startsWith('data-'),
|
||||
),
|
||||
),
|
||||
},
|
||||
children,
|
||||
);
|
||||
const source = fs.readFileSync(
|
||||
new URL(
|
||||
'../../resources/js/components/portal-grouped-menu.tsx',
|
||||
import.meta.url,
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
module = { exports: {} };
|
||||
const output = ts.transpileModule(source, {
|
||||
compilerOptions: {
|
||||
module: ts.ModuleKind.CommonJS,
|
||||
target: ts.ScriptTarget.ES2022,
|
||||
jsx: ts.JsxEmit.ReactJSX,
|
||||
},
|
||||
}).outputText;
|
||||
compileFunction(output, ['require', 'module', 'exports'])(
|
||||
(name) =>
|
||||
name === '@/components/portal-product-identity'
|
||||
? {}
|
||||
: name === '@inertiajs/react'
|
||||
? {
|
||||
usePage: () => state,
|
||||
Link: ({ children, href, className, ...props }) =>
|
||||
React.createElement(
|
||||
'a',
|
||||
{
|
||||
href,
|
||||
className,
|
||||
...Object.fromEntries(
|
||||
Object.entries(props).filter(
|
||||
([k]) =>
|
||||
k.startsWith('aria-') ||
|
||||
k.startsWith('data-'),
|
||||
),
|
||||
),
|
||||
},
|
||||
children,
|
||||
),
|
||||
router: {},
|
||||
}
|
||||
: name === 'lucide-react'
|
||||
? new Proxy({}, { get: () => () => null })
|
||||
: name === '@/hooks/use-mobile'
|
||||
? { useIsMobile: () => mobile }
|
||||
: name === '@/components/navigation-context'
|
||||
? { useNavigationContext: () => ({ view: null }) }
|
||||
: name === '@/components/tempo-brand'
|
||||
? { useTempoUi: () => ({ t: (x) => x, language: 'pt' }) }
|
||||
: name === '@/components/tempo-header-tools'
|
||||
? { TempoLanguageSelector: () => null }
|
||||
: name === '@/components/ui/dropdown-menu'
|
||||
? new Proxy(
|
||||
{},
|
||||
{
|
||||
get: (_, key) =>
|
||||
primitive(
|
||||
String(key).includes('Trigger')
|
||||
? 'button'
|
||||
: 'div',
|
||||
),
|
||||
},
|
||||
)
|
||||
: name === '@/lib/portal-navigation'
|
||||
? navigation
|
||||
: name === '@/lib/search-territory'
|
||||
? { searchTerritoryHref }
|
||||
: name === '@/routes'
|
||||
? { logout: () => '/logout' }
|
||||
: require(name),
|
||||
module,
|
||||
module.exports,
|
||||
);
|
||||
test('actual header has exactly the requested six primary entries on desktop and mobile for public and signed-in users', () => {
|
||||
const out = new URL(
|
||||
'../../outputs/funding-uniform-menu-20261111/',
|
||||
import.meta.url,
|
||||
);
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
for (const authenticated of [false, true])
|
||||
for (mobile of [false, true]) {
|
||||
state = {
|
||||
url: '/',
|
||||
props: {
|
||||
auth: {
|
||||
user: authenticated
|
||||
? {
|
||||
name: 'SYNTHETIC QA',
|
||||
email: 'qa@example.invalid',
|
||||
}
|
||||
: null,
|
||||
canManagePortal: false,
|
||||
},
|
||||
procurementEnabled: true,
|
||||
fundingEnabled: true,
|
||||
},
|
||||
};
|
||||
const html = renderToStaticMarkup(
|
||||
React.createElement(module.exports.PortalGroupedMenu),
|
||||
);
|
||||
const nav = html.match(
|
||||
/<nav[^>]*aria-label="Áreas do portal"[\s\S]*?<\/nav>/,
|
||||
)?.[0];
|
||||
assert.ok(nav);
|
||||
const labels = [
|
||||
...nav.matchAll(
|
||||
/<(?:a|button)[^>]*class="tempo-nav-trigger"[^>]*>([\s\S]*?)<\/(?:a|button)>/g,
|
||||
),
|
||||
].map((m) => m[1].replace(/<[^>]*>/g, '').trim());
|
||||
assert.deepEqual(labels, [
|
||||
'Início',
|
||||
'Descoberta',
|
||||
'Realização',
|
||||
'Suporte',
|
||||
'Sobre nós',
|
||||
'Mais',
|
||||
]);
|
||||
for (const href of [
|
||||
'/concursos',
|
||||
'/fundos',
|
||||
'/documentos',
|
||||
'/legislacao',
|
||||
'/sobre',
|
||||
'/contacto',
|
||||
'/acesso-gestao',
|
||||
])
|
||||
assert.ok(html.includes('href="' + href + '"'), href);
|
||||
assert.ok(html.includes('step=S08'));
|
||||
assert.equal(html.includes('/management/'), false);
|
||||
fs.writeFileSync(
|
||||
new URL(
|
||||
'header-' + authenticated + '-' + mobile + '.html',
|
||||
out,
|
||||
),
|
||||
html,
|
||||
);
|
||||
}
|
||||
fs.writeFileSync(
|
||||
new URL('verification.json', out),
|
||||
JSON.stringify(
|
||||
{
|
||||
synthetic: true,
|
||||
sharedInstitutionProfile: true,
|
||||
suppliedExamples: ['Quorus', 'Moza Banco'],
|
||||
sourceRowsPreserved: true,
|
||||
primaryEntries: [
|
||||
'Início',
|
||||
'Descoberta',
|
||||
'Realização',
|
||||
'Suporte',
|
||||
'Sobre nós',
|
||||
'Mais',
|
||||
],
|
||||
actualHeaderRendered: true,
|
||||
publicAndSignedIn: true,
|
||||
mobileAndDesktopBranches: true,
|
||||
visualBrowserReview: false,
|
||||
activeDataWrites: 0,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user