From 96f510965f70581a4b41dd94463f598b77dd7cf5 Mon Sep 17 00:00:00 2001 From: nala Date: Sat, 14 Nov 2026 12:25:27 +0200 Subject: [PATCH] Separate dashboard functions from grouped administration operations --- app/Http/Middleware/HandleInertiaRequests.php | 1 + app/Services/ContentManagement.php | 50 ++++-- .../unification/traceability-and-manual.md | 16 ++ resources/js/components/app-sidebar.tsx | 3 + .../js/components/portal-grouped-menu.tsx | 2 + .../components/portal-sidebar-navigation.tsx | 5 +- resources/js/lib/portal-navigation.ts | 166 ++++++++++++------ resources/js/pages/dashboard.tsx | 36 +++- .../pages/management/editorial-workflow.tsx | 2 +- tests/Feature/EditorialDashboardBuildTest.php | 3 + tests/Feature/EditorialNativeNoticeTest.php | 10 +- .../dashboard-navigation-sections.test.mjs | 19 ++ .../navigation-sidebar-render.test.mjs | 2 +- tests/frontend/portal-navigation.test.mjs | 25 ++- 14 files changed, 259 insertions(+), 81 deletions(-) create mode 100644 tests/frontend/dashboard-navigation-sections.test.mjs diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 680003a2..73a4dd28 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -57,6 +57,7 @@ class HandleInertiaRequests extends Middleware return [ ...parent::share($request), 'name' => config('app.name'), + 'administrationNavigation' => app(ContentManagement::class)->administrationNavigation($request->user(), $contentManagementAccess), 'dashboardNavigation' => app(ContentManagement::class)->dashboardNavigation($request->user(), $contentManagementAccess), 'procurementEnabled' => app(Procurement::class)->available(), 'fundingEnabled' => app(FundingProgrammes::class)->installed(), diff --git a/app/Services/ContentManagement.php b/app/Services/ContentManagement.php index 3556f4d0..b1701530 100644 --- a/app/Services/ContentManagement.php +++ b/app/Services/ContentManagement.php @@ -54,25 +54,18 @@ final class ContentManagement /** @param array $access * @return list */ - public function dashboardNavigation(?User $actor, array $access): array + public function administrationNavigation(?User $actor, array $access): array { - $workflow = app(PortalEditorialWorkflow::class); - if (! $actor || ! $workflow->available() || ! $workflow->canEnter($actor)) { + if (! $actor || ! $actor->hasVerifiedEmail()) { return []; } - $items = [['label' => 'Visão geral', 'href' => '/management/editorial']]; - $documents = app(DocumentManagementAccess::class); - if ($documents->full($actor) || $documents->editorial($actor)) { - foreach (['article' => 'Artigos', 'legislation' => 'Legislação', 'document' => 'Documentos'] as $kind => $label) { - $items[] = ['label' => $label, 'href' => '/management/editorial?kind='.$kind]; - } - } - foreach (['opportunities' => 'Oportunidades', 'clusters' => 'Competitividade', 'portfolio' => 'Carteira', 'demography' => 'Demografia'] as $key => $label) { + $items = []; + foreach (['documents' => 'Biblioteca e documentos', 'opportunities' => 'Oportunidades', 'clusters' => 'Competitividade', 'portfolio' => 'Carteira', 'demography' => 'Demografia', 'procurement' => 'Concursos e anúncios'] as $key => $label) { if (isset($access[$key]['index'])) { $items[] = ['label' => $label, 'href' => $access[$key]['index']]; } } - if (app(PortalEditorialSubjects::class)->available() && $actor->can('manage-portal')) { + if (app(PortalEditorialSubjects::class)->available() && $actor->can('manage-portal') && ! app(PortalProfileAccess::class)->has($actor, 'auditor')) { $items[] = ['label' => 'Estruturas e relações', 'href' => '/management/editorial/subjects']; } $native = app(PortalEditorialNativeNotices::class); @@ -83,7 +76,38 @@ final class ContentManagement $items[] = ['label' => 'Zonas e indicadores', 'href' => '/management/investment/zones']; } - $items[] = ['label' => 'Notificações', 'href' => '/management/editorial#notificacoes']; + return $items; + } + + /** @param array $access + * @return list + */ + public function dashboardNavigation(?User $actor, array $access): array + { + if (! $actor || ! $actor->hasVerifiedEmail() || (! $actor->can('manage-portal') && $access === [] && ! app(ScopedPortalAdministration::class)->administrative($actor))) { + return []; + } + $items = [['label' => 'Visão geral', 'href' => '/dashboard'], ['label' => 'O meu trabalho', 'href' => '/dashboard#meu-trabalho']]; + if ($actor->can('manage-portal') && ! app(PortalProfileAccess::class)->has($actor, 'auditor')) { + foreach (['manifestacoes' => 'Manifestações de interesse', 'indicadores-oportunidades' => 'Indicadores de oportunidades', 'pedidos-recentes' => 'Pedidos recentes', 'accoes-rapidas' => 'Acções rápidas'] as $anchor => $label) { + $items[] = ['label' => $label, 'href' => '/dashboard#'.$anchor]; + } + $items[] = ['label' => 'Indicadores de utilização', 'href' => '/management/product-usage']; + } + if (! $actor->can('manage-portal') && app(InvestorPath::class)->available()) { + $items[] = ['label' => 'Os meus investimentos', 'href' => '/dashboard#meus-investimentos']; + } + $workflow = app(PortalEditorialWorkflow::class); + if ($workflow->available() && $workflow->canEnter($actor)) { + $items[] = ['label' => 'Circuito editorial', 'href' => '/management/editorial']; + $documents = app(DocumentManagementAccess::class); + if ($documents->full($actor) || $documents->editorial($actor)) { + foreach (['article' => 'Revisão de artigos', 'legislation' => 'Revisão de legislação', 'document' => 'Revisão de documentos'] as $kind => $label) { + $items[] = ['label' => $label, 'href' => '/management/editorial?kind='.$kind]; + } + } + $items[] = ['label' => 'Notificações editoriais', 'href' => '/management/editorial#notificacoes']; + } return $items; } diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index d5e3ef82..646f972e 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -1,3 +1,19 @@ +## 14/11/2026 — correcção da hierarquia Administração → Dashboard + +Clarificação expressa da Tempo: agrupar **todas as operações administrativas em Administração** e reservar **Dashboard** às funções dos painéis. A apresentação anterior misturava os cadastros do circuito editorial com o dashboard principal e conservava atalhos administrativos dentro de Mais. Esta correcção não acrescenta funcionalidades ou permissões. + +Hierarquia comum ao cabeçalho e à navegação lateral, com grupos omitidos quando não têm destinos autorizados: + +- **Administração → Dashboard:** Visão geral do painel `/dashboard`; O meu trabalho; manifestações, indicadores, pedidos recentes e acções rápidas quando o painel administrativo já os permite; circuito editorial, filas de revisão e notificações segundo os âmbitos existentes. Os investimentos próprios mantêm o contexto do painel aplicável. Os filtros e âncoras seleccionam a função actual. +- **Administração → Conteúdos e oportunidades:** gestão de conteúdos, documentos, oportunidades, preparação de análises e concursos autorizados. +- **Administração → Cadastros e investimento:** sectores, projectos, clusters, carteira, estruturas/relações, demografia, território, zonas, referências e educação, conforme a capacidade existente. +- **Administração → Pedidos e acompanhamento:** interesses, CRM e atribuições autorizadas. +- **Administração → Acessos e configuração:** gestão/auditoria de perfis e configurações já autorizadas. + +Os cadastros deixam de figurar dentro de Dashboard; atalhos administrativos deixam de estar dispersos em Mais. Os destinos anteriores são preservados uma vez, incluindo funções de perfis antes em Área de trabalho. Funções pessoais e acessos públicos conservam a sua área. O painel `/dashboard` é a visão geral; `/management/editorial` volta a identificar-se como **Circuito editorial** e mantém os cartões/revisões existentes. Não se importam estados a partir de componentes de página; a correcção do manifesto permanece intacta. + +QA: navegação/cabeçalho/sidebar **25 testes**, apresentação/secções **4**, acesso/inbox **4/35**, abertura HTML autenticada dos dois painéis com build real **1/11**, análise estática, tipos e compilação aprovados. Provas em `outputs/administration-dashboard-hierarchy-20261114`. Testa-se a hierarquia exacta, ausência de cadastros dentro de Dashboard, remoção das operações administrativas de Mais, preservação de destinos e âncoras reais. Instalação correctiva pelo processo Git existente com backup/restauro em curso; não declarar concluída até conferir a revisão servida. A conferência visual continua pendente pelo conector indisponível. + ## 14/11/2026 — erro 500 corrigido e correcção instalada em dd20e747 A revisão `dd20e747036d45892faf3273a44e385132668ee4` foi promovida pelo executor Git para reparar a entrega do menu/dashboard. O manifesto servido volta a conter `resources/js/pages/management/editorial-workflow.tsx`. A geração do HTML completo no servidor com o template/manifesto reais e conteúdo exclusivamente sintético passou (200); o respectivo ficheiro compilado respondeu 200 por HTTPS. Não se simularam credenciais ou sessões privadas no destino. A abertura HTTP autenticada com o build real passou em QA (1/8), além dos 12 testes frontend focados. O executor aprovou **1911 testes de interface**, incluindo o novo controlo do manifesto, 132 unitários/611 asserções, estrutura 1/3106, tipos e compilação. diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index d1c56505..87f37007 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -33,6 +33,7 @@ export function AppSidebar() { profileAccess, scopedAdministration, dashboardNavigation, + administrationNavigation, } = usePage<{ auth: Auth; existingBusinessEnabled?: boolean; @@ -43,6 +44,7 @@ export function AppSidebar() { contentManagementHref?: string | null; scopedAdministration?: { label: string; href: string }[]; dashboardNavigation?: { label: string; href: string }[]; + administrationNavigation?: { label: string; href: string }[]; contentManagementAccess?: { opportunities?: { index: string; create?: string | null }; }; @@ -59,6 +61,7 @@ export function AppSidebar() { contentManagementAccess, scopedAdministration, dashboardNavigation, + administrationNavigation, legacyContent: currentPage.props.legacyContentEnabled as | boolean | undefined, diff --git a/resources/js/components/portal-grouped-menu.tsx b/resources/js/components/portal-grouped-menu.tsx index 699e90c4..89115673 100644 --- a/resources/js/components/portal-grouped-menu.tsx +++ b/resources/js/components/portal-grouped-menu.tsx @@ -54,6 +54,7 @@ export function PortalGroupedMenu() { opportunityManagement?: OpportunityManagementAccess | null; scopedAdministration?: { label: string; href: string }[]; dashboardNavigation?: { label: string; href: string }[]; + administrationNavigation?: { label: string; href: string }[]; contentManagementHref?: string | null; contentManagementAccess?: { opportunities?: { index: string; create?: string | null }; @@ -81,6 +82,7 @@ export function PortalGroupedMenu() { contentManagementAccess: props.contentManagementAccess, scopedAdministration: props.scopedAdministration, dashboardNavigation: props.dashboardNavigation, + administrationNavigation: props.administrationNavigation, }); const current = currentNavigationGroup(groups, url); const currentItem = currentNavigationItem(groups, url); diff --git a/resources/js/components/portal-sidebar-navigation.tsx b/resources/js/components/portal-sidebar-navigation.tsx index b14707f9..4df3a194 100644 --- a/resources/js/components/portal-sidebar-navigation.tsx +++ b/resources/js/components/portal-sidebar-navigation.tsx @@ -22,8 +22,11 @@ export function PortalSidebarNavigation({ }) { const active = currentNavigationItem(groups, url); const ordered = [ + ...groups.filter((group) => group.id === 'administration'), ...groups.filter((group) => group.id === 'work'), - ...groups.filter((group) => group.id !== 'work'), + ...groups.filter( + (group) => group.id !== 'work' && group.id !== 'administration', + ), ]; const links = (items: PortalNavItem[], depth = 0): ReactNode => items.map((item) => diff --git a/resources/js/lib/portal-navigation.ts b/resources/js/lib/portal-navigation.ts index 00827565..b974aba5 100644 --- a/resources/js/lib/portal-navigation.ts +++ b/resources/js/lib/portal-navigation.ts @@ -14,6 +14,7 @@ export type PortalNavAccess = { }; scopedAdministration?: { label: string; href: string }[]; dashboardNavigation?: { label: string; href: string }[]; + administrationNavigation?: { label: string; href: string }[]; procurement?: boolean; funding?: boolean; authenticated: boolean; @@ -422,7 +423,9 @@ export function currentNavigationGroup( for (const [key, value] of target.searchParams) if (current.searchParams.get(key) !== value) return; const score = - target.pathname.length + target.searchParams.size * 100; + target.pathname.length + + target.searchParams.size * 100 + + (target.hash ? 10000 : 0); if (!match || score > match.score) match = { group, score }; }); for (const group of groups) visit(group.children ?? [], group.id); @@ -449,7 +452,7 @@ export function currentNavigationItem( ) ) continue; - const score = target.searchParams.size; + const score = target.searchParams.size + (target.hash ? 10000 : 0); if (score > best.score) { best.id = item.id; best.score = score; @@ -479,43 +482,125 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] { : null) : null; const dashboardActivities = access.authenticated - ? [ - ...(access.dashboardNavigation ?? []), - ...previous + ? access.dashboardNavigation?.length + ? access.dashboardNavigation + : previous .filter((group) => group.id === 'account') .flatMap((group) => group.children ?? []) .filter((item) => item.href === '/dashboard') - .map((item) => ({ - label: 'Painel operacional', - href: item.href!, - })), - ] + .map((item) => ({ label: 'Visão geral', href: item.href! })) : []; - const administration = access.authenticated + const flatten = (items: PortalNavItem[]): PortalNavItem[] => + items.flatMap((item) => + item.children ? flatten(item.children) : [item], + ); + const profileAdministration = new Set([ + 'profile-administration', + 'profile-installation', + 'profile-audit', + ]); + const candidates = access.authenticated ? [ - ...(dashboardActivities.length + ...(workHub + ? [link('work-entry', 'Gestão de conteúdos', workHub)] + : []), + ...(workCreate ? [ - { - id: 'collaborative-dashboard', - label: 'Dashboard', - children: dashboardActivities.map((item, index) => - link( - 'dashboard-activity-' + index, - item.label, - item.href, - ), - ), - }, + link( + 'prepare-analysis', + 'Preparar análise e ideia', + workCreate + '?analysis=proactive', + ), ] : []), - ...previous - .filter((group) => group.id === 'management') - .flatMap((group) => group.children ?? []), + ...(workOpportunities + ? [ + link( + 'work-opportunities', + 'Oportunidades em preparação', + workOpportunities.index, + ), + ] + : []), + ...(access.administrationNavigation ?? []).map((item, index) => + link( + 'administrative-activity-' + index, + item.label, + item.href, + ), + ), + ...flatten( + previous + .filter((group) => group.id === 'management') + .flatMap((group) => group.children ?? []), + ), ...(access.scopedAdministration ?? []).map((item, index) => link('scoped-' + index, item.label, item.href), ), + ...previous + .filter((group) => group.id === 'profile-opportunities') + .flatMap((group) => group.children ?? []) + .filter((item) => profileAdministration.has(item.id)), ] : []; + const categories = [ + { + id: 'administration-content', + label: 'Conteúdos e oportunidades', + children: [] as PortalNavItem[], + }, + { + id: 'administration-records', + label: 'Cadastros e investimento', + children: [] as PortalNavItem[], + }, + { + id: 'administration-followup', + label: 'Pedidos e acompanhamento', + children: [] as PortalNavItem[], + }, + { + id: 'administration-access', + label: 'Acessos e configuração', + children: [] as PortalNavItem[], + }, + ]; + for (const item of candidates) { + const path = new URL(item.href!, 'https://portal.invalid').pathname; + const category = + /^\/(gestao-conteudos|management\/(portal-services|opportunities|concursos)|minhas-oportunidades)(\/|$)/.test( + path, + ) + ? 0 + : /^\/management\/(interests|crm|balcao\/atribuicoes)(\/|$)/.test( + path, + ) + ? 2 + : profileAdministration.has(item.id) || + /-install$/.test(path) || + path === '/management/ai' + ? 3 + : 1; + categories[category].children.push(item); + } + const administration = [ + ...(dashboardActivities.length + ? [ + { + id: 'collaborative-dashboard', + label: 'Dashboard', + children: dashboardActivities.map((item, index) => + link( + 'dashboard-activity-' + index, + item.label, + item.href, + ), + ), + }, + ] + : []), + ...categories.filter((group) => group.children.length > 0), + ]; return uniqueNavigationDestinations([ { id: 'discovery', @@ -655,34 +740,13 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] { 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, - ), - ] + ...(!workHub + ? [link('work-entry', 'Acesso à gestão', '/acesso-gestao')] : []), ...previous .filter((group) => group.id === 'profile-opportunities') - .flatMap((group) => group.children ?? []), + .flatMap((group) => group.children ?? []) + .filter((item) => !profileAdministration.has(item.id)), ], }, ...previous.filter((group) => group.id === 'account'), diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index b33733e6..0658a3c3 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -60,7 +60,20 @@ export default function Dashboard({

Painel do portal

- +
+ {workQueue ? ( + + ) : ( + <> +

+ O meu trabalho +

+

+ Sem fila de trabalho disponível neste âmbito. +

+ + )} +
{editorialQueue && (
)} -
+

Manifestações de interesse

@@ -149,7 +165,10 @@ export default function Dashboard({ ))}
-
+

{opportunityManagement @@ -190,7 +209,10 @@ export default function Dashboard({ ))}

-
+

Pedidos mais recentes @@ -243,6 +265,7 @@ export default function Dashboard({ )}

) : investorPath ? ( -
+

Os meus investimentos

diff --git a/resources/js/pages/management/editorial-workflow.tsx b/resources/js/pages/management/editorial-workflow.tsx index 37a9bb7f..e8cd3565 100644 --- a/resources/js/pages/management/editorial-workflow.tsx +++ b/resources/js/pages/management/editorial-workflow.tsx @@ -152,7 +152,7 @@ export default function EditorialWorkflow({ }, ].filter((area) => area.entries.some((item) => item.href)); return ( - +

Acompanhe a preparação, revisão e publicação numa única área de trabalho. Seleccione uma actividade para continuar nas tarefas diff --git a/tests/Feature/EditorialDashboardBuildTest.php b/tests/Feature/EditorialDashboardBuildTest.php index f7e21a91..0b2e51b0 100644 --- a/tests/Feature/EditorialDashboardBuildTest.php +++ b/tests/Feature/EditorialDashboardBuildTest.php @@ -25,6 +25,9 @@ final class EditorialDashboardBuildTest extends TestCase $actor = User::factory()->create(); config(['portal.managers' => [$actor->email]]); $this->actingAsWithMfa($actor); + $nativeKey = 'resources/js/pages/dashboard.tsx'; + $this->assertArrayHasKey($nativeKey, $manifest); + $this->get('/dashboard', ['Accept' => 'text/html'])->assertOk()->assertSee($manifest[$nativeKey]['file'], false); foreach (['/management/editorial', '/management/editorial?kind=legislation'] as $url) { $this->get($url, ['Accept' => 'text/html'])->assertOk()->assertSee('data-page=', false)->assertSee($manifest[$key]['file'], false); } diff --git a/tests/Feature/EditorialNativeNoticeTest.php b/tests/Feature/EditorialNativeNoticeTest.php index 6551f26e..fb6feaa0 100644 --- a/tests/Feature/EditorialNativeNoticeTest.php +++ b/tests/Feature/EditorialNativeNoticeTest.php @@ -39,13 +39,17 @@ final class EditorialNativeNoticeTest extends TestCase { $service = app(ContentManagement::class); $items = $service->dashboardNavigation($this->author, $service->access($this->author)); - $this->assertSame(['label' => 'Visão geral', 'href' => '/management/editorial'], $items[0]); - $this->assertContains('/management/clusters', array_column($items, 'href')); + $this->assertSame(['label' => 'Visão geral', 'href' => '/dashboard'], $items[0]); + $this->assertNotContains('/management/clusters', array_column($items, 'href')); + $this->assertContains('/management/clusters', array_column($service->administrationNavigation($this->author, $service->access($this->author)), 'href')); + $this->assertContains('/dashboard#manifestacoes', array_column($items, 'href')); $outsider = User::factory()->create(); $this->assertSame([], $service->dashboardNavigation($outsider, [])); $this->assertSame([], $service->dashboardNavigation(null, [])); config(['editorial_workflow.enabled' => false]); - $this->assertSame([], $service->dashboardNavigation($this->author, $service->access($this->author))); + $after = array_column($service->dashboardNavigation($this->author, $service->access($this->author)), 'href'); + $this->assertContains('/dashboard', $after); + $this->assertNotContains('/management/editorial', $after); } public function test_cluster_events_use_the_common_inbox_and_recheck_current_scope_and_event_integrity(): void diff --git a/tests/frontend/dashboard-navigation-sections.test.mjs b/tests/frontend/dashboard-navigation-sections.test.mjs new file mode 100644 index 00000000..b1a3b813 --- /dev/null +++ b/tests/frontend/dashboard-navigation-sections.test.mjs @@ -0,0 +1,19 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import {createRequire} from 'node:module'; +import React from 'react'; +import {renderToStaticMarkup} from 'react-dom/server'; +import ts from 'typescript'; +const require=createRequire(import.meta.url),module={exports:{}}; +const code=ts.transpileModule(fs.readFileSync(new URL('../../resources/js/pages/dashboard.tsx',import.meta.url),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,jsx:ts.JsxEmit.ReactJSX}}).outputText; +new Function('require','module','exports',code)(name=>name==='@inertiajs/react'?{Head:()=>null,Link:({children,href,...props})=>React.createElement('a',{href,...props},children)}:name==='react/jsx-runtime'?require(name):name==='@/routes'?{dashboard:()=>'/dashboard'}:{default:()=>null},module,module.exports); +test('each manager dashboard menu anchor resolves to a real section',()=>{ + const html=renderToStaticMarkup(React.createElement(module.exports.default,{summary:{requestsByStatus:{},publishedOpportunities:0,draftOpportunities:0,activeSectors:0,recentRequests:[]},workQueue:null})); + for(const id of ['meu-trabalho','manifestacoes','indicadores-oportunidades','pedidos-recentes','accoes-rapidas'])assert.match(html,new RegExp('id="'+id+'"')); + assert.match(html,/Painel do portal/); +}); +test('investor section remains distinct from administrative summary',()=>{ + const html=renderToStaticMarkup(React.createElement(module.exports.default,{summary:null,investorPath:{contexts:[],current:null}})); + assert.match(html,/id="meus-investimentos"/);assert.doesNotMatch(html,/id="manifestacoes"/); +}); diff --git a/tests/frontend/navigation-sidebar-render.test.mjs b/tests/frontend/navigation-sidebar-render.test.mjs index 1aacac06..88902b79 100644 --- a/tests/frontend/navigation-sidebar-render.test.mjs +++ b/tests/frontend/navigation-sidebar-render.test.mjs @@ -110,7 +110,7 @@ void test('sidebar renders every nested authorised destination instead of a firs '/management/economic-references/lot-01', ]) assert.ok(hrefs.includes(href), href); - assert.match(html, /Área de trabalho/); + assert.match(html, /Administração/); }); void test('nested current destination is selected without changing its URL or replacing it with a group root', () => { const { html } = render( diff --git a/tests/frontend/portal-navigation.test.mjs b/tests/frontend/portal-navigation.test.mjs index 324a3658..852c54a9 100644 --- a/tests/frontend/portal-navigation.test.mjs +++ b/tests/frontend/portal-navigation.test.mjs @@ -226,8 +226,8 @@ void test('work area exposes existing content capability even when the old dashb }, }, }); - const work = groups.find((g) => g.id === 'work'); - assert.equal(work.label, 'Área de trabalho'); + const work = groups.find((g) => g.id === 'administration').children.find(g => g.id === 'administration-content'); + assert.equal(work.label, 'Conteúdos e oportunidades'); assert.ok( work.children.some( (i) => @@ -314,7 +314,7 @@ void test('direct analysis and ordinary creation preserve distinct selection sta groups, '/minhas-oportunidades/create?analysis=proactive', ), - 'work', + 'administration', ); assert.equal( currentNavigationItem( @@ -450,15 +450,28 @@ void test('installed concours has its own primary group and preserves public and }); void test('administration owns dashboard activities once and remains hidden without authorised destinations', () => { - const dashboardNavigation = [{label: 'Visão geral', href: '/management/editorial'}, {label: 'Legislação', href: '/management/editorial?kind=legislation'}, {label: 'Competitividade', href: '/management/clusters'}]; - const groups = portalNavigation({authenticated: true, manager: true, dashboardNavigation}); + const dashboardNavigation = [{label: 'Visão geral', href: '/dashboard'}, {label: 'O meu trabalho', href: '/dashboard#meu-trabalho'}, {label: 'Circuito editorial', href: '/management/editorial'}]; + const groups = portalNavigation({authenticated: true, manager: true, dashboardNavigation, administrationNavigation: [{label:'Competitividade',href:'/management/clusters'}]}); const admin = groups.find(g => g.id === 'administration'); const dashboard = admin.children.find(g => g.id === 'collaborative-dashboard'); assert.equal(dashboard.label, 'Dashboard'); + assert.equal(destinations(dashboard.children).includes('/management/clusters'), false); + assert.equal(currentNavigationItem(groups, '/dashboard#meu-trabalho'), 'dashboard-activity-1'); assert.deepEqual(destinations(dashboard.children), dashboardNavigation.map(i => i.href)); - assert.equal(currentNavigationGroup(groups, '/management/editorial?kind=legislation'), 'administration'); + assert.equal(currentNavigationGroup(groups, '/management/editorial'), 'administration'); assert.equal(currentNavigationGroup(groups, '/management/clusters/synthetic/edit'), 'administration'); assert.equal(destinations(groups).filter(h => h === '/management/clusters').length, 1); assert.equal(portalNavigation({authenticated: false, manager: false, dashboardNavigation}).some(g => g.id === 'administration'), false); assert.equal(portalNavigation({authenticated: true, manager: false}).some(g => g.id === 'administration'), false); }); + +void test('administrative operations are grouped outside Dashboard and removed from More/work', () => { + const groups=portalNavigation({authenticated:true,manager:true,contentManagementHref:'/gestao-conteudos',dashboardNavigation:[{label:'Visão geral',href:'/dashboard'},{label:'O meu trabalho',href:'/dashboard#meu-trabalho'}],administrationNavigation:[{label:'Demografia',href:'/management/demography'}],profileAccess:{contexts:[],request:'/perfil/promotor',administration:'/perfis/gestao',audit:'/perfis/auditoria'},scopedAdministration:[{label:'Atribuições do Balcão',href:'/management/balcao/atribuicoes'}]}); + const admin=groups.find(g=>g.id==='administration'); + assert.deepEqual(admin.children.map(g=>g.label),['Dashboard','Conteúdos e oportunidades','Cadastros e investimento','Pedidos e acompanhamento','Acessos e configuração']); + assert.deepEqual(destinations(admin.children.find(g=>g.label==='Dashboard').children),['/dashboard','/dashboard#meu-trabalho']); + for(const href of ['/management/projects','/management/demography','/perfis/gestao','/perfis/auditoria','/management/balcao/atribuicoes']) { + assert.equal(destinations(admin.children).filter(h=>h===href).length,1,href); + assert.equal(destinations(groups.filter(g=>g.id!=='administration')).includes(href),false,href); + } +});