import { ChangeDetectionStrategy, Component, DestroyRef, ElementRef, HostListener, computed, inject, signal, viewChild } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; import { filter } from 'rxjs/operators'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { TranslatePipe } from '../../../i18n/translate.pipe'; import { TranslateService } from '../../../i18n/translate.service'; import { LanguageService } from '../../../services/language.service'; import { AdminAuthService } from '../../../core/admin-auth/admin-auth.service'; import { ADMIN_NAV_BOTTOM, ADMIN_NAV_PRIMARY, AdminBreadcrumbEntry, AdminNavEntry } from './admin-nav.model'; @Component({ selector: 'app-admin-layout', standalone: true, imports: [CommonModule, RouterOutlet, RouterLink, RouterLinkActive, TranslatePipe], templateUrl: './admin-layout.component.html', styleUrl: './admin-layout.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class AdminLayoutComponent { private readonly router = inject(Router); private readonly route = inject(ActivatedRoute); private readonly destroyRef = inject(DestroyRef); private readonly translate = inject(TranslateService); private readonly languageService = inject(LanguageService); private readonly adminAuth = inject(AdminAuthService); private readonly drawerEl = viewChild>('drawer'); private readonly menuToggleEl = viewChild>('menuToggle'); readonly navPrimary: AdminNavEntry[] = ADMIN_NAV_PRIMARY; readonly navBottom: AdminNavEntry[] = ADMIN_NAV_BOTTOM; readonly mobileDrawerOpen = signal(false); readonly notificationsOpen = signal(false); readonly currentLang = this.languageService.currentLanguage; readonly displayName = this.adminAuth.displayName; readonly initials = computed(() => { const name = this.displayName(); if (!name) { return '?'; } return name .split(/\s+/) .filter(Boolean) .slice(0, 2) .map(part => part[0]?.toUpperCase()) .join(''); }); private readonly breadcrumbSignal = signal([]); private readonly titleKeySignal = signal('adminShell.pages.dashboard.title'); private readonly descriptionKeySignal = signal('adminShell.pages.dashboard.description'); readonly breadcrumb = this.breadcrumbSignal.asReadonly(); readonly pageTitleKey = this.titleKeySignal.asReadonly(); readonly pageDescriptionKey = this.descriptionKeySignal.asReadonly(); constructor() { this.readRouteData(); this.router.events .pipe( filter(event => event instanceof NavigationEnd), takeUntilDestroyed(this.destroyRef), ) .subscribe(() => { this.readRouteData(); this.closeMobileDrawer(); this.notificationsOpen.set(false); }); } @HostListener('document:keydown.escape') onEscape(): void { if (this.mobileDrawerOpen()) { this.closeMobileDrawer(); return; } if (this.notificationsOpen()) { this.notificationsOpen.set(false); } } toggleMobileDrawer(): void { this.mobileDrawerOpen.update(open => !open); if (this.mobileDrawerOpen()) { queueMicrotask(() => { const drawer = this.drawerEl()?.nativeElement; drawer?.querySelector('a, button')?.focus(); }); } } closeMobileDrawer(): void { if (!this.mobileDrawerOpen()) { return; } this.mobileDrawerOpen.set(false); this.menuToggleEl()?.nativeElement.focus(); } toggleNotifications(): void { this.notificationsOpen.update(open => !open); } adminLinkFor(entry: Extract): string[] { const lang = this.currentLang(); if (entry.absolutePath) { return ['/', lang, ...entry.absolutePath]; } return ['/', lang, 'backoffice', ...(entry.path ?? [])]; } breadcrumbLink(entry: AdminBreadcrumbEntry): string[] { return ['/', this.currentLang(), 'backoffice', ...(entry.path ?? [])]; } logout(): void { this.adminAuth.logout(); } cardTitle(key: string): string { return this.translate.t(key); } private readRouteData(): void { let leaf = this.route; while (leaf.firstChild) { leaf = leaf.firstChild; } // Route metadata is optional: during the initial navigation (or for routes // registered without `data`) the leaf snapshot may not exist yet. Fall back // to dashboard defaults instead of crashing the whole admin shell. const data = (leaf.snapshot?.data ?? {}) as { titleKey?: string; descriptionKey?: string; breadcrumb?: AdminBreadcrumbEntry[]; }; this.titleKeySignal.set(data.titleKey ?? 'adminShell.pages.dashboard.title'); this.descriptionKeySignal.set(data.descriptionKey ?? 'adminShell.pages.dashboard.description'); this.breadcrumbSignal.set(data.breadcrumb ?? []); } }