Merge branch 'main' of https://git.tempo.co.mz/nala/Investidor-V2 into front
This commit is contained in:
@@ -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,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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 } : {}),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user