349 lines
11 KiB
TypeScript
349 lines
11 KiB
TypeScript
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<void>();
|
|
|
|
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 || '';
|
|
}
|
|
} |