This commit is contained in:
2026-11-13 09:54:01 +02:00
33 changed files with 3442 additions and 314 deletions
+156 -126
View File
@@ -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>
+3
View File
@@ -1,8 +1,10 @@
import SectorResourceReference, { type SectorResourceData } from './sector-resource-reference';
import { Link } from '@inertiajs/react';
import EducationalArticleCard, {
type EducationalArticle,
} from './educational-article-card';
export type SectorEntryData = {
resource_reference?: SectorResourceData | null;
articles: EducationalArticle[];
relations: {
id: string;
@@ -43,6 +45,7 @@ export default function SectorEntry({
modalidade adequada. Cada oportunidade tem promotor, objetivo e
dados próprios.
</p>
{data.resource_reference && <SectorResourceReference data={data.resource_reference} />}
<nav aria-label="Ações no setor" className="flex flex-wrap gap-3">
{data.start_href ? (
<Link
@@ -0,0 +1,233 @@
import { useState } from "react";
import { Link, usePage } from "@inertiajs/react";
export type SectorResourceArea = {
id: string;
group_ids: string[];
title: string;
locations_text: string;
location_labels: string[];
market_evidence: string;
value_chain: string;
indicative_location: string;
promotion_ready: boolean;
source_row: string;
};
export type SectorResourceSelection = {
sector_slug: string;
key: string;
group_id?: string | null;
group_name?: string | null;
area_id: string;
title: string;
location_label: string | null;
locations_text: string;
value_chain: string;
source_sha256: string;
reference_date: string;
href: string;
};
export type SectorResourceData = {
sector_slug: string;
title: string;
introduction: string;
group_label: string;
resource_heading: string;
resource_notice: string;
background: {title:string;paragraphs:string[]}[];
source_sha256: string;
reference_date: string;
groups: { id: string; name: string; locations_text: string; location_labels: string[] }[];
areas: SectorResourceArea[];
projects: string[];
indicators: { label: string; value: string; source: string }[];
risks: string[];
sources: string[];
source_links?: {title:string;url:string}[];
};
export default function SectorResourceReference({ data }: { data: SectorResourceData }) {
const url = usePage().url,
query = new URL(url, "https://investmoz.invalid").searchParams;
const [location, setLocation] = useState(query.get("resource_location") ?? ""),
[search, setSearch] = useState(""),
[area, setArea] = useState(query.get("resource_area") ?? ""),
[group, setGroup] = useState(query.get("resource_group") ?? "");
const locations = [
...new Set(
data.groups
.flatMap((m) => m.location_labels)
.concat(data.areas.flatMap((m) => m.location_labels)),
),
].sort((a, b) => a.localeCompare(b, "pt"));
const matches = (x: { location_labels: string[] }, text: string) =>
(!location || x.location_labels.includes(location)) &&
(!search || text.toLocaleLowerCase("pt").includes(search.toLocaleLowerCase("pt")));
const groups = data.groups.filter(
(m) => (!group || m.id === group) && matches(m, m.name + " " + m.locations_text),
),
areas = data.areas.filter(
(a) =>
(!area || a.id === area) &&
(!group || a.group_ids.includes(group)) &&
matches(a, a.title + " " + a.value_chain + " " + a.locations_text),
);
return (
<section
aria-label={data.title}
className="space-y-5 rounded border p-4"
>
<h2 className="text-xl font-semibold">{data.title}</h2>
<p>{data.introduction}</p>
<div className="flex flex-wrap gap-3">
<label>
Localização
<select
className="mt-1 block max-w-full rounded border p-2"
aria-label="Localização sectorial"
value={location}
onChange={(e) => {
setLocation(e.target.value);
setArea("");
}}
>
<option value="">Todas as localizações</option>
{locations.map((p) => (
<option key={p} value={p}>
{p === "Maputo" ? "Maputo — província/cidade a precisar" : p}
</option>
))}
</select>
</label>
<label>
{data.group_label}
<select
className="mt-1 block max-w-full rounded border p-2"
aria-label={data.group_label}
value={group}
onChange={(e) => {
setGroup(e.target.value);
setArea("");
}}
>
<option value="">Todos os grupos</option>
{data.groups.map((m) => (
<option key={m.id} value={m.id}>
{m.name}
</option>
))}
</select>
</label>
<label>
Recurso, local ou actividade
<input
className="mt-1 block max-w-full rounded border p-2"
aria-label="Pesquisar recursos e cadeia de valor"
value={search}
onChange={(e) => {
setSearch(e.target.value);
setArea("");
}}
/>
</label>
{area && (
<button type="button" onClick={() => setArea("")}>
Ver todas as áreas
</button>
)}
</div>
<div>
<h3 className="font-semibold">{data.resource_heading}</h3>
{groups.length ? (
<ul className="space-y-3">
{groups.map((m) => (
<li key={m.id}>
<strong>{m.name}</strong>
<p>{m.locations_text}</p>
</li>
))}
</ul>
) : (
<p>Sem referências para esta pesquisa.</p>
)}
<p className="text-sm">{data.resource_notice}</p>
</div>
<div>
<h3 className="font-semibold">Possibilidades na cadeia de valor</h3>
{areas.map((a) => (
<article key={a.id} className="my-3 space-y-2 rounded border p-3">
<h4 className="font-semibold">{a.title}</h4>
<p>{a.locations_text}</p>
<p>{a.value_chain.replace(/\s*\(indicativo\)/, "")}</p>
<details>
<summary>Evidência e âmbito territorial</summary>
<p>{a.market_evidence}</p>
{a.indicative_location !== a.locations_text && <p>{a.indicative_location}</p>}
<p>Referência de {data.reference_date}; preparação da actividade sujeita ao investimento concreto.</p>
</details>
<Link
className="underline"
href={
"/investimentos/a-partir-do-setor/" + data.sector_slug + "?" +
new URLSearchParams({
resource_area: a.id,
resource_source: data.source_sha256,
...(group ? { resource_group: group } : {}),
...(location ? { resource_location: location } : {}),
})
}
>
Preparar uma ideia nesta cadeia de valor
</Link>
</article>
))}
{!areas.length && <p>Sem áreas de investimento para esta selecção.</p>}
</div>
<details>
<summary>Projectos de referência</summary>
<ul>
{data.projects.map((p, i) => (
<li key={i}>{p}</li>
))}
</ul>
</details>
{!!data.indicators.length && (
<details>
<summary>Produção e recursos por período</summary>
{data.indicators.map((s, i) => (
<article key={i}>
<h4>{s.label}</h4>
<p>
{s.value.replace(
"Não encontrei dados de produção das outras gemas.",
"Outras gemas: sem valores apresentados.",
)}
</p>
<p>Fonte: {s.source}</p>
</article>
))}
<p>
Produção, capacidade, recursos e previsões são grandezas distintas. Os valores
conservam os períodos e divergências das fontes.
</p>
</details>
)}
<details>
<summary>Riscos a considerar</summary>
<ul>
{data.risks.map((r, i) => (
<li key={i}>{r}</li>
))}
</ul>
</details>
{data.background.map(section=><details key={section.title}><summary>{section.title}</summary>{section.paragraphs.map((text,i)=><p key={i}>{text}</p>)}{section.title.startsWith("Enquadramento")&&<p>Referência documental; não activa benefícios fiscais, licenças ou direitos no portal.</p>}</details>)}
<details>
<summary>Fontes e data</summary>
<p>Referências reunidas em {data.reference_date}.</p>
<ul>
{data.sources.map((s, i) => (
<li key={i}>{data.source_links?.find(link=>link.title===s)?.url ? <a className="underline" href={data.source_links.find(link=>link.title===s)!.url} target="_blank" rel="noopener noreferrer">{s}</a> : s}</li>
))}
</ul>
</details>
</section>
);
}
+100 -4
View File
@@ -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;
}
+27 -26
View File
@@ -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',
+35 -26
View File
@@ -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>
+124 -85
View File
@@ -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>
@@ -1,3 +1,4 @@
import type { SectorResourceSelection } from '@/components/sector-resource-reference';
import { createOperationKey } from '@/lib/operation-key';
import { Head, Link, router, useForm } from '@inertiajs/react';
import { useState } from 'react';
@@ -19,11 +20,13 @@ export default function InvestmentContexts({
contexts,
contentSource,
sectorSource,
resourceSource,
zoneSource,
companyComparisonAnswers,
guideSelection,
}: {
contexts: InvestmentContext[];
resourceSource?: SectorResourceSelection;
companyComparisonAnswers?: CompanyAnswers | null;
guideSelection?: GuideSelection | null;
contentSource?: LegacyArticle;
@@ -46,6 +49,14 @@ export default function InvestmentContexts({
reuse_consent: false,
content_resource_id: contentSource?.id,
sector_slug: sectorSource?.slug,
sector_reference: resourceSource
? {
area_id: resourceSource.area_id,
group_id: resourceSource.group_id,
source_sha256: resourceSource.source_sha256,
location_label: resourceSource.location_label,
}
: undefined,
...(companyComparisonAnswers
? { company_comparison: companyComparisonAnswers }
: {}),
@@ -87,6 +98,14 @@ export default function InvestmentContexts({
publicada.
</p>
)}
{resourceSource && (
<section className="rounded border p-4">
<h2>{resourceSource.title}</h2>
<p>{resourceSource.location_label ?? resourceSource.locations_text}</p>
<p>{resourceSource.value_chain}</p>
<p>Esta referência será guardada apenas no dossier que escolher.</p>
</section>
)}
{contentSource && (
<p>
Preparar a partir de{' '}
@@ -155,6 +174,7 @@ export default function InvestmentContexts({
{
version: item.version,
sector_slug: sectorSource.slug,
...(resourceSource ? { sector_reference: form.data.sector_reference } : {}),
},
)
}
+17
View File
@@ -1,3 +1,4 @@
import type { SectorResourceSelection } from '@/components/sector-resource-reference';
import {
financialCategories,
financialTypes,
@@ -44,6 +45,7 @@ type Step = {
source_paragraph: number;
};
type Path = {
sector_references?: SectorResourceSelection[];
funding_selections?: {
id: string;
title: string;
@@ -639,6 +641,21 @@ export default function InvestmentPath(path: Path) {
)}
</label>
)}
{!!path.sector_references?.length && (
<section className="space-y-3 rounded border p-4">
<h2 className="text-lg font-semibold">Sectores e cadeias de valor</h2>
{path.sector_references.map((reference) => (
<article key={reference.key}>
<h3>{reference.title}</h3>
<p>{reference.location_label ?? reference.locations_text}</p>
<p>{reference.value_chain}</p>
<Link className="underline" href={reference.href}>
Consultar a referência mineral
</Link>
</article>
))}
</section>
)}
<ContextDocuments
source={path.investment_context?.selection.opportunity}
/>