mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
✨ feat(shell-navigation): implement navigation components with collapsible menu
Add modular navigation system with three reusable components: - NavigationGroupComponent for grouped navigation sections - NavigationItemComponent for main navigation entries with expand/collapse - NavigationSubItemComponent for nested child routes Include navigation types, route configuration with dynamic tabId support, and integrate navigation into shell-layout sidebar.
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
<shell-network-status-banner />
|
<shell-network-status-banner />
|
||||||
<shell-header />
|
<shell-header />
|
||||||
|
|
||||||
|
@if (renderNavigation()) {
|
||||||
|
<shell-navigation />
|
||||||
|
}
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { ChangeDetectionStrategy, Component } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
} from '@angular/core';
|
||||||
import { NetworkStatusBannerComponent } from './components/network-status-banner.component';
|
import { NetworkStatusBannerComponent } from './components/network-status-banner.component';
|
||||||
|
import { NavigationService } from '@isa/shell/common';
|
||||||
import { ShellHeaderComponent } from '@isa/shell/header';
|
import { ShellHeaderComponent } from '@isa/shell/header';
|
||||||
|
import { ShellNavigationComponent } from '@isa/shell/navigation';
|
||||||
|
import { Breakpoint, breakpoint } from '@isa/ui/layout';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'shell-layout',
|
selector: 'shell-layout',
|
||||||
@@ -9,9 +16,27 @@ import { ShellHeaderComponent } from '@isa/shell/header';
|
|||||||
templateUrl: './shell-layout.component.html',
|
templateUrl: './shell-layout.component.html',
|
||||||
styleUrls: ['./shell-layout.component.css'],
|
styleUrls: ['./shell-layout.component.css'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
imports: [NetworkStatusBannerComponent, ShellHeaderComponent],
|
imports: [
|
||||||
|
NetworkStatusBannerComponent,
|
||||||
|
ShellHeaderComponent,
|
||||||
|
ShellNavigationComponent,
|
||||||
|
],
|
||||||
host: {
|
host: {
|
||||||
class: 'text-isa-neutral-900',
|
class: 'text-isa-neutral-900',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export class ShellLayoutComponent {}
|
export class ShellLayoutComponent {
|
||||||
|
#navigationService = inject(NavigationService);
|
||||||
|
|
||||||
|
readonly tablet = breakpoint(Breakpoint.Tablet);
|
||||||
|
|
||||||
|
readonly renderNavigation = computed(() => {
|
||||||
|
const tablet = this.tablet();
|
||||||
|
|
||||||
|
if (!tablet) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#navigationService.get();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export * from './lib/shell-navigation/shell-navigation.component';
|
export * from './lib/shell-navigation.component';
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
@apply block self-stretch;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<div class="flex flex-col h-14 items-start self-stretch px-4">
|
||||||
|
<div
|
||||||
|
class="isa-text-caption-caps flex h-6 flex-col justify-center flex-[1_0_0] self-stretch"
|
||||||
|
>
|
||||||
|
{{ group().label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@for (item of group().items; track item.label) {
|
||||||
|
<shell-navigation-item [item]="item" />
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||||
|
import { NavigationGroup } from '../../types';
|
||||||
|
import { ShellNavigationItemComponent } from '../navigation-item/navigation-item.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'shell-navigation-group',
|
||||||
|
templateUrl: './navigation-group.component.html',
|
||||||
|
styleUrls: ['./navigation-group.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [ShellNavigationItemComponent],
|
||||||
|
})
|
||||||
|
export class ShellNavigationGroupComponent {
|
||||||
|
readonly group = input.required<NavigationGroup>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
:host {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
button {
|
||||||
|
@apply isa-text-body-1-regular;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease-out,
|
||||||
|
font-weight 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active,
|
||||||
|
a:hover,
|
||||||
|
a:focus,
|
||||||
|
button:hover,
|
||||||
|
button:focus {
|
||||||
|
@apply bg-isa-neutral-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active .label {
|
||||||
|
@apply isa-text-body-1-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-bar {
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: right;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active .active-bar {
|
||||||
|
animation: barReveal 0.25s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes barReveal {
|
||||||
|
from {
|
||||||
|
transform: scaleX(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron {
|
||||||
|
transition: transform 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron.expanded {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-items {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
opacity: 0;
|
||||||
|
transition:
|
||||||
|
grid-template-rows 0.25s ease-out,
|
||||||
|
opacity 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-items.expanded {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-items.collapsed {
|
||||||
|
grid-template-rows: 0fr;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.chevron {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active .active-bar {
|
||||||
|
animation: none;
|
||||||
|
transform: scaleX(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sub-items {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
@if (route(); as r) {
|
||||||
|
<a
|
||||||
|
class="relative flex flex-row h-14 px-6 items-center justify-start gap-2 self-stretch"
|
||||||
|
routerLinkActive="active"
|
||||||
|
[routerLink]="r.route"
|
||||||
|
[queryParams]="r.queryParams"
|
||||||
|
[queryParamsHandling]="r.queryParamsHandling"
|
||||||
|
data-what="navigation-item"
|
||||||
|
[attr.data-which]="item().label"
|
||||||
|
[attr.aria-label]="'Navigate to ' + item().label"
|
||||||
|
>
|
||||||
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
|
</a>
|
||||||
|
} @else {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="relative flex flex-row h-14 px-6 items-center justify-start gap-2 self-stretch w-full text-left"
|
||||||
|
(click)="toggleExpand()"
|
||||||
|
data-what="navigation-toggle"
|
||||||
|
[attr.data-which]="item().label"
|
||||||
|
[attr.aria-expanded]="expanded()"
|
||||||
|
[attr.aria-label]="'Toggle ' + item().label + ' submenu'"
|
||||||
|
>
|
||||||
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (hasChildren()) {
|
||||||
|
<div
|
||||||
|
class="sub-items bg-isa-neutral-100 mx-6 desktop:mx-[0.62rem] rounded-[.25rem] overflow-hidden"
|
||||||
|
[class.expanded]="expanded()"
|
||||||
|
[class.collapsed]="!expanded()"
|
||||||
|
>
|
||||||
|
<div class="min-h-0">
|
||||||
|
@for (child of item().childRoutes; track $index) {
|
||||||
|
<shell-navigation-sub-item [item]="child" [style.--i]="$index" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<ng-template #content>
|
||||||
|
<div
|
||||||
|
class="active-bar absolute right-0 top-0 bottom-0 w-[0.375rem] bg-isa-neutral-700 rounded-full"
|
||||||
|
></div>
|
||||||
|
<div class="size-6 grow-0" [innerHTML]="sanitizedHtml()"></div>
|
||||||
|
<div class="label grow">
|
||||||
|
{{ item().label }}
|
||||||
|
</div>
|
||||||
|
@if (hasChildren()) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
(click)="$event.stopPropagation(); toggleExpand()"
|
||||||
|
data-what="navigation-chevron"
|
||||||
|
[attr.data-which]="item().label"
|
||||||
|
[attr.aria-expanded]="expanded()"
|
||||||
|
[attr.aria-label]="'Toggle ' + item().label + ' submenu'"
|
||||||
|
>
|
||||||
|
<ng-icon
|
||||||
|
name="isaActionChevronRight"
|
||||||
|
size="1.5rem"
|
||||||
|
class="chevron"
|
||||||
|
[class.expanded]="expanded()"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</ng-template>
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
effect,
|
||||||
|
inject,
|
||||||
|
Injector,
|
||||||
|
input,
|
||||||
|
isSignal,
|
||||||
|
runInInjectionContext,
|
||||||
|
signal,
|
||||||
|
viewChildren,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { NavigationItem } from '../../types';
|
||||||
|
import { DomSanitizer } from '@angular/platform-browser';
|
||||||
|
import { isaActionChevronRight } from '@isa/icons';
|
||||||
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
import { ShellNavigationSubItemComponent } from '../navigation-sub-item/navigation-sub-item.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'shell-navigation-item',
|
||||||
|
templateUrl: './navigation-item.component.html',
|
||||||
|
styleUrls: ['./navigation-item.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
NgIcon,
|
||||||
|
NgTemplateOutlet,
|
||||||
|
RouterLink,
|
||||||
|
RouterLinkActive,
|
||||||
|
ShellNavigationSubItemComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
provideIcons({
|
||||||
|
isaActionChevronRight,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ShellNavigationItemComponent {
|
||||||
|
#injector = inject(Injector);
|
||||||
|
#sanitizer = inject(DomSanitizer);
|
||||||
|
|
||||||
|
readonly subItems = viewChildren(ShellNavigationSubItemComponent);
|
||||||
|
|
||||||
|
expanded = signal(false);
|
||||||
|
|
||||||
|
readonly item = input.required<NavigationItem>();
|
||||||
|
|
||||||
|
sanitizedHtml = computed(() => {
|
||||||
|
const icon = this.item().icon;
|
||||||
|
|
||||||
|
if (!icon) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.#sanitizer.bypassSecurityTrustHtml(icon);
|
||||||
|
});
|
||||||
|
|
||||||
|
hasChildren = computed(() => {
|
||||||
|
return (this.item().childRoutes?.length ?? 0) > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
toggleExpand(): void {
|
||||||
|
this.expanded.update((value) => !value);
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly route = computed(() => {
|
||||||
|
const route = this.item().route;
|
||||||
|
|
||||||
|
if (typeof route === 'function') {
|
||||||
|
const result = runInInjectionContext(this.#injector, () => route());
|
||||||
|
return isSignal(result) ? result() : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return route;
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Auto-expand when a child route becomes active.
|
||||||
|
// This is a valid effect use case: one-way sync of UI state based on router state.
|
||||||
|
// It only expands (never auto-collapses), preserving user's manual toggle preference.
|
||||||
|
effect(() => {
|
||||||
|
const subItems = this.subItems();
|
||||||
|
if (!subItems.length) return;
|
||||||
|
|
||||||
|
const hasActiveChild = subItems.some((subItem) => subItem.isActive());
|
||||||
|
|
||||||
|
if (hasActiveChild) {
|
||||||
|
this.expanded.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
:host {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply isa-text-body-2-regular;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease-out,
|
||||||
|
font-weight 0.15s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.slide-in {
|
||||||
|
animation: slideIn 0.2s ease-out backwards;
|
||||||
|
animation-delay: calc(var(--i, 0) * 50ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active,
|
||||||
|
a:hover,
|
||||||
|
a:focus,
|
||||||
|
a:active {
|
||||||
|
@apply bg-isa-neutral-200 isa-text-body-2-semibold;
|
||||||
|
}
|
||||||
|
|
||||||
|
a .active-bar {
|
||||||
|
@apply hidden;
|
||||||
|
transform: scaleX(0);
|
||||||
|
transform-origin: right;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active .active-bar {
|
||||||
|
@apply block;
|
||||||
|
animation: barReveal 0.25s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes barReveal {
|
||||||
|
from {
|
||||||
|
transform: scaleX(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scaleX(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
a.slide-in {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.active .active-bar {
|
||||||
|
animation: none;
|
||||||
|
transform: scaleX(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@if (route(); as r) {
|
||||||
|
<a
|
||||||
|
class="flex flex-row h-14 px-4 items-center self-stretch rounded-[.25rem]"
|
||||||
|
animate.enter="slide-in"
|
||||||
|
routerLinkActive="active"
|
||||||
|
(isActiveChange)="isActive.set($event)"
|
||||||
|
[routerLink]="r.route"
|
||||||
|
[queryParams]="r.queryParams"
|
||||||
|
[queryParamsHandling]="r.queryParamsHandling"
|
||||||
|
data-what="navigation-sub-item"
|
||||||
|
[attr.data-which]="label()"
|
||||||
|
[attr.aria-label]="'Navigate to ' + label()"
|
||||||
|
>
|
||||||
|
<div class="grow">
|
||||||
|
{{ label() }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="active-bar grow-0 w-[0.375rem] bg-isa-neutral-700 rounded-full self-stretch -mr-4"
|
||||||
|
></div>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
afterNextRender,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
Injector,
|
||||||
|
input,
|
||||||
|
isSignal,
|
||||||
|
runInInjectionContext,
|
||||||
|
signal,
|
||||||
|
viewChild,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||||
|
import {
|
||||||
|
NavigationRouteWithLabel,
|
||||||
|
NavigationRouteWithLabelFn,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'shell-navigation-sub-item',
|
||||||
|
templateUrl: './navigation-sub-item.component.html',
|
||||||
|
styleUrls: ['./navigation-sub-item.component.css'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [RouterLink, RouterLinkActive],
|
||||||
|
})
|
||||||
|
export class ShellNavigationSubItemComponent {
|
||||||
|
#injector = inject(Injector);
|
||||||
|
|
||||||
|
readonly routerLinkActive = viewChild(RouterLinkActive);
|
||||||
|
readonly isActive = signal(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
afterNextRender(() => {
|
||||||
|
const rla = this.routerLinkActive();
|
||||||
|
if (rla?.isActive) {
|
||||||
|
this.isActive.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly item = input.required<
|
||||||
|
NavigationRouteWithLabel | NavigationRouteWithLabelFn
|
||||||
|
>();
|
||||||
|
|
||||||
|
readonly route = computed(() => {
|
||||||
|
const item = this.item();
|
||||||
|
|
||||||
|
if (typeof item === 'function') {
|
||||||
|
const result = runInInjectionContext(this.#injector, () => item());
|
||||||
|
return isSignal(result) ? result() : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly label = computed(() => this.route()?.label ?? '');
|
||||||
|
}
|
||||||
187
libs/shell/navigation/src/lib/navigations.ts
Normal file
187
libs/shell/navigation/src/lib/navigations.ts
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
import {
|
||||||
|
isaNavigationAbholfach,
|
||||||
|
isaNavigationArtikelsuche,
|
||||||
|
isaNavigationCalender,
|
||||||
|
isaNavigationDashboard,
|
||||||
|
isaNavigationKunden,
|
||||||
|
isaNavigationRemission1,
|
||||||
|
isaNavigationReturn,
|
||||||
|
isaNavigationSortiment,
|
||||||
|
isaNavigationWarenausgabe,
|
||||||
|
isaNavigationWareneingang1,
|
||||||
|
} from '@isa/icons';
|
||||||
|
import { injectTabId } from '@isa/core/tabs';
|
||||||
|
|
||||||
|
import { NavigationGroup, NavigationItem } from './types';
|
||||||
|
import { computed } from '@angular/core';
|
||||||
|
|
||||||
|
export const navigations: Array<NavigationGroup | NavigationItem> = [
|
||||||
|
// Standalone item with dynamic tabId route
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationDashboard,
|
||||||
|
label: 'Dashboard',
|
||||||
|
route: {
|
||||||
|
route: '/dashboard',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// KUNDEN group
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
label: 'Kunden',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationArtikelsuche,
|
||||||
|
label: 'Artikelsuche',
|
||||||
|
route: () => {
|
||||||
|
const activeTabId = injectTabId();
|
||||||
|
return computed(() => {
|
||||||
|
const tabId = activeTabId() ?? Date.now();
|
||||||
|
return {
|
||||||
|
route: ['/kunde', tabId, 'product'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationKunden,
|
||||||
|
label: 'Kunden',
|
||||||
|
route: () => {
|
||||||
|
const activeTabId = injectTabId();
|
||||||
|
return computed(() => {
|
||||||
|
const tabId = activeTabId() ?? Date.now();
|
||||||
|
return {
|
||||||
|
route: ['/kunde', tabId, 'customer'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationWarenausgabe,
|
||||||
|
label: 'Warenausgabe',
|
||||||
|
route: () => {
|
||||||
|
const activeTabId = injectTabId();
|
||||||
|
return computed(() => {
|
||||||
|
const tabId = activeTabId() ?? Date.now();
|
||||||
|
return {
|
||||||
|
route: ['/kunde', tabId, 'pickup-shelf'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationReturn,
|
||||||
|
label: 'Rückgabe',
|
||||||
|
route: () => {
|
||||||
|
const activeTabId = injectTabId();
|
||||||
|
return computed(() => {
|
||||||
|
const tabId = activeTabId() ?? Date.now();
|
||||||
|
return {
|
||||||
|
route: ['/', tabId, 'return'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// FILIALE group
|
||||||
|
{
|
||||||
|
type: 'group',
|
||||||
|
label: 'Filiale',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationCalender,
|
||||||
|
label: 'Kalender',
|
||||||
|
route: {
|
||||||
|
route: '/filiale/task-calendar',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationSortiment,
|
||||||
|
label: 'Sortiment',
|
||||||
|
route: {
|
||||||
|
route: '/filiale/assortment',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationAbholfach,
|
||||||
|
label: 'Abholfach',
|
||||||
|
childRoutes: [
|
||||||
|
{
|
||||||
|
label: 'Einbuchen',
|
||||||
|
route: '/filiale/pickup-shelf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Reservierung',
|
||||||
|
route: '/filiale/goods/in/reservation',
|
||||||
|
queryParams: { view: 'reservation' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Ausräumen',
|
||||||
|
route: '/filiale/goods/in/cleanup',
|
||||||
|
queryParams: { view: 'cleanup' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Remi Vorschau',
|
||||||
|
route: '/filiale/goods/in/preview',
|
||||||
|
queryParams: { view: 'remission' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fehlende',
|
||||||
|
route: '/filiale/goods/in/list',
|
||||||
|
queryParams: { view: 'wareneingangsliste' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationRemission1,
|
||||||
|
label: 'Remission',
|
||||||
|
childRoutes: [
|
||||||
|
() => {
|
||||||
|
const activeTabId = injectTabId();
|
||||||
|
return computed(() => {
|
||||||
|
const tabId = activeTabId() ?? Date.now();
|
||||||
|
return {
|
||||||
|
label: 'Remission',
|
||||||
|
route: ['/', tabId, 'remission'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const activeTabId = injectTabId();
|
||||||
|
return computed(() => {
|
||||||
|
const tabId = activeTabId() ?? Date.now();
|
||||||
|
return {
|
||||||
|
label: 'Warenbegleitscheine',
|
||||||
|
route: ['/', tabId, 'remission', 'return-receipt'],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'item',
|
||||||
|
icon: isaNavigationWareneingang1,
|
||||||
|
label: 'Wareneingang',
|
||||||
|
route: {
|
||||||
|
route: '/filiale/package-inspection/packages',
|
||||||
|
queryParams: {
|
||||||
|
filter_status: '0;8',
|
||||||
|
filter_zeitraum: '"2025-11-26T23:00:00Z"-"2025-12-04T23:00:00Z"',
|
||||||
|
filter_lieferant: 'libri;knv%7Cknvbs',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<nav
|
||||||
|
class="bg-isa-white rounded-r-2xl flex w-[13.75rem] py-8 gap-8 flex-col"
|
||||||
|
aria-label="Main navigation"
|
||||||
|
data-what="main-navigation"
|
||||||
|
>
|
||||||
|
@for (navigation of navigations; track navigation.label) {
|
||||||
|
@switch (navigation.type) {
|
||||||
|
@case ('group') {
|
||||||
|
<shell-navigation-group [group]="navigation" />
|
||||||
|
}
|
||||||
|
@case ('item') {
|
||||||
|
<shell-navigation-item [item]="navigation" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</nav>
|
||||||
14
libs/shell/navigation/src/lib/shell-navigation.component.ts
Normal file
14
libs/shell/navigation/src/lib/shell-navigation.component.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { navigations } from './navigations';
|
||||||
|
import { ShellNavigationGroupComponent } from './components/navigation-group/navigation-group.component';
|
||||||
|
import { ShellNavigationItemComponent } from './components/navigation-item/navigation-item.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'shell-navigation',
|
||||||
|
imports: [ShellNavigationGroupComponent, ShellNavigationItemComponent],
|
||||||
|
templateUrl: './shell-navigation.component.html',
|
||||||
|
styleUrl: './shell-navigation.component.css',
|
||||||
|
})
|
||||||
|
export class ShellNavigationComponent {
|
||||||
|
readonly navigations = navigations;
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
<p>ShellNavigation works!</p>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
||||||
import { ShellNavigationComponent } from './shell-navigation.component';
|
|
||||||
|
|
||||||
describe('ShellNavigationComponent', () => {
|
|
||||||
let component: ShellNavigationComponent;
|
|
||||||
let fixture: ComponentFixture<ShellNavigationComponent>;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [ShellNavigationComponent],
|
|
||||||
}).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ShellNavigationComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'shell-shell-navigation',
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './shell-navigation.component.html',
|
|
||||||
styleUrl: './shell-navigation.component.css',
|
|
||||||
})
|
|
||||||
export class ShellNavigationComponent {}
|
|
||||||
33
libs/shell/navigation/src/lib/types.ts
Normal file
33
libs/shell/navigation/src/lib/types.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Signal } from '@angular/core';
|
||||||
|
import { Params, QueryParamsHandling, UrlTree } from '@angular/router';
|
||||||
|
|
||||||
|
export type NavigationRoute = {
|
||||||
|
route: string | unknown[] | UrlTree;
|
||||||
|
queryParams?: Params;
|
||||||
|
queryParamsHandling?: QueryParamsHandling;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationRouteWithLabel = NavigationRoute & {
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationRouteFn = () => NavigationRoute | Signal<NavigationRoute>;
|
||||||
|
|
||||||
|
export type NavigationRouteWithLabelFn = () =>
|
||||||
|
| NavigationRouteWithLabel
|
||||||
|
| Signal<NavigationRouteWithLabel>;
|
||||||
|
|
||||||
|
export type NavigationItem = {
|
||||||
|
type: 'item';
|
||||||
|
icon?: string;
|
||||||
|
label: string;
|
||||||
|
|
||||||
|
route?: NavigationRoute | NavigationRouteFn;
|
||||||
|
childRoutes?: Array<NavigationRouteWithLabel | NavigationRouteWithLabelFn>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NavigationGroup = {
|
||||||
|
type: 'group';
|
||||||
|
label: string;
|
||||||
|
items: Array<NavigationItem>;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user