diff --git a/config/sidebar.php b/config/sidebar.php new file mode 100644 index 0000000..15bd7a1 --- /dev/null +++ b/config/sidebar.php @@ -0,0 +1,29 @@ +.). + */ +return [ + [ + 'label' => 'sidebar.general.section', + 'icon' => 'mdi:view-dashboard-outline', + 'items' => [ + ['label' => 'sidebar.general.dashboard', 'to' => '/', 'icon' => 'mdi:view-dashboard-outline'], + ['label' => 'sidebar.general.myTasks', 'to' => '/my-tasks', 'icon' => 'mdi:checkbox-marked-circle-outline'], + ['label' => 'sidebar.general.projects', 'to' => '/projects', 'icon' => 'mdi:folder-multiple-outline'], + ['label' => 'sidebar.general.timeTracking', 'to' => '/time-tracking', 'icon' => 'mdi:clock-outline'], + ], + ], + [ + 'label' => 'sidebar.hr.section', + 'icon' => 'mdi:calendar-account-outline', + 'items' => [ + ['label' => 'sidebar.hr.absences', 'to' => '/absences', 'icon' => 'mdi:calendar-remove-outline'], + ['label' => 'sidebar.hr.teamAbsences', 'to' => '/team-absences', 'icon' => 'mdi:account-group-outline'], + ], + ], +]; diff --git a/src/Shared/Domain/Sidebar/SidebarFilter.php b/src/Shared/Domain/Sidebar/SidebarFilter.php new file mode 100644 index 0000000..e97ca8d --- /dev/null +++ b/src/Shared/Domain/Sidebar/SidebarFilter.php @@ -0,0 +1,40 @@ +}> $sections + * @param list $activeModuleIds + * + * @return array{sections: list}>, disabledRoutes: list} + */ + public static function filter(array $sections, array $activeModuleIds): array + { + $outSections = []; + $disabledRoutes = []; + + foreach ($sections as $section) { + $items = []; + foreach ($section['items'] as $item) { + $module = $item['module'] ?? null; + if (null !== $module && !in_array($module, $activeModuleIds, true)) { + $disabledRoutes[] = $item['to']; + + continue; + } + + $items[] = ['label' => $item['label'], 'to' => $item['to'], 'icon' => $item['icon']]; + } + + if ([] !== $items) { + $outSections[] = ['label' => $section['label'], 'icon' => $section['icon'], 'items' => $items]; + } + } + + return ['sections' => $outSections, 'disabledRoutes' => $disabledRoutes]; + } +} diff --git a/src/Shared/Infrastructure/ApiPlatform/Resource/SidebarResource.php b/src/Shared/Infrastructure/ApiPlatform/Resource/SidebarResource.php new file mode 100644 index 0000000..c90877e --- /dev/null +++ b/src/Shared/Infrastructure/ApiPlatform/Resource/SidebarResource.php @@ -0,0 +1,34 @@ + ['sidebar:read']], + provider: SidebarProvider::class, + ), + ], +)] +final class SidebarResource +{ + /** + * @var list}> + */ + #[Groups(['sidebar:read'])] + public array $sections = []; + + /** + * @var list + */ + #[Groups(['sidebar:read'])] + public array $disabledRoutes = []; +} diff --git a/src/Shared/Infrastructure/ApiPlatform/State/SidebarProvider.php b/src/Shared/Infrastructure/ApiPlatform/State/SidebarProvider.php new file mode 100644 index 0000000..84333d5 --- /dev/null +++ b/src/Shared/Infrastructure/ApiPlatform/State/SidebarProvider.php @@ -0,0 +1,37 @@ + $moduleClasses */ + $moduleClasses = require $this->projectDir.'/config/modules.php'; + + /** @var list}> $sidebar */ + $sidebar = require $this->projectDir.'/config/sidebar.php'; + + $filtered = SidebarFilter::filter($sidebar, ModuleRegistry::ids($moduleClasses)); + + $dto = new SidebarResource(); + $dto->sections = $filtered['sections']; + $dto->disabledRoutes = $filtered['disabledRoutes']; + + return $dto; + } +} diff --git a/tests/Functional/Shared/SidebarEndpointTest.php b/tests/Functional/Shared/SidebarEndpointTest.php new file mode 100644 index 0000000..55f0628 --- /dev/null +++ b/tests/Functional/Shared/SidebarEndpointTest.php @@ -0,0 +1,40 @@ +request('GET', '/api/sidebar'); + + self::assertResponseStatusCodeSame(401); + } + + public function testSidebarReturnsSectionsForAuthenticatedUser(): void + { + $client = self::createClient(); + $container = self::getContainer(); + $em = $container->get('doctrine.orm.entity_manager'); + + $user = $em->getRepository(User::class)->findOneBy(['username' => 'alice']); + $client->loginUser($user); + + $client->request('GET', '/api/sidebar'); + + self::assertResponseIsSuccessful(); + $data = json_decode($client->getResponse()->getContent(), true); + self::assertArrayHasKey('sections', $data); + self::assertArrayHasKey('disabledRoutes', $data); + self::assertNotEmpty($data['sections']); + } +} diff --git a/tests/Unit/Shared/Sidebar/SidebarFilterTest.php b/tests/Unit/Shared/Sidebar/SidebarFilterTest.php new file mode 100644 index 0000000..22dbf74 --- /dev/null +++ b/tests/Unit/Shared/Sidebar/SidebarFilterTest.php @@ -0,0 +1,59 @@ + 'sidebar.core.section', 'icon' => 'mdi:home', 'items' => [ + ['label' => 'sidebar.core.dashboard', 'to' => '/', 'icon' => 'mdi:view-dashboard'], + ]], + ]; + + $result = SidebarFilter::filter($sections, []); + + self::assertCount(1, $result['sections']); + self::assertSame('/', $result['sections'][0]['items'][0]['to']); + self::assertSame([], $result['disabledRoutes']); + self::assertArrayNotHasKey('module', $result['sections'][0]['items'][0]); + } + + public function testItemWithInactiveModuleIsHiddenAndRouteDisabled(): void + { + $sections = [ + ['label' => 'sidebar.tt.section', 'icon' => 'mdi:clock', 'items' => [ + ['label' => 'sidebar.tt.timesheet', 'to' => '/time-tracking', 'icon' => 'mdi:clock', 'module' => 'time_tracking'], + ]], + ]; + + $result = SidebarFilter::filter($sections, []); + + self::assertSame([], $result['sections']); + self::assertSame(['/time-tracking'], $result['disabledRoutes']); + } + + public function testItemWithActiveModuleIsVisible(): void + { + $sections = [ + ['label' => 'sidebar.tt.section', 'icon' => 'mdi:clock', 'items' => [ + ['label' => 'sidebar.tt.timesheet', 'to' => '/time-tracking', 'icon' => 'mdi:clock', 'module' => 'time_tracking'], + ]], + ]; + + $result = SidebarFilter::filter($sections, ['time_tracking']); + + self::assertCount(1, $result['sections']); + self::assertSame('/time-tracking', $result['sections'][0]['items'][0]['to']); + self::assertSame([], $result['disabledRoutes']); + } +}