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(); 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); }