import { Component, OnInit, OnDestroy, HostListener } from '@angular/core'; import { CommonModule } from '@angular/common'; import { ActivatedRoute, 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, gearIcon, } from '@progress/kendo-svg-icons'; import { AuthService, UserInfo } from '../../shared/services/auth.service'; import { PageHeaderService } from '../../shared/services/page-header.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'; currentPageTitleZh = ''; currentSection = 'Home'; 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' } }, { text: 'Ministries', icon: groupIcon, path: '/user-portal/admin/ministries', permission: { module: PermissionModules.Ministries, 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 logsAdminNavItems: NavItem[] = [ { text: 'System Logs', icon: fileReportIcon, path: '/user-portal/admin/logs/system', permission: { module: PermissionModules.SystemLogs, action: 'read' } }, { text: 'Audit Logs', icon: fileReportIcon, path: '/user-portal/admin/logs/audit', permission: { module: PermissionModules.AuditLogs, 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: 'Form 990 Report', icon: fileReportIcon, path: '/user-portal/finance/form-990-report', permission: { module: PermissionModules.Form990Report, 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: 'Expense Snapshots', icon: categorizeIcon, path: '/user-portal/finance/expense-snapshots', permission: { module: PermissionModules.Expenses, action: 'write' } }, { 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' }, { text: 'Account Settings', icon: gearIcon, path: '/user-portal/account' }, ]; public showMemberAdminSection = false; public showUserAdminSection = false; public showLogsAdminSection = false; public showFinanceSection = false; private destroy$ = new Subject(); constructor( private authService: AuthService, private permissions: PermissionService, private router: Router, private activatedRoute: ActivatedRoute, public pageHeader: PageHeaderService ) { } 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.showLogsAdminSection = this.logsAdminNavItems.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, ...this.logsAdminNavItems, ...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 = ''; } /** * The unified header reads its title/subtitle/section from the active route's * `data` (see app.routes.ts). Walk to the deepest activated child so nested * routes win, and fall back gracefully when a route carries no metadata. */ private updatePageTitle(): void { let route = this.activatedRoute; while (route.firstChild) { route = route.firstChild; } const data = route.snapshot.data; this.currentPageTitle = data['title'] ?? 'Dashboard'; this.currentPageTitleZh = data['titleZh'] ?? ''; this.currentSection = data['section'] ?? 'Home'; } 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 { const member = this.currentUser?.memberInfo; if (member) { return `${member.nickName ?? member.firstName_en} ${member.lastName_en}`; } return this.currentUser?.email || ''; } }