import { Component, HostListener, computed, effect, inject, signal } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet, Router } from '@angular/router'; import type { Permission } from '@boilerplate/api-client'; import { NotificationStore } from '../core/notification.store'; import { AuthService } from '../core/auth.service'; import { NotificationPanelComponent } from './notification-panel'; import { UiIconButtonComponent, UiIconComponent, UiToastHostComponent } from '../shared/ui'; interface NavItem { label: string; path: string; permission?: Permission; } @Component({ selector: 'app-shell', standalone: true, imports: [ RouterOutlet, RouterLink, RouterLinkActive, NotificationPanelComponent, UiIconButtonComponent, UiIconComponent, UiToastHostComponent, ], template: `
@if (auth.user()) { } HausPilot @if (auth.user()) { Abmelden } @else { Anmelden } @if (auth.user() && notificationPanelOpen()) { }
@if (auth.loaded()) { @if (auth.user()) { @if (drawerOpen()) { }
} @else {
} } @else {
}
`, styles: [ ` .shell { min-height: 100vh; background: var(--color-background); color: var(--color-text-primary); } .topbar { position: sticky; top: 0; z-index: var(--z-header); height: var(--header-height); display: flex; align-items: center; gap: var(--space-4); padding: 0 var(--space-5); background: var(--color-surface); border-bottom: 1px solid var(--color-border); } .brand { white-space: nowrap; } .logout { margin-left: auto; } .notification-button { margin-left: auto; } .badge { position: absolute; top: var(--space-1); right: var(--space-1); min-width: 1.125rem; height: 1.125rem; border-radius: var(--radius-pill); background: var(--color-danger); color: var(--color-surface); display: grid; place-items: center; font-size: var(--font-size-xs); padding: 0 var(--space-2); } .public-content { min-height: calc(100vh - var(--header-height)); display: grid; place-items: center; margin-left: 0; } .login-panel { width: min(100%, 440px); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); padding: var(--space-6); display: grid; gap: var(--space-4); } .login-panel h1, .login-panel p { margin: 0; } .login-panel p { color: var(--color-text-secondary); } .drawer-backdrop { position: fixed; inset: var(--header-height) 0 0; z-index: calc(var(--z-drawer) - 1); border: 0; background: color-mix(in srgb, var(--color-text-primary) 36%, transparent); } .sidebar { position: fixed; inset: var(--header-height) auto 0 0; width: var(--sidebar-width); background: var(--color-surface); border-right: 1px solid var(--color-border); transform: translateX(-100%); transition: transform var(--transition-base) ease; z-index: var(--z-drawer); } .sidebar.open { transform: translateX(0); } nav a { display: block; padding: var(--space-4) var(--space-5); color: var(--color-text-primary); text-decoration: none; min-height: var(--touch-target); } nav a.active { background: var(--color-primary-subtle); border-left: 4px solid var(--color-primary); } .content { min-width: 0; padding: var(--space-5) var(--space-5) var(--space-8); } .breadcrumbs { color: var(--color-text-muted); font-size: var(--font-size-sm); margin-bottom: var(--space-4); } @media (min-width: 64rem) { ui-icon-button { display: none; } .drawer-backdrop { display: none; } .sidebar { transform: none; } .content { margin-left: var(--sidebar-width); padding: var(--space-7); } } `, ], }) export class AppShellComponent { private readonly router = inject(Router); readonly auth = inject(AuthService); readonly notifications = inject(NotificationStore); readonly drawerOpen = signal(false); readonly notificationPanelOpen = signal(false); readonly title = computed( () => this.router.routerState.snapshot.root.firstChild?.firstChild?.title ?? 'Dashboard', ); readonly nav: NavItem[] = [ { label: 'Dashboard', path: '/' }, { label: 'Projekte', path: '/projekte', permission: 'projects.use' }, { label: 'Einladungen', path: '/einladungen', permission: 'projects.use' }, { label: 'Profil', path: '/profil' }, { label: 'Sicherheit', path: '/account/security' }, { label: 'Benachrichtigungen', path: '/notifications', permission: 'notifications.readOwn', }, { label: 'Sessions', path: '/sessions', permission: 'sessions.readOwn' }, { label: 'Items', path: '/items', permission: 'items.read' }, { label: 'Admin Benutzer', path: '/admin/users', permission: 'users.read' }, { label: 'Admin Rollen', path: '/admin/roles', permission: 'roles.read' }, { label: 'Admin Audit', path: '/admin/audit', permission: 'audit.read' }, ]; constructor() { this.auth.loadMe(); effect(() => { if (this.auth.user()) { this.notifications.startPolling(); } else { this.notifications.stopPolling(); this.notificationPanelOpen.set(false); } }); } visible(item: NavItem): boolean { return !item.permission || this.auth.has(item.permission); } loginHref(): string { const returnTo = this.router.url.startsWith('/einladungen') ? this.router.url : '/'; return `/api/auth/login?returnTo=${encodeURIComponent(returnTo)}`; } toggleNotifications(): void { this.notificationPanelOpen.update((open) => !open); if (this.notificationPanelOpen()) { this.notifications.openPanel(); } } toggleDrawer(): void { this.drawerOpen.update((open) => !open); } closeDrawerOnNavigation(): void { this.drawerOpen.set(false); } @HostListener('document:keydown.escape') closeOverlays(): void { this.drawerOpen.set(false); this.notificationPanelOpen.set(false); } }