724 lines
26 KiB
TypeScript
724 lines
26 KiB
TypeScript
import type { ProfileAccess } from '@/types/profile-access';
|
|
import type { OpportunityManagementAccess } from '@/types/opportunity-management';
|
|
|
|
export type PortalNavItem = {
|
|
id: string;
|
|
label: string;
|
|
href?: string;
|
|
children?: PortalNavItem[];
|
|
};
|
|
export type PortalNavAccess = {
|
|
contentManagementHref?: string | null;
|
|
contentManagementAccess?: {
|
|
opportunities?: { index: string; create?: string | null };
|
|
};
|
|
scopedAdministration?: { label: string; href: string }[];
|
|
procurement?: boolean;
|
|
funding?: boolean;
|
|
authenticated: boolean;
|
|
manager: boolean;
|
|
businesses?: boolean;
|
|
businessReviewUrl?: string | null;
|
|
education?: boolean;
|
|
ai?: boolean;
|
|
legacyContent?: boolean;
|
|
profileAccess?: ProfileAccess | null;
|
|
opportunityManagement?: OpportunityManagementAccess | null;
|
|
context?: {
|
|
id: string;
|
|
simulator?: string | null;
|
|
land?: string | null;
|
|
desk?: string | null;
|
|
guidance?: string | null;
|
|
};
|
|
};
|
|
const link = (id: string, label: string, href: string): PortalNavItem => ({
|
|
id,
|
|
label,
|
|
href,
|
|
});
|
|
/** Display the existing server decision; a generic manager flag cannot override a scoped denial. */
|
|
export function canShowLegacyManagement(auth?: {
|
|
user?: unknown;
|
|
canManagePortal?: boolean;
|
|
legacyManagementAccess?: boolean;
|
|
}): boolean {
|
|
return (
|
|
!!auth?.user &&
|
|
(auth.legacyManagementAccess ?? auth.canManagePortal ?? false)
|
|
);
|
|
}
|
|
|
|
/** One route catalogue for the public shell and authenticated pages. */
|
|
export function legacyPortalNavigation(
|
|
access: PortalNavAccess,
|
|
): PortalNavItem[] {
|
|
const opportunities = access.manager ? access.opportunityManagement : null;
|
|
const groups: PortalNavItem[] = [
|
|
{
|
|
id: 'explore',
|
|
label: 'Explorar investimentos',
|
|
children: [
|
|
link('opportunities', 'Oportunidades', '/oportunidades'),
|
|
link('sectors', 'Sectores', '/sectores'),
|
|
...(access.legacyContent
|
|
? [
|
|
link(
|
|
'legacy-agronegocio',
|
|
'Explorar agronegócio',
|
|
'/explorar/agronegocio',
|
|
),
|
|
]
|
|
: []),
|
|
{
|
|
id: 'structures',
|
|
label: 'Carteira e estruturas',
|
|
children: [
|
|
link(
|
|
'portfolio',
|
|
'Carteira de investimento',
|
|
'/carteira',
|
|
),
|
|
link(
|
|
'projects',
|
|
'Projectos âncora',
|
|
'/carteira?kind=project',
|
|
),
|
|
link('clusters', 'Clusters', '/carteira?kind=cluster'),
|
|
link(
|
|
'corridors',
|
|
'Corredores',
|
|
'/carteira?kind=corridor',
|
|
),
|
|
],
|
|
},
|
|
{
|
|
id: 'zones',
|
|
label: 'Zonas e parques',
|
|
children: [
|
|
link('all-zones', 'Todas as zonas', '/zonas'),
|
|
link('zee', 'ZEE', '/zonas?category=zee'),
|
|
link('zfi', 'ZFI', '/zonas?category=zfi'),
|
|
link(
|
|
'parks',
|
|
'Parques industriais',
|
|
'/zonas?physical_category=industrial_park',
|
|
),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'prepare',
|
|
label: 'Preparar o projecto',
|
|
children: [
|
|
link('roadmap', 'Como investir', '/como-investir'),
|
|
link('dossier', 'Preparar dossier', '/simulador-fiscal'),
|
|
link('simulator', 'Simulador', '/simulador-fiscal'),
|
|
link('land', 'Terra e DUAT', '/terra-duat'),
|
|
...(access.education
|
|
? [
|
|
link(
|
|
'education',
|
|
'Educação e competências',
|
|
'/educacao',
|
|
),
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
{
|
|
id: 'support',
|
|
label: 'Serviços e apoio',
|
|
children: [
|
|
link('desk', 'Balcão Virtual', '/balcao-virtual'),
|
|
link(
|
|
'help',
|
|
'Apoio ao investidor',
|
|
access.context?.guidance || '/apoio',
|
|
),
|
|
link('documents', 'Documentos e formulários', '/documentos'),
|
|
link('law', 'Legislação', '/legislacao'),
|
|
],
|
|
},
|
|
{
|
|
id: 'partnerships',
|
|
label: 'Parcerias',
|
|
children: [
|
|
link('exchange', 'Bolsa de Parcerias', '/parcerias'),
|
|
link('businesses', 'Negócios e participações', '/negocios'),
|
|
...(access.authenticated
|
|
? [
|
|
link(
|
|
'my-partnerships',
|
|
'As minhas parcerias',
|
|
'/parcerias/painel',
|
|
),
|
|
]
|
|
: []),
|
|
...(access.authenticated && access.businesses
|
|
? [
|
|
link(
|
|
'my-businesses',
|
|
'Os meus negócios',
|
|
'/investidor/negocios',
|
|
),
|
|
]
|
|
: []),
|
|
...(access.authenticated &&
|
|
access.businesses &&
|
|
access.businessReviewUrl
|
|
? [
|
|
link(
|
|
'review-businesses',
|
|
'Revisão de negócios',
|
|
access.businessReviewUrl,
|
|
),
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
{
|
|
id: 'account',
|
|
label: 'A minha área',
|
|
children: access.authenticated
|
|
? [
|
|
link(
|
|
'dashboard',
|
|
opportunities
|
|
? 'Painel da gestão'
|
|
: 'Dossiers e percursos',
|
|
opportunities ? '/dashboard' : '/investidor',
|
|
),
|
|
link(
|
|
'profile',
|
|
'Perfil e empresas',
|
|
'/investidor/profile',
|
|
),
|
|
link('dossiers', 'Dossiers e percursos', '/investidor'),
|
|
link(
|
|
'requests',
|
|
'Pedidos e tarefas de acompanhamento',
|
|
'/balcao-virtual',
|
|
),
|
|
link(
|
|
'interests',
|
|
'Manifestações e favoritos',
|
|
'/investidor/manifestacoes',
|
|
),
|
|
link(
|
|
'notifications',
|
|
'Notificações',
|
|
'/investidor/notifications',
|
|
),
|
|
]
|
|
: [
|
|
link('login', 'Entrar', '/login'),
|
|
link('register', 'Criar conta', '/register'),
|
|
],
|
|
},
|
|
];
|
|
if (access.manager)
|
|
groups.push({
|
|
id: 'management',
|
|
label: 'Gestão',
|
|
children: [
|
|
link(
|
|
'manage-interests',
|
|
'Pedidos de interesse',
|
|
'/management/interests',
|
|
),
|
|
link('crm', 'CRM interno', '/management/crm/leads'),
|
|
...(opportunities
|
|
? [
|
|
link(
|
|
'manage-opportunities',
|
|
opportunities.manage.label,
|
|
opportunities.manage.href,
|
|
),
|
|
...(opportunities.installed
|
|
? [
|
|
link(
|
|
'create-opportunity',
|
|
opportunities.create.label,
|
|
opportunities.create.href,
|
|
),
|
|
]
|
|
: []),
|
|
]
|
|
: []),
|
|
{
|
|
id: 'manage-catalogue',
|
|
label: 'Catálogo de investimento',
|
|
children: [
|
|
link(
|
|
'manage-sectors',
|
|
'Sectores',
|
|
'/management/sectors',
|
|
),
|
|
...(!opportunities
|
|
? [
|
|
link(
|
|
'manage-opportunities',
|
|
'Oportunidades',
|
|
'/management/opportunities',
|
|
),
|
|
]
|
|
: []),
|
|
link(
|
|
'manage-projects',
|
|
'Projectos',
|
|
'/management/projects',
|
|
),
|
|
link(
|
|
'manage-clusters',
|
|
'Clusters',
|
|
'/management/clusters',
|
|
),
|
|
],
|
|
},
|
|
link(
|
|
'manage-portfolio',
|
|
'Carteira e estruturas',
|
|
'/management/portfolio',
|
|
),
|
|
link(
|
|
'manage-documents',
|
|
'Acervo documental',
|
|
'/management/portal-services',
|
|
),
|
|
link(
|
|
'manage-zones',
|
|
'Zonas e parques',
|
|
'/management/investment/zones',
|
|
),
|
|
link(
|
|
'manage-territory',
|
|
'Associações territoriais',
|
|
'/management/territory-associations',
|
|
),
|
|
link(
|
|
'manage-references',
|
|
'Referências e formulários',
|
|
'/management/investment/references',
|
|
),
|
|
link(
|
|
'desk-installation',
|
|
'Estado do Balcão e orientação',
|
|
'/management/balcao-install',
|
|
),
|
|
link(
|
|
'exchange-installation',
|
|
'Estado da Bolsa de Parcerias',
|
|
'/management/partnerships-install',
|
|
),
|
|
...(access.ai
|
|
? [
|
|
link(
|
|
'manage-ai',
|
|
'Assistência interna',
|
|
'/management/ai',
|
|
),
|
|
]
|
|
: []),
|
|
...(access.education
|
|
? [
|
|
link(
|
|
'manage-education',
|
|
'Educação e competências',
|
|
'/management/education',
|
|
),
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
if (access.authenticated && access.profileAccess) {
|
|
const profile = access.profileAccess;
|
|
groups.push({
|
|
id: 'profile-opportunities',
|
|
label: 'Perfil e oportunidades',
|
|
children: [
|
|
...(profile.mine
|
|
? [
|
|
link(
|
|
'my-opportunities',
|
|
'Minhas oportunidades',
|
|
profile.mine,
|
|
),
|
|
]
|
|
: []),
|
|
...(profile.create
|
|
? [
|
|
link(
|
|
'new-my-opportunity',
|
|
'Nova oportunidade',
|
|
profile.create,
|
|
),
|
|
]
|
|
: []),
|
|
link(
|
|
'promoter-profile',
|
|
profile.mine
|
|
? 'Autorizações de Promotor'
|
|
: 'Solicitar perfil de Promotor',
|
|
profile.request,
|
|
),
|
|
...(!profile.mine && profile.review
|
|
? [
|
|
link(
|
|
'profile-review',
|
|
'Revisão de oportunidades',
|
|
profile.review,
|
|
),
|
|
]
|
|
: []),
|
|
...(profile.audit
|
|
? [
|
|
link(
|
|
'profile-audit',
|
|
'Auditoria de acessos',
|
|
profile.audit,
|
|
),
|
|
]
|
|
: []),
|
|
...(profile.administration
|
|
? [
|
|
link(
|
|
'profile-administration',
|
|
'Gestão de perfis',
|
|
profile.administration,
|
|
),
|
|
link(
|
|
'profile-installation',
|
|
'Instalação dos perfis 040000',
|
|
'/management/profile-install',
|
|
),
|
|
]
|
|
: []),
|
|
],
|
|
});
|
|
}
|
|
return groups;
|
|
}
|
|
|
|
export function currentNavigationGroup(
|
|
groups: PortalNavItem[],
|
|
url: string,
|
|
): string | null {
|
|
const current = new URL(url, 'https://portal.invalid');
|
|
let match: { group: string; score: number } | null = null;
|
|
const visit = (items: PortalNavItem[], group: string) =>
|
|
items.forEach((item) => {
|
|
if (item.children) visit(item.children, group);
|
|
if (!item.href) return;
|
|
const target = new URL(item.href, current.origin);
|
|
if (target.hash && target.hash !== current.hash) return;
|
|
if (
|
|
current.pathname !== target.pathname &&
|
|
!current.pathname.startsWith(target.pathname + '/')
|
|
)
|
|
return;
|
|
for (const [key, value] of target.searchParams)
|
|
if (current.searchParams.get(key) !== value) return;
|
|
const score =
|
|
target.pathname.length + target.searchParams.size * 100;
|
|
if (!match || score > match.score) match = { group, score };
|
|
});
|
|
for (const group of groups) visit(group.children ?? [], group.id);
|
|
return (match as { group: string; score: number } | null)?.group ?? null;
|
|
}
|
|
|
|
/** Select one destination, preferring a matching catalogue filter. */
|
|
export function currentNavigationItem(
|
|
groups: PortalNavItem[],
|
|
url: string,
|
|
): string | null {
|
|
const current = new URL(url, 'https://portal.invalid');
|
|
const best = { id: null as string | null, score: -1 };
|
|
const visit = (items: PortalNavItem[]) => {
|
|
for (const item of items) {
|
|
if (item.children) visit(item.children);
|
|
if (!item.href) continue;
|
|
const target = new URL(item.href, current.origin);
|
|
if (target.hash && target.hash !== current.hash) continue;
|
|
if (current.pathname !== target.pathname) continue;
|
|
if (
|
|
[...target.searchParams].some(
|
|
([key, value]) => current.searchParams.get(key) !== value,
|
|
)
|
|
)
|
|
continue;
|
|
const score = target.searchParams.size;
|
|
if (score > best.score) {
|
|
best.id = item.id;
|
|
best.score = score;
|
|
}
|
|
}
|
|
};
|
|
for (const group of groups) visit(group.children ?? []);
|
|
return best.id;
|
|
}
|
|
|
|
/** Investor organization; existing capability filters still determine private destinations. */
|
|
export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
|
|
const previous = legacyPortalNavigation({
|
|
...access,
|
|
manager: access.authenticated && access.manager,
|
|
profileAccess: access.authenticated ? access.profileAccess : null,
|
|
});
|
|
const workHub = access.authenticated ? access.contentManagementHref : null;
|
|
const workOpportunities = access.authenticated
|
|
? access.contentManagementAccess?.opportunities
|
|
: undefined;
|
|
const workCreate = access.authenticated
|
|
? access.profileAccess?.create ||
|
|
workOpportunities?.create ||
|
|
(access.manager && access.opportunityManagement?.installed
|
|
? access.opportunityManagement.create.href
|
|
: 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: [
|
|
link('locations', 'Localizações', '/oportunidades?view=map'),
|
|
...(access.funding
|
|
? [
|
|
link(
|
|
'funding',
|
|
'Fundos e programas de apoio',
|
|
'/fundos',
|
|
),
|
|
...(access.authenticated
|
|
? [
|
|
link(
|
|
'funding-saved',
|
|
'Programas que guardei',
|
|
'/fundos/guardados',
|
|
),
|
|
]
|
|
: []),
|
|
]
|
|
: []),
|
|
{
|
|
id: 'related-catalogues',
|
|
label: 'Catálogos relacionados',
|
|
children: [
|
|
link(
|
|
'portfolio',
|
|
'Carteira, clusters e corredores',
|
|
'/carteira',
|
|
),
|
|
link('zones', 'Zonas e parques', '/zonas'),
|
|
...(access.businesses
|
|
? [
|
|
link(
|
|
'business-catalogue',
|
|
'Negócios',
|
|
'/negocios',
|
|
),
|
|
]
|
|
: []),
|
|
...(access.education
|
|
? [
|
|
link(
|
|
'education-catalogue',
|
|
'Oportunidades na educação',
|
|
'/educacao',
|
|
),
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: 'realization',
|
|
label: 'Realização',
|
|
children: [
|
|
link('start', 'Iniciar investimento', '/investimentos/iniciar'),
|
|
link(
|
|
'licensing',
|
|
'Licenciamento',
|
|
access.context
|
|
? `/investimentos/${access.context.id}#S08`
|
|
: '/investimentos/iniciar?step=S08',
|
|
),
|
|
link(
|
|
'land',
|
|
'Acesso à terra',
|
|
access.context?.land || '/terra-duat',
|
|
),
|
|
],
|
|
},
|
|
{
|
|
id: 'support',
|
|
label: 'Suporte',
|
|
children: [
|
|
link('documents', 'Biblioteca de documentos', '/documentos'),
|
|
link('legislation', 'Legislação', '/legislacao'),
|
|
link(
|
|
'services',
|
|
'Serviços ao investidor',
|
|
access.context?.desk || '/balcao-virtual',
|
|
),
|
|
link(
|
|
'followup',
|
|
'Acompanhamento do projecto',
|
|
access.context
|
|
? `/investimentos/${access.context.id}`
|
|
: '/investimentos',
|
|
),
|
|
link(
|
|
'help',
|
|
'Apoio ao investidor',
|
|
access.context?.guidance || '/apoio',
|
|
),
|
|
],
|
|
},
|
|
|
|
{
|
|
id: 'work',
|
|
label: 'Área de trabalho',
|
|
children: [
|
|
link(
|
|
'work-entry',
|
|
workHub
|
|
? 'Gestão de conteúdos e oportunidades'
|
|
: 'Acesso à gestão',
|
|
workHub || '/acesso-gestao',
|
|
),
|
|
...(workCreate
|
|
? [
|
|
link(
|
|
'prepare-analysis',
|
|
'Preparar análise e ideia',
|
|
workCreate + '?analysis=proactive',
|
|
),
|
|
]
|
|
: []),
|
|
...(workOpportunities
|
|
? [
|
|
link(
|
|
'work-opportunities',
|
|
'Oportunidades em preparação',
|
|
workOpportunities.index,
|
|
),
|
|
]
|
|
: []),
|
|
...previous
|
|
.filter((group) =>
|
|
['management', 'profile-opportunities'].includes(
|
|
group.id,
|
|
),
|
|
)
|
|
.flatMap((group) => group.children ?? []),
|
|
...(access.authenticated && access.scopedAdministration?.length
|
|
? [
|
|
{
|
|
id: 'work-administration',
|
|
label: 'Administração',
|
|
children: access.scopedAdministration.map(
|
|
(item, index) =>
|
|
link(
|
|
'scoped-' + index,
|
|
item.label,
|
|
item.href,
|
|
),
|
|
),
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
...previous.filter((group) => group.id === 'account'),
|
|
...(access.authenticated &&
|
|
access.businesses &&
|
|
access.businessReviewUrl
|
|
? [
|
|
{
|
|
id: 'business-work',
|
|
label: 'Revisão de negócios',
|
|
children: [
|
|
link(
|
|
'review-businesses',
|
|
'Revisão de negócios',
|
|
access.businessReviewUrl,
|
|
),
|
|
],
|
|
},
|
|
]
|
|
: []),
|
|
...(access.authenticated
|
|
? [
|
|
{
|
|
id: 'personal-activity',
|
|
label: 'A minha atividade',
|
|
children: [
|
|
link(
|
|
'my-partnerships',
|
|
'As minhas parcerias',
|
|
'/parcerias/painel',
|
|
),
|
|
...(access.businesses
|
|
? [
|
|
link(
|
|
'my-businesses',
|
|
'Os meus negócios',
|
|
'/investidor/negocios',
|
|
),
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
]
|
|
: []),
|
|
]);
|
|
}
|
|
|
|
/** One menu entry per exact destination; filters and anchors retain their meaning. */
|
|
export function uniqueNavigationDestinations(
|
|
groups: PortalNavItem[],
|
|
): PortalNavItem[] {
|
|
const seen = new Set<string>();
|
|
const visit = (items: PortalNavItem[]): PortalNavItem[] =>
|
|
items.flatMap((item) => {
|
|
const children = item.children ? visit(item.children) : undefined;
|
|
if (children && !children.length && !item.href) return [];
|
|
if (item.href) {
|
|
const url = new URL(item.href, 'https://portal.invalid');
|
|
url.searchParams.sort();
|
|
const identity = url.href;
|
|
if (seen.has(identity)) return [];
|
|
seen.add(identity);
|
|
}
|
|
return [{ ...item, ...(children ? { children } : {}) }];
|
|
});
|
|
return visit(groups);
|
|
}
|