Add audit logs.
ci-cd-vm / ci-cd (push) Successful in 4m2s

This commit is contained in:
Chris Chen
2026-06-23 12:13:47 -07:00
parent 870eeec82a
commit 62592c29ae
106 changed files with 2522 additions and 311 deletions
@@ -69,7 +69,7 @@
</ng-container>
</div>
<div class="nav-section" *ngIf="showMemberAdminSection || showUserAdminSection">
<div class="nav-section" *ngIf="showMemberAdminSection || showUserAdminSection || showLogsAdminSection">
<h4 *ngIf="!sidebarCollapsed">Administration</h4>
<ng-container *ngFor="let item of memberAdminNavItems">
<a class="nav-item" [class.active]="item.active" *ngIf="isVisible(item)"
@@ -89,6 +89,15 @@
<span *ngIf="!sidebarCollapsed">{{ item.text }}</span>
</a>
</ng-container>
<ng-container *ngFor="let item of logsAdminNavItems">
<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>
</div>
<div class="nav-section" *ngIf="showFinanceSection">
@@ -163,7 +172,7 @@
<!-- Main Content Area -->
<main [class]="mainContentClass">
<!-- Top Header -->
<!-- Unified system header (shared by every child page) -->
<header class="top-header">
<div class="header-left">
<button class="mobile-menu-btn" (click)="toggleSidebar()" *ngIf="isMobile" title="Toggle menu"
@@ -174,29 +183,17 @@
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<div class="breadcrumb">
<span class="breadcrumb-item">{{ currentPageTitle }}</span>
<div class="app-header">
<span class="app-header__eyebrow">River of Life · {{ currentSection }}</span>
<h1 class="app-header__title">
{{ currentPageTitle }}
<span *ngIf="currentPageTitleZh">{{ currentPageTitleZh }}</span>
</h1>
</div>
</div>
<div class="header-right">
<div class="header-actions">
<!-- <button class="action-btn" title="Notifications">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
<div class="notification-badge" *ngIf="unreadNotifications > 0">{{ unreadNotifications }}
</div>
</button>
<button class="action-btn" title="Search">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<path d="M21 21l-4.35-4.35"></path>
</svg>
</button> -->
</div>
<div class="header-right app-header__actions">
<ng-container *ngTemplateOutlet="pageHeader.actions()"></ng-container>
</div>
</header>
@@ -483,11 +483,12 @@
}
.top-header {
padding: 1.5rem 2rem;
padding: 1.25rem 2rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
background: rgba(255, 255, 255, 0.8);
}
@@ -495,6 +496,7 @@
display: flex;
align-items: center;
gap: 1rem;
min-width: 0;
}
.mobile-menu-btn {
@@ -518,14 +520,6 @@
}
}
.breadcrumb {
.breadcrumb-item {
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
}
}
.header-right {
display: flex;
align-items: center;
@@ -1,6 +1,6 @@
import { Component, OnInit, OnDestroy, HostListener } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, NavigationEnd, RouterModule, RouterOutlet } from '@angular/router';
import { ActivatedRoute, Router, NavigationEnd, RouterModule, RouterOutlet } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { IconsModule } from '@progress/kendo-angular-icons';
import {
@@ -23,6 +23,7 @@ import {
lockIcon,
} 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';
@@ -61,6 +62,8 @@ export class UserPortalComponent implements OnInit, OnDestroy {
isMobile = false;
currentUser: UserInfo | null = null;
currentPageTitle = 'Dashboard';
currentPageTitleZh = '';
currentSection = 'Home';
public searchQuery = '';
@@ -86,6 +89,13 @@ export class UserPortalComponent implements OnInit, OnDestroy {
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',
@@ -139,6 +149,7 @@ export class UserPortalComponent implements OnInit, OnDestroy {
public showMemberAdminSection = false;
public showUserAdminSection = false;
public showLogsAdminSection = false;
public showFinanceSection = false;
private destroy$ = new Subject<void>();
@@ -146,7 +157,9 @@ export class UserPortalComponent implements OnInit, OnDestroy {
constructor(
private authService: AuthService,
private permissions: PermissionService,
private router: Router
private router: Router,
private activatedRoute: ActivatedRoute,
public pageHeader: PageHeaderService
) { }
ngOnInit(): void {
@@ -183,6 +196,7 @@ export class UserPortalComponent implements OnInit, OnDestroy {
// 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)));
});
@@ -223,6 +237,7 @@ export class UserPortalComponent implements OnInit, OnDestroy {
...this.mainNavItems,
...this.memberAdminNavItems,
...this.userAdminNavItems,
...this.logsAdminNavItems,
...financeItems,
...this.personalNavItems,
];
@@ -273,49 +288,20 @@ export class UserPortalComponent implements OnInit, OnDestroy {
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 {
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';
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 {