Add role control
This commit is contained in:
@@ -32,50 +32,56 @@
|
||||
</div>
|
||||
|
||||
<nav class="sidebar-nav">
|
||||
<!-- Quick search / filter -->
|
||||
<div class="nav-search" *ngIf="!sidebarCollapsed">
|
||||
<kendo-svgicon [icon]="searchIcon" class="nav-search-icon"></kendo-svgicon>
|
||||
<input type="text" class="nav-search-input" placeholder="Search pages..."
|
||||
[(ngModel)]="searchQuery" aria-label="Search pages">
|
||||
<button type="button" class="nav-search-clear" *ngIf="searchQuery" (click)="clearSearch()"
|
||||
title="Clear search" aria-label="Clear search">
|
||||
<kendo-svgicon [icon]="clearIcon"></kendo-svgicon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4 *ngIf="!sidebarCollapsed">Main</h4>
|
||||
<a *ngFor="let item of mainNavItems" class="nav-item" [class.active]="item.active"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
|
||||
</a>
|
||||
<ng-container *ngFor="let item of mainNavItems">
|
||||
<a class="nav-item" [class.active]="item.active" *ngIf="matchesSearch(item.text)"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4 *ngIf="!sidebarCollapsed">Personal</h4>
|
||||
<a *ngFor="let item of personalNavItems" class="nav-item" [class.active]="item.active"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="nav-section">
|
||||
<h4 *ngIf="!sidebarCollapsed">Management</h4>
|
||||
<a *ngFor="let item of managementNavItems" class="nav-item" [class.active]="item.active"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
|
||||
</a>
|
||||
<ng-container *ngFor="let item of personalNavItems">
|
||||
<a class="nav-item" [class.active]="item.active" *ngIf="matchesSearch(item.text)"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="nav-section" *ngIf="showMemberAdminSection || showUserAdminSection">
|
||||
<h4 *ngIf="!sidebarCollapsed">Administration</h4>
|
||||
<a *ngFor="let item of memberAdminNavItems" class="nav-item" [class.active]="item.active"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
|
||||
</a>
|
||||
<ng-container *ngIf="showUserAdminSection">
|
||||
<a *ngFor="let item of userAdminNavItems" class="nav-item" [class.active]="item.active"
|
||||
<ng-container *ngFor="let item of memberAdminNavItems">
|
||||
<a class="nav-item" [class.active]="item.active" *ngIf="isVisible(item)"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let item of userAdminNavItems">
|
||||
<a class="nav-item" [class.active]="item.active" *ngIf="isVisible(item)"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
@@ -87,13 +93,46 @@
|
||||
|
||||
<div class="nav-section" *ngIf="showFinanceSection">
|
||||
<h4 *ngIf="!sidebarCollapsed">Finance</h4>
|
||||
<a *ngFor="let item of financeNavItems" class="nav-item" [class.active]="item.active"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
|
||||
</a>
|
||||
|
||||
<!-- Collapsed sidebar: flat icon-only list, no group headers -->
|
||||
<ng-container *ngIf="sidebarCollapsed">
|
||||
<ng-container *ngFor="let group of financeGroups">
|
||||
<ng-container *ngFor="let item of group.items">
|
||||
<a *ngIf="canShow(item)" class="nav-item" [class.active]="item.active"
|
||||
[title]="item.text" (click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- Expanded sidebar: collapsible groups -->
|
||||
<ng-container *ngIf="!sidebarCollapsed">
|
||||
<ng-container *ngFor="let group of financeGroups">
|
||||
<div class="nav-group" *ngIf="groupVisible(group)">
|
||||
<button type="button" class="nav-group-header"
|
||||
[class.expanded]="group.expanded || searchQuery"
|
||||
(click)="toggleGroup(group)">
|
||||
<span class="nav-group-title">{{ group.text }}</span>
|
||||
<kendo-svgicon class="nav-group-chevron" [icon]="chevronDownIcon"></kendo-svgicon>
|
||||
</button>
|
||||
<div class="nav-group-items" *ngIf="group.expanded || searchQuery">
|
||||
<ng-container *ngFor="let item of group.items">
|
||||
<a class="nav-item nav-item-nested" [class.active]="item.active"
|
||||
*ngIf="isVisible(item)" [title]="item.text"
|
||||
(click)="navigateTo(item.path)">
|
||||
<div class="nav-icon">
|
||||
<kendo-svgicon [icon]="item.icon"></kendo-svgicon>
|
||||
</div>
|
||||
<span>{{ item.text }}</span>
|
||||
</a>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -200,6 +200,70 @@
|
||||
max-height: calc(100vh - 200px); // Account for header and footer
|
||||
}
|
||||
|
||||
// Quick search / filter box
|
||||
.nav-search {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0 1.25rem 1.5rem;
|
||||
|
||||
.nav-search-icon {
|
||||
position: absolute;
|
||||
left: 0.625rem;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nav-search-input {
|
||||
width: 100%;
|
||||
padding: 0.5rem 2rem 0.5rem 2rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
background: #f9fafb;
|
||||
font-size: 0.85rem;
|
||||
color: #1f2937;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #1e40af;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-search-clear {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: #9ca3af;
|
||||
border-radius: 4px;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
kendo-svgicon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #1e40af;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-section {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@@ -213,6 +277,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Collapsible finance group
|
||||
.nav-group {
|
||||
margin-bottom: 0.25rem;
|
||||
|
||||
.nav-group-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.625rem 1.5rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: #4b5563;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
.nav-group-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-group-chevron {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #9ca3af;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(30, 64, 175, 0.06);
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
&.expanded .nav-group-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-group-items {
|
||||
padding: 0.125rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Nested item inside a group — indent the icon to show hierarchy
|
||||
.nav-item.nav-item-nested {
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,28 +1,30 @@
|
||||
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,
|
||||
calendarIcon,
|
||||
userIcon,
|
||||
groupIcon,
|
||||
usersOutlineIcon,
|
||||
bedOutlineIcon,
|
||||
pillsOutlineIcon,
|
||||
graphIcon,
|
||||
buildingsOutlineIcon,
|
||||
banknoteOutlineIcon,
|
||||
questionCircleIcon,
|
||||
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 {
|
||||
@@ -30,6 +32,15 @@ interface NavItem {
|
||||
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({
|
||||
@@ -37,6 +48,7 @@ interface NavItem {
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterModule,
|
||||
RouterOutlet,
|
||||
IconsModule,
|
||||
@@ -49,57 +61,76 @@ export class UserPortalComponent implements OnInit, OnDestroy {
|
||||
isMobile = false;
|
||||
currentUser: UserInfo | null = null;
|
||||
currentPageTitle = 'Dashboard';
|
||||
unreadMessages = 3;
|
||||
unreadNotifications = 2;
|
||||
|
||||
public searchQuery = '';
|
||||
|
||||
public homeIcon: SVGIcon = homeIcon;
|
||||
public calendarIcon: SVGIcon = calendarIcon;
|
||||
public peopleIcon: SVGIcon = usersOutlineIcon;
|
||||
public bedIcon: SVGIcon = bedOutlineIcon;
|
||||
public userIcon: SVGIcon = userIcon;
|
||||
public pillIcon: SVGIcon = pillsOutlineIcon;
|
||||
public chartIcon: SVGIcon = graphIcon;
|
||||
public buildingIcon: SVGIcon = buildingsOutlineIcon;
|
||||
public creditCardIcon: SVGIcon = banknoteOutlineIcon;
|
||||
public supportIcon: SVGIcon = questionCircleIcon;
|
||||
public searchIcon: SVGIcon = searchIcon;
|
||||
public clearIcon: SVGIcon = xIcon;
|
||||
public chevronDownIcon: SVGIcon = chevronDownIcon;
|
||||
|
||||
public mainNavItems: NavItem[] = [
|
||||
{ text: 'Dashboard', icon: this.homeIcon, path: '/user-portal/dashboard' },
|
||||
// { text: 'Schedule', icon: this.calendarIcon, path: '/user-portal/schedule' },
|
||||
// { text: 'Patients', icon: this.peopleIcon, path: '/user-portal/patients' },
|
||||
];
|
||||
|
||||
public managementNavItems: NavItem[] = [
|
||||
// { text: 'Staff', icon: this.userIcon, path: '/user-portal/staff' },
|
||||
// { text: 'Pharmacy', icon: this.pillIcon, path: '/user-portal/pharmacy' },
|
||||
// { text: 'Reports', icon: this.chartIcon, path: '/user-portal/reports' },1124
|
||||
// { text: 'Departments', icon: this.buildingIcon, path: '/user-portal/departments' },
|
||||
// { text: 'Payments', icon: this.creditCardIcon, path: '/user-portal/payments' },
|
||||
];
|
||||
|
||||
public supportNavItems: NavItem[] = [
|
||||
{ text: 'Support', icon: this.supportIcon, path: '/user-portal/support' },
|
||||
];
|
||||
|
||||
public memberAdminNavItems: NavItem[] = [
|
||||
{ text: 'Members', icon: groupIcon, path: '/user-portal/admin/members' },
|
||||
{ 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' },
|
||||
{ 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 financeNavItems: NavItem[] = [
|
||||
{ text: 'Finance Dashboard', icon: graphIcon, path: '/user-portal/finance/dashboard' },
|
||||
{ text: 'Offering Entry', icon: handIcon, path: '/user-portal/finance/offering-session' },
|
||||
{ text: 'Givings', icon: dollarIcon, path: '/user-portal/finance/givings' },
|
||||
{ text: 'Giving Types', icon: categorizeIcon, path: '/user-portal/finance/giving-categories' },
|
||||
{ text: 'Expenses', icon: moneyExchangeIcon, path: '/user-portal/finance/expenses' },
|
||||
{ text: 'Expense Categories', icon: categorizeIcon, path: '/user-portal/finance/expense-categories' },
|
||||
{ text: 'Disbursements', icon: banknoteOutlineIcon, path: '/user-portal/finance/disbursements' },
|
||||
{ text: 'Check Register', icon: walletOutlineIcon, path: '/user-portal/finance/check-register' },
|
||||
{ text: 'Monthly Statement', icon: fileReportIcon, path: '/user-portal/finance/monthly-statement' },
|
||||
{ text: 'Church Profile', icon: buildingsOutlineIcon, path: '/user-portal/finance/church-profile' },
|
||||
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[] = [
|
||||
@@ -114,6 +145,7 @@ export class UserPortalComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private permissions: PermissionService,
|
||||
private router: Router
|
||||
) { }
|
||||
|
||||
@@ -148,13 +180,22 @@ export class UserPortalComponent implements OnInit, OnDestroy {
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(user => {
|
||||
this.currentUser = user;
|
||||
const roles = user?.roles ?? [];
|
||||
this.showMemberAdminSection = roles.some(r => r === 'super_admin' || r === 'secretary');
|
||||
this.showUserAdminSection = roles.includes('super_admin');
|
||||
this.showFinanceSection = roles.some(r => r === 'finance' || r === 'super_admin');
|
||||
// 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(
|
||||
@@ -176,13 +217,13 @@ export class UserPortalComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private updateActiveStates(currentUrl: string): void {
|
||||
const financeItems: NavItem[] = [];
|
||||
this.financeGroups.forEach(group => financeItems.push(...group.items));
|
||||
const allItems = [
|
||||
...this.mainNavItems,
|
||||
...this.managementNavItems,
|
||||
...this.supportNavItems,
|
||||
...this.memberAdminNavItems,
|
||||
...this.userAdminNavItems,
|
||||
...this.financeNavItems,
|
||||
...financeItems,
|
||||
...this.personalNavItems,
|
||||
];
|
||||
allItems.forEach(item => (item.active = false));
|
||||
@@ -191,6 +232,45 @@ export class UserPortalComponent implements OnInit, OnDestroy {
|
||||
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 {
|
||||
@@ -222,6 +302,7 @@ export class UserPortalComponent implements OnInit, OnDestroy {
|
||||
'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',
|
||||
|
||||
Reference in New Issue
Block a user