Separate dashboard functions from grouped administration operations
tests / ci (push) Waiting to run

This commit is contained in:
2026-11-14 12:25:27 +02:00
parent 88f18d0b7e
commit 96f510965f
14 changed files with 259 additions and 81 deletions
@@ -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(),
+37 -13
View File
@@ -54,25 +54,18 @@ final class ContentManagement
/** @param array<array-key,mixed> $access
* @return list<array{label:string,href:string}>
*/
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<array-key,mixed> $access
* @return list<array{label:string,href:string}>
*/
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;
}
@@ -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.
+3
View File
@@ -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,
@@ -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);
@@ -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) =>
+115 -51
View File
@@ -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'),
+31 -5
View File
@@ -60,7 +60,20 @@ export default function Dashboard({
<h1 className="text-2xl font-semibold">Painel do portal</h1>
<ProfileNavigation />
<ContentManagementEntry />
<DashboardWorkQueue queue={workQueue} />
<section id="meu-trabalho" className="scroll-mt-24">
{workQueue ? (
<DashboardWorkQueue queue={workQueue} />
) : (
<>
<h2 className="text-lg font-semibold">
O meu trabalho
</h2>
<p className="text-muted-foreground text-sm">
Sem fila de trabalho disponível neste âmbito.
</p>
</>
)}
</section>
{editorialQueue && (
<section
className="rounded-lg border p-4"
@@ -128,7 +141,10 @@ export default function Dashboard({
</p>
</section>
)}
<section className="space-y-4">
<section
id="manifestacoes"
className="scroll-mt-24 space-y-4"
>
<h2 className="text-lg font-semibold">
Manifestações de interesse
</h2>
@@ -149,7 +165,10 @@ export default function Dashboard({
))}
</div>
</section>
<section className="space-y-4">
<section
id="indicadores-oportunidades"
className="scroll-mt-24 space-y-4"
>
<h2 className="text-lg font-semibold">
<Link href="/management/opportunities">
{opportunityManagement
@@ -190,7 +209,10 @@ export default function Dashboard({
))}
</div>
</section>
<section className="space-y-4">
<section
id="pedidos-recentes"
className="scroll-mt-24 space-y-4"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<h2 className="text-lg font-semibold">
Pedidos mais recentes
@@ -243,6 +265,7 @@ export default function Dashboard({
)}
</section>
<nav
id="accoes-rapidas"
aria-label="Acções rápidas"
className="flex flex-wrap gap-4"
>
@@ -264,7 +287,10 @@ export default function Dashboard({
</nav>
</>
) : investorPath ? (
<section className="space-y-4 rounded-xl border p-6">
<section
id="meus-investimentos"
className="scroll-mt-24 space-y-4 rounded-xl border p-6"
>
<h2 className="text-lg font-semibold">
Os meus investimentos
</h2>
@@ -152,7 +152,7 @@ export default function EditorialWorkflow({
},
].filter((area) => area.entries.some((item) => item.href));
return (
<Management title="Dashboard">
<Management title="Circuito editorial">
<p className="max-w-3xl text-sm text-slate-600">
Acompanhe a preparação, revisão e publicação numa única área de
trabalho. Seleccione uma actividade para continuar nas tarefas
@@ -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);
}
+7 -3
View File
@@ -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
@@ -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"/);
});
@@ -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(
+19 -6
View File
@@ -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);
}
});