import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Router, NavigationEnd, RouterModule, RouterOutlet } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { IconsModule } from '@progress/kendo-angular-icons'; import { SVGIcon, homeIcon, userIcon, groupIcon, graphIcon, buildingsOutlineIcon, banknoteOutlineIcon, dollarIcon, categorizeIcon, moneyExchangeIcon, fileReportIcon, walletOutlineIcon, handIcon, searchIcon, xIcon, chevronDownIcon, lockIcon, } from '@progress/kendo-svg-icons'; import { AuthService, UserInfo } from '../../shared/services/auth.service'; import { PermissionService } from '../../core/services/permission.service'; import { PermissionAction, PermissionModules } from '../../core/models/permission.model'; import { Subject, takeUntil, filter } from 'rxjs'; interface NavItem { text: string; icon: SVGIcon; path: string; active?: boolean; /** When set, the item is shown only if the user has this permission. */ permission?: { module: string; action: PermissionAction }; } interface NavGroup { text: string; icon?: SVGIcon; items: NavItem[]; expanded: boolean; } @Component({ selector: 'app-user-portal', standalone: true, imports: [ CommonModule, FormsModule, RouterModule, RouterOutlet, IconsModule, ], templateUrl: './user-portal.component.html', styleUrls: ['./user-portal.component.scss'] }) export class UserPortalComponent implements OnInit, OnDestroy { sidebarCollapsed = false; isMobile = false; currentUser: UserInfo | null = null; currentPageTitle = 'Dashboard'; public searchQuery = ''; public homeIcon: SVGIcon = homeIcon; public userIcon: SVGIcon = userIcon; public searchIcon: SVGIcon = searchIcon; public clearIcon: SVGIcon = xIcon; public chevronDownIcon: SVGIcon = chevronDownIcon; public mainNavItems: NavItem[] = [ { text: 'Dashboard', icon: this.homeIcon, path: '/user-portal/dashboard' }, ]; public memberAdminNavItems: NavItem[] = [ { text: 'Members', icon: groupIcon, path: '/user-portal/admin/members', permission: { module: PermissionModules.Members, action: 'read' } }, ]; public userAdminNavItems: NavItem[] = [ { text: 'User Management', icon: userIcon, path: '/user-portal/admin/users', permission: { module: PermissionModules.Users, action: 'read' } }, { text: 'Role Permissions', icon: lockIcon, path: '/user-portal/admin/permissions', permission: { module: PermissionModules.Permissions, action: 'read' } }, ]; public financeGroups: NavGroup[] = [ { text: 'Overview', expanded: false, items: [ { text: 'Finance Dashboard', icon: graphIcon, path: '/user-portal/finance/dashboard', permission: { module: PermissionModules.FinanceDashboard, action: 'read' } }, { text: 'Monthly Statement', icon: fileReportIcon, path: '/user-portal/finance/monthly-statement', permission: { module: PermissionModules.MonthlyStatements, action: 'read' } }, ], }, { text: 'Income', expanded: false, items: [ { text: 'Offering Entry', icon: handIcon, path: '/user-portal/finance/offering-session', permission: { module: PermissionModules.OfferingSessions, action: 'read' } }, { text: 'Givings', icon: dollarIcon, path: '/user-portal/finance/givings', permission: { module: PermissionModules.Givings, action: 'read' } }, { text: 'Giving Types', icon: categorizeIcon, path: '/user-portal/finance/giving-categories', permission: { module: PermissionModules.GivingCategories, action: 'read' } }, ], }, { text: 'Expenses', expanded: false, items: [ { text: 'Expenses', icon: moneyExchangeIcon, path: '/user-portal/finance/expenses', permission: { module: PermissionModules.Expenses, action: 'read' } }, { text: 'Expense Categories', icon: categorizeIcon, path: '/user-portal/finance/expense-categories', permission: { module: PermissionModules.ExpenseCategories, action: 'read' } }, { text: 'Disbursements', icon: banknoteOutlineIcon, path: '/user-portal/finance/disbursements', permission: { module: PermissionModules.Disbursements, action: 'read' } }, { text: 'Check Register', icon: walletOutlineIcon, path: '/user-portal/finance/check-register', permission: { module: PermissionModules.Disbursements, action: 'read' } }, ], }, { text: 'Settings', expanded: false, items: [ { text: 'Church Profile', icon: buildingsOutlineIcon, path: '/user-portal/finance/church-profile', permission: { module: PermissionModules.ChurchProfile, action: 'read' } }, ], }, ]; public personalNavItems: NavItem[] = [ { text: 'My Reimbursements', icon: walletOutlineIcon, path: '/user-portal/reimbursements' }, ]; public showMemberAdminSection = false; public showUserAdminSection = false; public showFinanceSection = false; private destroy$ = new Subject(); constructor( private authService: AuthService, private permissions: PermissionService, private router: Router ) { } ngOnInit(): void { this.checkScreenSize(); this.setupUserSubscription(); this.setupRouteSubscription(); } ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); } @HostListener('window:resize', ['$event']) onResize(event: any): void { this.checkScreenSize(); } private checkScreenSize(): void { this.isMobile = window.innerWidth <= 768; if (this.isMobile) { this.sidebarCollapsed = true; } else { // On desktop, start with sidebar expanded this.sidebarCollapsed = false; } } private setupUserSubscription(): void { this.authService.currentUser$ .pipe(takeUntil(this.destroy$)) .subscribe(user => { this.currentUser = user; // Section visibility is derived from effective permissions (super_admin → all). this.showMemberAdminSection = this.memberAdminNavItems.some(item => this.canShow(item)); this.showUserAdminSection = this.userAdminNavItems.some(item => this.canShow(item)); this.showFinanceSection = this.financeGroups .some(group => group.items.some(item => this.canShow(item))); }); } /** True if a nav item should be shown — items without a permission are always visible. */ public canShow(item: NavItem): boolean { if (!item.permission) { return true; } return this.permissions.can(item.permission.module, item.permission.action); } private setupRouteSubscription(): void { this.router.events .pipe( filter(event => event instanceof NavigationEnd), takeUntil(this.destroy$) ) .subscribe((event: NavigationEnd) => { this.updateActiveStates(event.url); this.updatePageTitle(); }); this.updateActiveStates(this.router.url); this.updatePageTitle(); } public navigateTo(path: string): void { this.router.navigate([path]); this.onNavigationClick(); } private updateActiveStates(currentUrl: string): void { const financeItems: NavItem[] = []; this.financeGroups.forEach(group => financeItems.push(...group.items)); const allItems = [ ...this.mainNavItems, ...this.memberAdminNavItems, ...this.userAdminNavItems, ...financeItems, ...this.personalNavItems, ]; allItems.forEach(item => (item.active = false)); const activeItem = allItems.find(item => currentUrl.startsWith(item.path)); if (activeItem) { activeItem.active = true; } // Auto-expand the finance group that contains the active page so the // current location is visible on load/navigation. const activeGroup = this.financeGroups.find(group => group.items.some(item => item.active) ); if (activeGroup) { activeGroup.expanded = true; } } public toggleGroup(group: NavGroup): void { group.expanded = !group.expanded; } public matchesSearch(text: string): boolean { const query = this.searchQuery.trim().toLowerCase(); if (!query) { return true; } return text.toLowerCase().includes(query); } public groupHasMatch(group: NavGroup): boolean { return group.items.some(item => this.matchesSearch(item.text)); } /** Combined search + permission filter for a single nav item. */ public isVisible(item: NavItem): boolean { return this.matchesSearch(item.text) && this.canShow(item); } /** True if a finance group has at least one visible (permitted + matching) item. */ public groupVisible(group: NavGroup): boolean { return group.items.some(item => this.isVisible(item)); } public clearSearch(): void { this.searchQuery = ''; } private updatePageTitle(): void { const url = this.router.url; const segments = url.split('/').filter(s => s); const key = segments.length >= 3 ? `${segments[1]}/${segments[2]}` // e.g. 'admin/members' : segments[1] ?? ''; this.currentPageTitle = this.getPageTitle(key); } private getPageTitle(page: string): string { const titles: { [key: string]: string } = { 'dashboard': 'Dashboard', 'schedule': 'Schedule', 'patients': 'Patients', 'bed-management': 'Bed Management', 'staff': 'Staff', 'pharmacy': 'Pharmacy', 'reports': 'Reports', 'departments': 'Departments', 'payments': 'Payments', 'support': 'Support', 'transactions': 'Escrow Transactions', 'tasks': 'Tasks & Todos', 'contacts': 'Contacts', 'documents': 'Documents', 'messages': 'Messages', 'settings': 'Settings', 'admin/members': 'Member Management', 'admin/users': 'User Management', 'admin/permissions': 'Role Permissions', 'finance/dashboard': 'Finance Dashboard', 'finance/offering-session': 'Sunday Offering Entry', 'finance/givings': 'Givings', 'finance/giving-categories': 'Giving Types', 'reimbursements': 'My Reimbursements', 'finance/expenses': 'Expenses', 'finance/expense-categories': 'Expense Categories', 'finance/disbursements': 'Disbursement Management', 'finance/check-register': 'Check Register', 'finance/monthly-statement': 'Monthly Statement', 'finance/church-profile': 'Church Profile', }; return titles[page] ?? 'Dashboard'; } toggleSidebar(): void { this.sidebarCollapsed = !this.sidebarCollapsed; } get mainContentClass(): string { return this.sidebarCollapsed ? 'main-content sidebar-collapsed' : 'main-content'; } onSidebarOverlayClick(): void { if (this.isMobile && !this.sidebarCollapsed) { this.sidebarCollapsed = true; } } onNavigationClick(): void { if (this.isMobile) { this.sidebarCollapsed = true; } } logout(): void { this.authService.logout(); this.router.navigate(['/login']); } getDisplayName(): string { return this.currentUser?.email || ''; } }