Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
54 KiB
Member & User Management — Part 3: Angular Frontend (Tasks 10–16)
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-developmentorsuperpowers:executing-plansto implement task-by-task. Prerequisite: Parts 1 & 2 complete (API running at configuredenvironment.apiUrl).
Goal: Build Member Management page (Kendo Grid + form dialog + create-user-account dialog) and User Management page (Kendo Grid + edit dialog), wired into the sidebar.
Architecture: Two standalone feature folders under features/members and features/users. Custom Angular services (not extending CrudBaseApiService) because the API returns PagedResult<T>, not T[]. All components are standalone. Dialogs are embedded in the page component template.
Tech Stack: Angular 18, Kendo Angular Grid/Dialog/Inputs/Layout, Reactive Forms
Spec: docs/superpowers/specs/2026-05-27-aspnetusers-member-management-design.md
File Structure
APP/src/app/
features/
members/
models/
member.model.ts ← NEW
services/
member-api.service.ts ← NEW
components/
member-form-dialog/
member-form-dialog.component.ts ← NEW
member-form-dialog.component.html← NEW
create-user-dialog/
create-user-dialog.component.ts ← NEW
create-user-dialog.component.html← NEW
pages/
members-page/
members-page.component.ts ← NEW
members-page.component.html ← NEW
members-page.component.scss ← NEW
users/
models/
user.model.ts ← NEW
services/
user-api.service.ts ← NEW
components/
edit-user-dialog/
edit-user-dialog.component.ts ← NEW
edit-user-dialog.component.html ← NEW
pages/
users-page/
users-page.component.ts ← NEW
users-page.component.html ← NEW
users-page.component.scss ← NEW
app.routes.ts ← MODIFY
portals/user-portal/
components/user-navbar/
user-navbar.component.ts ← MODIFY
user-navbar.component.html ← MODIFY
user-portal.component.ts ← MODIFY (add getPageTitle entries)
Task 10: Angular Models + API Services
Files:
-
Create:
APP/src/app/features/members/models/member.model.ts -
Create:
APP/src/app/features/members/services/member-api.service.ts -
Create:
APP/src/app/features/users/models/user.model.ts -
Create:
APP/src/app/features/users/services/user-api.service.ts -
Step 1: Create
member.model.ts
// APP/src/app/features/members/models/member.model.ts
export type MemberStatus = 'Member' | 'Visitor' | 'Inactive' | 'Former';
export interface MemberListItemDto {
id: number;
firstName_en: string;
lastName_en: string;
nickName: string | null;
firstName_zh: string | null;
lastName_zh: string | null;
status: MemberStatus;
email: string | null;
phoneCell: string | null;
joinDate: string | null;
linkedUserId: string | null;
}
export interface MemberDto extends MemberListItemDto {
gender: string | null;
dateOfBirth: string | null;
baptismDate: string | null;
baptismChurch: string | null;
phoneHome: string | null;
address: string | null;
city: string | null;
state: string | null;
zipCode: string | null;
country: string;
photoBlobPath: string | null;
languagePreference: string;
notes: string | null;
familyUnitId: number | null;
createdAt: string;
updatedAt: string;
}
export interface CreateMemberRequest {
firstName_en: string;
lastName_en: string;
nickName: string | null;
firstName_zh: string | null;
lastName_zh: string | null;
gender: string | null;
dateOfBirth: string | null;
baptismDate: string | null;
baptismChurch: string | null;
email: string | null;
phoneCell: string | null;
phoneHome: string | null;
address: string | null;
city: string | null;
state: string | null;
zipCode: string | null;
country: string;
status: string;
languagePreference: string;
joinDate: string | null;
notes: string | null;
familyUnitId: number | null;
}
export type UpdateMemberRequest = CreateMemberRequest;
export interface PagedResult<T> {
items: T[];
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
}
export interface MemberQueryParams {
page?: number;
pageSize?: number;
search?: string;
status?: string;
hasUser?: boolean;
}
/** Display name: NickName (if present) else FirstName_en, plus LastName_en */
export function memberDisplayName(
m: Pick<MemberListItemDto, 'nickName' | 'firstName_en' | 'lastName_en'>
): string {
return `${m.nickName ?? m.firstName_en} ${m.lastName_en}`;
}
- Step 2: Create
member-api.service.ts
// APP/src/app/features/members/services/member-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { ApiConfigService } from '../../../core/services/api-config.service';
import {
MemberDto, MemberListItemDto, CreateMemberRequest,
UpdateMemberRequest, MemberQueryParams, PagedResult
} from '../models/member.model';
@Injectable({ providedIn: 'root' })
export class MemberApiService {
private readonly endpoint: string;
constructor(private http: HttpClient, apiConfig: ApiConfigService) {
this.endpoint = apiConfig.getApiUrl('members');
}
getPaged(params: MemberQueryParams = {}): Observable<PagedResult<MemberListItemDto>> {
let p = new HttpParams()
.set('page', params.page ?? 1)
.set('pageSize', params.pageSize ?? 20);
if (params.search !== undefined && params.search !== '') p = p.set('search', params.search);
if (params.status !== undefined && params.status !== '') p = p.set('status', params.status);
if (params.hasUser !== undefined) p = p.set('hasUser', params.hasUser);
return this.http.get<PagedResult<MemberListItemDto>>(this.endpoint, { params: p });
}
getById(id: number): Observable<MemberDto> {
return this.http.get<MemberDto>(`${this.endpoint}/${id}`);
}
create(request: CreateMemberRequest): Observable<{ id: number }> {
return this.http.post<{ id: number }>(this.endpoint, request);
}
update(id: number, request: UpdateMemberRequest): Observable<void> {
return this.http.put<void>(`${this.endpoint}/${id}`, request);
}
delete(id: number): Observable<void> {
return this.http.delete<void>(`${this.endpoint}/${id}`);
}
}
- Step 3: Create
user.model.ts
// APP/src/app/features/users/models/user.model.ts
export interface UserListItemDto {
id: string;
email: string;
memberId: number | null;
memberDisplayName: string | null;
roles: string[];
isActive: boolean;
languagePreference: string;
lastLoginAt: string | null;
createdAt: string;
}
export type UserDto = UserListItemDto;
export interface CreateUserRequest {
memberId: number;
email: string;
roles: string[];
languagePreference: string;
}
export interface CreateUserResult {
userId: string;
tempPassword: string;
}
export interface UpdateUserRequest {
email: string;
roles: string[];
isActive: boolean;
languagePreference: string;
}
export interface UserQueryParams {
page?: number;
pageSize?: number;
search?: string;
}
export interface PagedResult<T> {
items: T[];
totalCount: number;
page: number;
pageSize: number;
totalPages: number;
}
export const ALL_ROLES = [
'super_admin','pastor','board_member','coworker_chair','ministry_leader',
'district_leader','cell_leader','coworker','finance','secretary',
'worship_leader','member','visitor'
] as const;
- Step 4: Create
user-api.service.ts
// APP/src/app/features/users/services/user-api.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { ApiConfigService } from '../../../core/services/api-config.service';
import {
UserDto, UserListItemDto, CreateUserRequest, CreateUserResult,
UpdateUserRequest, UserQueryParams, PagedResult
} from '../models/user.model';
@Injectable({ providedIn: 'root' })
export class UserApiService {
private readonly endpoint: string;
constructor(private http: HttpClient, apiConfig: ApiConfigService) {
this.endpoint = apiConfig.getApiUrl('users');
}
getPaged(params: UserQueryParams = {}): Observable<PagedResult<UserListItemDto>> {
let p = new HttpParams()
.set('page', params.page ?? 1)
.set('pageSize', params.pageSize ?? 20);
if (params.search) p = p.set('search', params.search);
return this.http.get<PagedResult<UserListItemDto>>(this.endpoint, { params: p });
}
getById(id: string): Observable<UserDto> {
return this.http.get<UserDto>(`${this.endpoint}/${id}`);
}
createUser(request: CreateUserRequest): Observable<CreateUserResult> {
return this.http.post<CreateUserResult>(this.endpoint, request);
}
update(id: string, request: UpdateUserRequest): Observable<void> {
return this.http.put<void>(`${this.endpoint}/${id}`, request);
}
deactivate(id: string): Observable<void> {
return this.http.delete<void>(`${this.endpoint}/${id}`);
}
resetPassword(id: string): Observable<{ tempPassword: string }> {
return this.http.post<{ tempPassword: string }>(
`${this.endpoint}/${id}/reset-password`, {});
}
}
- Step 5: Build
cd APP && ng build --configuration development
Expected: Compiled successfully.
- Step 6: Commit
git add APP/src/app/features/
git commit -m "feat: add Angular member and user models + API services"
Task 11: MemberFormDialogComponent
Files:
-
Create:
APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.ts -
Create:
APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html -
Step 1: Create
member-form-dialog.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { DialogsModule } from '@progress/kendo-angular-dialog';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { LabelModule } from '@progress/kendo-angular-label';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
import { LayoutModule } from '@progress/kendo-angular-layout';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { MemberDto, CreateMemberRequest } from '../../models/member.model';
@Component({
selector: 'app-member-form-dialog',
standalone: true,
imports: [
CommonModule, ReactiveFormsModule, DialogsModule, InputsModule,
LabelModule, DropDownsModule, DateInputsModule, LayoutModule, ButtonsModule
],
templateUrl: './member-form-dialog.component.html',
})
export class MemberFormDialogComponent implements OnInit {
@Input() member: MemberDto | null = null; // null = create mode
@Output() saved = new EventEmitter<CreateMemberRequest>();
@Output() cancelled = new EventEmitter<void>();
form!: FormGroup;
isEditMode = false;
readonly statusOptions = ['Member', 'Visitor', 'Inactive', 'Former'];
readonly genderOptions = [
{ text: 'Male', value: 'M' },
{ text: 'Female', value: 'F' },
{ text: 'Other', value: 'Other' },
];
readonly langOptions = [
{ text: 'English', value: 'en' },
{ text: '中文', value: 'zh-TW' },
];
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.isEditMode = this.member !== null;
this.form = this.fb.group({
// Basic Info
firstName_en: [this.member?.firstName_en ?? '', [Validators.required, Validators.maxLength(100)]],
lastName_en: [this.member?.lastName_en ?? '', [Validators.required, Validators.maxLength(100)]],
nickName: [this.member?.nickName ?? null, Validators.maxLength(100)],
firstName_zh: [this.member?.firstName_zh ?? null, Validators.maxLength(100)],
lastName_zh: [this.member?.lastName_zh ?? null, Validators.maxLength(100)],
gender: [this.member?.gender ?? null],
dateOfBirth: [this.member?.dateOfBirth ?? null],
status: [this.member?.status ?? 'Member', Validators.required],
languagePreference: [this.member?.languagePreference ?? 'en', Validators.required],
// Contact
email: [this.member?.email ?? null, [Validators.email, Validators.maxLength(200)]],
phoneCell:[this.member?.phoneCell ?? null, Validators.maxLength(30)],
phoneHome:[this.member?.phoneHome ?? null, Validators.maxLength(30)],
address: [this.member?.address ?? null, Validators.maxLength(500)],
city: [this.member?.city ?? null, Validators.maxLength(100)],
state: [this.member?.state ?? null, Validators.maxLength(50)],
zipCode: [this.member?.zipCode ?? null, Validators.maxLength(20)],
country: [this.member?.country ?? 'USA', Validators.maxLength(100)],
// Church Info
joinDate: [this.member?.joinDate ?? null],
baptismDate: [this.member?.baptismDate ?? null],
baptismChurch:[this.member?.baptismChurch ?? null, Validators.maxLength(200)],
notes: [this.member?.notes ?? null],
});
}
get title(): string {
return this.isEditMode ? 'Edit Member' : 'Add Member';
}
onSubmit(): void {
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
this.saved.emit(this.form.value as CreateMemberRequest);
}
onCancel(): void {
this.cancelled.emit();
}
}
- Step 2: Create
member-form-dialog.component.html
<kendo-dialog [title]="title" (close)="onCancel()" [minWidth]="600" [width]="750">
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<kendo-tabstrip>
<!-- TAB 1: Basic Info -->
<kendo-tabstrip-tab title="Basic Info" [selected]="true">
<ng-template kendoTabContent>
<div class="k-form k-form-horizontal k-mt-4">
<kendo-formfield>
<kendo-label text="Legal First Name *"></kendo-label>
<kendo-textbox formControlName="firstName_en"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Last Name *"></kendo-label>
<kendo-textbox formControlName="lastName_en"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Nick Name (Common Name)"></kendo-label>
<kendo-textbox formControlName="nickName" placeholder="e.g. Chris"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Chinese First Name"></kendo-label>
<kendo-textbox formControlName="firstName_zh"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Chinese Last Name"></kendo-label>
<kendo-textbox formControlName="lastName_zh"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Gender"></kendo-label>
<kendo-dropdownlist
formControlName="gender"
[data]="genderOptions"
textField="text" valueField="value"
[defaultItem]="{ text: '-- Select --', value: null }">
</kendo-dropdownlist>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Date of Birth"></kendo-label>
<kendo-datepicker formControlName="dateOfBirth"></kendo-datepicker>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Status *"></kendo-label>
<kendo-dropdownlist formControlName="status" [data]="statusOptions">
</kendo-dropdownlist>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Language"></kendo-label>
<kendo-dropdownlist
formControlName="languagePreference"
[data]="langOptions" textField="text" valueField="value">
</kendo-dropdownlist>
</kendo-formfield>
</div>
</ng-template>
</kendo-tabstrip-tab>
<!-- TAB 2: Contact -->
<kendo-tabstrip-tab title="Contact">
<ng-template kendoTabContent>
<div class="k-form k-form-horizontal k-mt-4">
<kendo-formfield>
<kendo-label text="Email"></kendo-label>
<kendo-textbox formControlName="email" type="email"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Cell Phone"></kendo-label>
<kendo-textbox formControlName="phoneCell"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Home Phone"></kendo-label>
<kendo-textbox formControlName="phoneHome"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Address"></kendo-label>
<kendo-textbox formControlName="address"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="City"></kendo-label>
<kendo-textbox formControlName="city"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="State"></kendo-label>
<kendo-textbox formControlName="state"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Zip Code"></kendo-label>
<kendo-textbox formControlName="zipCode"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Country"></kendo-label>
<kendo-textbox formControlName="country"></kendo-textbox>
</kendo-formfield>
</div>
</ng-template>
</kendo-tabstrip-tab>
<!-- TAB 3: Church Info -->
<kendo-tabstrip-tab title="Church Info">
<ng-template kendoTabContent>
<div class="k-form k-form-horizontal k-mt-4">
<kendo-formfield>
<kendo-label text="Join Date"></kendo-label>
<kendo-datepicker formControlName="joinDate"></kendo-datepicker>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Baptism Date"></kendo-label>
<kendo-datepicker formControlName="baptismDate"></kendo-datepicker>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Baptism Church"></kendo-label>
<kendo-textbox formControlName="baptismChurch"></kendo-textbox>
</kendo-formfield>
<kendo-formfield>
<kendo-label text="Notes"></kendo-label>
<kendo-textarea formControlName="notes" [rows]="4"></kendo-textarea>
</kendo-formfield>
</div>
</ng-template>
</kendo-tabstrip-tab>
</kendo-tabstrip>
</form>
<kendo-dialog-actions>
<button kendoButton (click)="onCancel()">Cancel</button>
<button kendoButton themeColor="primary" (click)="onSubmit()" [disabled]="form.invalid">
{{ isEditMode ? 'Save Changes' : 'Add Member' }}
</button>
</kendo-dialog-actions>
</kendo-dialog>
- Step 3: Build
cd APP && ng build --configuration development
Expected: Compiled successfully.
- Step 4: Commit
git add APP/src/app/features/members/components/member-form-dialog/
git commit -m "feat: add MemberFormDialogComponent (3-tab form)"
Task 12: CreateUserAccountDialogComponent
Files:
-
Create:
APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.ts -
Create:
APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.html -
Step 1: Create
create-user-dialog.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { DialogsModule } from '@progress/kendo-angular-dialog';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { LabelModule } from '@progress/kendo-angular-label';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { IndicatorsModule } from '@progress/kendo-angular-indicators';
import { MemberListItemDto, memberDisplayName } from '../../models/member.model';
import { CreateUserRequest, CreateUserResult, ALL_ROLES } from '../../../users/models/user.model';
import { UserApiService } from '../../../users/services/user-api.service';
@Component({
selector: 'app-create-user-dialog',
standalone: true,
imports: [
CommonModule, ReactiveFormsModule, DialogsModule, InputsModule,
LabelModule, DropDownsModule, ButtonsModule, IndicatorsModule
],
templateUrl: './create-user-dialog.component.html',
})
export class CreateUserDialogComponent implements OnInit {
@Input({ required: true }) member!: MemberListItemDto;
@Output() created = new EventEmitter<void>();
@Output() cancelled = new EventEmitter<void>();
form!: FormGroup;
step: 'form' | 'success' = 'form';
tempPassword = '';
copied = false;
isLoading = false;
errorMessage = '';
readonly roleOptions = [...ALL_ROLES];
readonly langOptions = [
{ text: 'English', value: 'en' },
{ text: '中文', value: 'zh-TW' },
];
get memberName(): string { return memberDisplayName(this.member); }
constructor(private fb: FormBuilder, private userApi: UserApiService) {}
ngOnInit(): void {
this.form = this.fb.group({
email: [this.member.email ?? '', [Validators.required, Validators.email]],
roles: [['member'], Validators.required],
languagePreference: [this.member.languagePreference ?? 'en'],
});
}
onSubmit(): void {
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
this.isLoading = true;
this.errorMessage = '';
const request: CreateUserRequest = {
memberId: this.member.id,
email: this.form.value.email,
roles: this.form.value.roles,
languagePreference: this.form.value.languagePreference,
};
this.userApi.createUser(request).subscribe({
next: (result: CreateUserResult) => {
this.tempPassword = result.tempPassword;
this.step = 'success';
this.isLoading = false;
},
error: (err: any) => {
this.errorMessage = err.error?.message ?? 'Failed to create account.';
this.isLoading = false;
},
});
}
copyPassword(): void {
navigator.clipboard.writeText(this.tempPassword).then(() => {
this.copied = true;
setTimeout(() => (this.copied = false), 2000);
});
}
onDone(): void {
this.created.emit();
}
onCancel(): void {
this.cancelled.emit();
}
}
- Step 2: Create
create-user-dialog.component.html
<kendo-dialog title="Create User Account" (close)="onCancel()" [minWidth]="480" [width]="520">
<!-- STEP 1: Form -->
<ng-container *ngIf="step === 'form'">
<p class="k-mb-4">Creating account for <strong>{{ memberName }}</strong></p>
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="k-form k-form-vertical">
<kendo-formfield>
<kendo-label text="Login Email *"></kendo-label>
<kendo-textbox formControlName="email" type="email"></kendo-textbox>
<kendo-formerror *ngIf="form.get('email')?.errors?.['required']">Email is required.</kendo-formerror>
<kendo-formerror *ngIf="form.get('email')?.errors?.['email']">Invalid email address.</kendo-formerror>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-label text="Roles *"></kendo-label>
<kendo-multiselect
formControlName="roles"
[data]="roleOptions"
placeholder="Select role(s)">
</kendo-multiselect>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-label text="Language"></kendo-label>
<kendo-dropdownlist
formControlName="languagePreference"
[data]="langOptions" textField="text" valueField="value">
</kendo-dropdownlist>
</kendo-formfield>
<p *ngIf="errorMessage" class="k-color-error k-mt-3">{{ errorMessage }}</p>
</form>
<kendo-dialog-actions>
<button kendoButton (click)="onCancel()">Cancel</button>
<button kendoButton themeColor="primary" (click)="onSubmit()" [disabled]="isLoading">
<kendo-loader *ngIf="isLoading" size="small"></kendo-loader>
Create Account
</button>
</kendo-dialog-actions>
</ng-container>
<!-- STEP 2: Success — show temp password -->
<ng-container *ngIf="step === 'success'">
<div class="k-text-center k-p-4">
<p class="k-font-size-lg k-mb-2">✅ Account created!</p>
<p class="k-mb-4">Share this temporary password with <strong>{{ memberName }}</strong>.</p>
<div class="k-d-flex k-justify-content-center k-align-items-center k-gap-2 k-mb-3">
<code class="k-font-size-lg k-border k-p-2 k-rounded">{{ tempPassword }}</code>
<button kendoButton (click)="copyPassword()">{{ copied ? 'Copied!' : 'Copy' }}</button>
</div>
<p class="k-color-warning k-font-weight-bold">
⚠️ This password will not be shown again.
</p>
</div>
<kendo-dialog-actions>
<button kendoButton themeColor="primary" (click)="onDone()">Done</button>
</kendo-dialog-actions>
</ng-container>
</kendo-dialog>
- Step 3: Build
cd APP && ng build --configuration development
Expected: Compiled successfully.
- Step 4: Commit
git add APP/src/app/features/members/components/create-user-dialog/
git commit -m "feat: add CreateUserAccountDialogComponent with temp-password reveal"
Task 13: MembersPageComponent + Routing
Files:
-
Create:
APP/src/app/features/members/pages/members-page/members-page.component.ts -
Create:
APP/src/app/features/members/pages/members-page/members-page.component.html -
Create:
APP/src/app/features/members/pages/members-page/members-page.component.scss -
Step 1: Create
members-page.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { GridModule, PageChangeEvent } from '@progress/kendo-angular-grid';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { IndicatorsModule } from '@progress/kendo-angular-indicators';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { MemberApiService } from '../../services/member-api.service';
import { MemberFormDialogComponent } from '../../components/member-form-dialog/member-form-dialog.component';
import { CreateUserDialogComponent } from '../../components/create-user-dialog/create-user-dialog.component';
import {
MemberListItemDto, MemberDto, CreateMemberRequest,
PagedResult, memberDisplayName
} from '../../models/member.model';
@Component({
selector: 'app-members-page',
standalone: true,
imports: [
CommonModule, FormsModule, GridModule, InputsModule,
ButtonsModule, IndicatorsModule, DropDownsModule,
MemberFormDialogComponent, CreateUserDialogComponent,
],
templateUrl: './members-page.component.html',
styleUrls: ['./members-page.component.scss'],
})
export class MembersPageComponent implements OnInit {
// Grid state
data: MemberListItemDto[] = [];
totalCount = 0;
page = 1;
pageSize = 20;
isLoading = false;
// Filters
searchText = '';
filterStatus = '';
readonly statusOptions = ['', 'Member', 'Visitor', 'Inactive', 'Former'];
// Dialogs
showMemberDialog = false;
showCreateUserDialog = false;
editingMember: MemberDto | null = null;
selectedMemberForUser: MemberListItemDto | null = null;
readonly memberDisplayName = memberDisplayName;
constructor(private memberApi: MemberApiService) {}
ngOnInit(): void {
this.loadData();
}
loadData(): void {
this.isLoading = true;
this.memberApi.getPaged({
page: this.page,
pageSize: this.pageSize,
search: this.searchText || undefined,
status: this.filterStatus || undefined,
}).subscribe({
next: (result: PagedResult<MemberListItemDto>) => {
this.data = result.items;
this.totalCount = result.totalCount;
this.isLoading = false;
},
error: () => { this.isLoading = false; }
});
}
onPageChange(event: PageChangeEvent): void {
this.page = event.skip / this.pageSize + 1;
this.pageSize = event.take;
this.loadData();
}
onSearch(): void {
this.page = 1;
this.loadData();
}
// ── Member CRUD ─────────────────────────────────────────────────────────────
openAddDialog(): void {
this.editingMember = null;
this.showMemberDialog = true;
}
openEditDialog(member: MemberListItemDto): void {
this.memberApi.getById(member.id).subscribe(dto => {
this.editingMember = dto;
this.showMemberDialog = true;
});
}
closeMemberDialog(): void {
this.showMemberDialog = false;
this.editingMember = null;
}
onMemberSaved(request: CreateMemberRequest): void {
if (this.editingMember) {
this.memberApi.update(this.editingMember.id, request).subscribe(() => {
this.closeMemberDialog();
this.loadData();
});
} else {
this.memberApi.create(request).subscribe(() => {
this.closeMemberDialog();
this.loadData();
});
}
}
deleteMember(member: MemberListItemDto): void {
if (!confirm(`Delete ${memberDisplayName(member)}? This cannot be undone.`)) return;
this.memberApi.delete(member.id).subscribe(() => this.loadData());
}
// ── Create User Account ─────────────────────────────────────────────────────
openCreateUserDialog(member: MemberListItemDto): void {
this.selectedMemberForUser = member;
this.showCreateUserDialog = true;
}
closeCreateUserDialog(): void {
this.showCreateUserDialog = false;
this.selectedMemberForUser = null;
}
onUserCreated(): void {
this.closeCreateUserDialog();
this.loadData();
}
}
- Step 2: Create
members-page.component.html
<div class="k-p-4">
<!-- Toolbar -->
<div class="k-d-flex k-justify-content-between k-align-items-center k-mb-4">
<h2 class="k-m-0">Member Management</h2>
<button kendoButton themeColor="primary" (click)="openAddDialog()">+ Add Member</button>
</div>
<!-- Filters -->
<div class="k-d-flex k-gap-3 k-mb-4">
<kendo-textbox
[(ngModel)]="searchText"
placeholder="Search name, nick name, email…"
(keyup.enter)="onSearch()"
style="width: 300px">
</kendo-textbox>
<kendo-dropdownlist
[(ngModel)]="filterStatus"
[data]="statusOptions"
[defaultItem]="'All Status'"
(valueChange)="onSearch()"
style="width: 160px">
</kendo-dropdownlist>
<button kendoButton (click)="onSearch()">Search</button>
</div>
<!-- Grid -->
<kendo-grid
[data]="{ data: data, total: totalCount }"
[pageSize]="pageSize"
[skip]="(page - 1) * pageSize"
[pageable]="{ pageSizes: [10, 20, 50] }"
[sortable]="false"
(pageChange)="onPageChange($event)">
<kendo-grid-column title="Name" [width]="200">
<ng-template kendoGridCellTemplate let-row>
<div>
<strong>{{ memberDisplayName(row) }}</strong>
<span *ngIf="row.firstName_zh || row.lastName_zh" class="k-ml-1 k-color-subtle">
({{ row.lastName_zh }}{{ row.firstName_zh }})
</span>
</div>
<div *ngIf="row.nickName && row.nickName !== row.firstName_en" class="k-font-size-sm k-color-subtle">
Legal: {{ row.firstName_en }}
</div>
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Status" [width]="100">
<ng-template kendoGridCellTemplate let-row>
<kendo-badge
[themeColor]="row.status === 'Member' ? 'success' :
row.status === 'Visitor' ? 'info' :
row.status === 'Former' ? 'warning' : 'base'">
{{ row.status }}
</kendo-badge>
</ng-template>
</kendo-grid-column>
<kendo-grid-column field="email" title="Email" [width]="200"></kendo-grid-column>
<kendo-grid-column field="phoneCell" title="Phone" [width]="130"></kendo-grid-column>
<kendo-grid-column field="joinDate" title="Joined" [width]="110"></kendo-grid-column>
<kendo-grid-column title="Account" [width]="100">
<ng-template kendoGridCellTemplate let-row>
<kendo-badge [themeColor]="row.linkedUserId ? 'success' : 'base'">
{{ row.linkedUserId ? '✓ User' : '—' }}
</kendo-badge>
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Actions" [width]="200" [locked]="false">
<ng-template kendoGridCellTemplate let-row>
<div class="k-d-flex k-gap-2">
<button kendoButton size="small" (click)="openEditDialog(row)">Edit</button>
<button kendoButton size="small" themeColor="error" (click)="deleteMember(row)">Delete</button>
<button *ngIf="!row.linkedUserId" kendoButton size="small" themeColor="info"
(click)="openCreateUserDialog(row)">+ Account</button>
</div>
</ng-template>
</kendo-grid-column>
</kendo-grid>
</div>
<!-- Member Form Dialog -->
<app-member-form-dialog
*ngIf="showMemberDialog"
[member]="editingMember"
(saved)="onMemberSaved($event)"
(cancelled)="closeMemberDialog()">
</app-member-form-dialog>
<!-- Create User Account Dialog -->
<app-create-user-dialog
*ngIf="showCreateUserDialog && selectedMemberForUser"
[member]="selectedMemberForUser"
(created)="onUserCreated()"
(cancelled)="closeCreateUserDialog()">
</app-create-user-dialog>
- Step 3: Create
members-page.component.scss
:host {
display: block;
height: 100%;
}
- Step 4: Add routes to
app.routes.ts
Open APP/src/app/app.routes.ts and add admin child routes inside the user-portal children array:
// Add these imports at the top:
import { MembersPageComponent } from './features/members/pages/members-page/members-page.component';
import { UsersPageComponent } from './features/users/pages/users-page/users-page.component';
// Add inside user-portal children:
{ path: 'admin/members', component: MembersPageComponent },
{ path: 'admin/users', component: UsersPageComponent },
The full routes file after edit:
import { Routes } from '@angular/router';
import { DashboardComponent } from './portals/user-portal/pages/dashboard/dashboard.component';
import { LoginPage } from './features/login-page/login-page';
import { UserPortalComponent } from './portals/user-portal/user-portal.component';
import { AuthGuard } from './core/guards/auth.guard';
import { MembersPageComponent } from './features/members/pages/members-page/members-page.component';
import { UsersPageComponent } from './features/users/pages/users-page/users-page.component';
export const routes: Routes = [
{ path: 'login', component: LoginPage },
{
path: 'user-portal',
component: UserPortalComponent,
canActivate: [AuthGuard],
children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'admin/members', component: MembersPageComponent },
{ path: 'admin/users', component: UsersPageComponent },
]
},
{ path: '', redirectTo: 'login', pathMatch: 'full' },
{ path: 'dashboard', redirectTo: 'user-portal/dashboard' },
{ path: '**', redirectTo: 'login' }
];
- Step 5: Build
cd APP && ng build --configuration development
Expected: Compiled successfully.
- Step 6: Commit
git add APP/src/app/features/members/pages/ APP/src/app/app.routes.ts
git commit -m "feat: add MembersPageComponent with Kendo Grid and routing"
Task 14: EditUserDialogComponent
Files:
-
Create:
APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.ts -
Create:
APP/src/app/features/users/components/edit-user-dialog/edit-user-dialog.component.html -
Step 1: Create
edit-user-dialog.component.ts
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { DialogsModule } from '@progress/kendo-angular-dialog';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { LabelModule } from '@progress/kendo-angular-label';
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { UserDto, UpdateUserRequest, ALL_ROLES } from '../../models/user.model';
@Component({
selector: 'app-edit-user-dialog',
standalone: true,
imports: [
CommonModule, ReactiveFormsModule, DialogsModule,
InputsModule, LabelModule, DropDownsModule, ButtonsModule
],
templateUrl: './edit-user-dialog.component.html',
})
export class EditUserDialogComponent implements OnInit {
@Input({ required: true }) user!: UserDto;
@Output() saved = new EventEmitter<UpdateUserRequest>();
@Output() cancelled = new EventEmitter<void>();
form!: FormGroup;
readonly roleOptions = [...ALL_ROLES];
readonly langOptions = [
{ text: 'English', value: 'en' },
{ text: '中文', value: 'zh-TW' },
];
constructor(private fb: FormBuilder) {}
ngOnInit(): void {
this.form = this.fb.group({
email: [this.user.email, [Validators.required, Validators.email]],
roles: [this.user.roles, Validators.required],
isActive: [this.user.isActive],
languagePreference: [this.user.languagePreference],
});
}
onSubmit(): void {
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
this.saved.emit(this.form.value as UpdateUserRequest);
}
onCancel(): void {
this.cancelled.emit();
}
}
- Step 2: Create
edit-user-dialog.component.html
<kendo-dialog title="Edit User" (close)="onCancel()" [minWidth]="460" [width]="500">
<form [formGroup]="form" class="k-form k-form-vertical k-p-2">
<kendo-formfield>
<kendo-label text="Email *"></kendo-label>
<kendo-textbox formControlName="email" type="email"></kendo-textbox>
<kendo-formerror *ngIf="form.get('email')?.errors?.['required']">Required.</kendo-formerror>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-label text="Roles *"></kendo-label>
<kendo-multiselect formControlName="roles" [data]="roleOptions"
placeholder="Select roles"></kendo-multiselect>
</kendo-formfield>
<kendo-formfield class="k-mt-3">
<kendo-label text="Language"></kendo-label>
<kendo-dropdownlist formControlName="languagePreference"
[data]="langOptions" textField="text" valueField="value">
</kendo-dropdownlist>
</kendo-formfield>
<div class="k-d-flex k-align-items-center k-gap-2 k-mt-4">
<input kendoCheckBox type="checkbox" formControlName="isActive" id="isActiveCheck" />
<kendo-label for="isActiveCheck" text="Account Active"></kendo-label>
</div>
</form>
<kendo-dialog-actions>
<button kendoButton (click)="onCancel()">Cancel</button>
<button kendoButton themeColor="primary" (click)="onSubmit()" [disabled]="form.invalid">
Save Changes
</button>
</kendo-dialog-actions>
</kendo-dialog>
- Step 3: Commit
git add APP/src/app/features/users/components/edit-user-dialog/
git commit -m "feat: add EditUserDialogComponent"
Task 15: UsersPageComponent + Routing
Files:
-
Create:
APP/src/app/features/users/pages/users-page/users-page.component.ts -
Create:
APP/src/app/features/users/pages/users-page/users-page.component.html -
Create:
APP/src/app/features/users/pages/users-page/users-page.component.scss -
Step 1: Create
users-page.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { GridModule, PageChangeEvent } from '@progress/kendo-angular-grid';
import { InputsModule } from '@progress/kendo-angular-inputs';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { IndicatorsModule } from '@progress/kendo-angular-indicators';
import { UserApiService } from '../../services/user-api.service';
import { EditUserDialogComponent } from '../../components/edit-user-dialog/edit-user-dialog.component';
import {
UserListItemDto, UserDto, UpdateUserRequest, PagedResult
} from '../../models/user.model';
@Component({
selector: 'app-users-page',
standalone: true,
imports: [
CommonModule, FormsModule, GridModule, InputsModule,
ButtonsModule, IndicatorsModule, EditUserDialogComponent,
],
templateUrl: './users-page.component.html',
styleUrls: ['./users-page.component.scss'],
})
export class UsersPageComponent implements OnInit {
data: UserListItemDto[] = [];
totalCount = 0;
page = 1;
pageSize = 20;
isLoading = false;
searchText = '';
// Edit dialog
showEditDialog = false;
editingUser: UserDto | null = null;
// Reset password result
resetPasswordResult: { userId: string; tempPassword: string } | null = null;
constructor(private userApi: UserApiService) {}
ngOnInit(): void { this.loadData(); }
loadData(): void {
this.isLoading = true;
this.userApi.getPaged({ page: this.page, pageSize: this.pageSize, search: this.searchText || undefined })
.subscribe({
next: (result: PagedResult<UserListItemDto>) => {
this.data = result.items;
this.totalCount = result.totalCount;
this.isLoading = false;
},
error: () => { this.isLoading = false; }
});
}
onPageChange(event: PageChangeEvent): void {
this.page = event.skip / this.pageSize + 1;
this.pageSize = event.take;
this.loadData();
}
onSearch(): void { this.page = 1; this.loadData(); }
openEditDialog(user: UserListItemDto): void {
this.userApi.getById(user.id).subscribe(dto => {
this.editingUser = dto;
this.showEditDialog = true;
});
}
closeEditDialog(): void {
this.showEditDialog = false;
this.editingUser = null;
}
onUserSaved(request: UpdateUserRequest): void {
if (!this.editingUser) return;
this.userApi.update(this.editingUser.id, request).subscribe(() => {
this.closeEditDialog();
this.loadData();
});
}
deactivateUser(user: UserListItemDto): void {
if (!confirm(`Deactivate ${user.email}? They will lose access immediately.`)) return;
this.userApi.deactivate(user.id).subscribe(() => this.loadData());
}
resetPassword(user: UserListItemDto): void {
if (!confirm(`Reset password for ${user.email}? A new temporary password will be generated.`)) return;
this.userApi.resetPassword(user.id).subscribe(result => {
this.resetPasswordResult = { userId: user.id, tempPassword: result.tempPassword };
});
}
copyResetPassword(): void {
if (this.resetPasswordResult) {
navigator.clipboard.writeText(this.resetPasswordResult.tempPassword);
}
}
dismissResetResult(): void { this.resetPasswordResult = null; }
}
- Step 2: Create
users-page.component.html
<div class="k-p-4">
<div class="k-d-flex k-justify-content-between k-align-items-center k-mb-4">
<h2 class="k-m-0">User Management</h2>
</div>
<!-- Reset password result banner -->
<div *ngIf="resetPasswordResult"
class="k-d-flex k-align-items-center k-gap-3 k-p-3 k-mb-4 k-border k-rounded k-bg-success-subtle">
<span>New temporary password: <strong><code>{{ resetPasswordResult.tempPassword }}</code></strong></span>
<button kendoButton size="small" (click)="copyResetPassword()">Copy</button>
<button kendoButton size="small" (click)="dismissResetResult()">✕</button>
<span class="k-color-warning k-ml-2">⚠️ This will not be shown again.</span>
</div>
<!-- Search -->
<div class="k-d-flex k-gap-3 k-mb-4">
<kendo-textbox [(ngModel)]="searchText" placeholder="Search email or member name…"
(keyup.enter)="onSearch()" style="width:300px"></kendo-textbox>
<button kendoButton (click)="onSearch()">Search</button>
</div>
<!-- Grid -->
<kendo-grid
[data]="{ data: data, total: totalCount }"
[pageSize]="pageSize"
[skip]="(page - 1) * pageSize"
[pageable]="{ pageSizes: [10, 20, 50] }"
(pageChange)="onPageChange($event)">
<kendo-grid-column field="email" title="Email" [width]="220"></kendo-grid-column>
<kendo-grid-column title="Member" [width]="180">
<ng-template kendoGridCellTemplate let-row>
{{ row.memberDisplayName ?? '—' }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Roles" [width]="200">
<ng-template kendoGridCellTemplate let-row>
<kendo-badge *ngFor="let role of row.roles" class="k-mr-1" themeColor="info">
{{ role }}
</kendo-badge>
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Active" [width]="90">
<ng-template kendoGridCellTemplate let-row>
<kendo-badge [themeColor]="row.isActive ? 'success' : 'error'">
{{ row.isActive ? 'Active' : 'Inactive' }}
</kendo-badge>
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Last Login" [width]="160">
<ng-template kendoGridCellTemplate let-row>
{{ row.lastLoginAt ? (row.lastLoginAt | date:'MM/dd/yyyy HH:mm') : '—' }}
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Actions" [width]="240">
<ng-template kendoGridCellTemplate let-row>
<div class="k-d-flex k-gap-2">
<button kendoButton size="small" (click)="openEditDialog(row)">Edit</button>
<button kendoButton size="small" (click)="resetPassword(row)">Reset Pwd</button>
<button *ngIf="row.isActive" kendoButton size="small" themeColor="warning"
(click)="deactivateUser(row)">Deactivate</button>
</div>
</ng-template>
</kendo-grid-column>
</kendo-grid>
</div>
<!-- Edit User Dialog -->
<app-edit-user-dialog
*ngIf="showEditDialog && editingUser"
[user]="editingUser"
(saved)="onUserSaved($event)"
(cancelled)="closeEditDialog()">
</app-edit-user-dialog>
- Step 3: Create
users-page.component.scss
:host { display: block; height: 100%; }
- Step 4: Build
cd APP && ng build --configuration development
Expected: Compiled successfully.
- Step 5: Commit
git add APP/src/app/features/users/pages/ APP/src/app/features/users/components/
git commit -m "feat: add UsersPageComponent with Kendo Grid + edit/deactivate/reset-password"
Task 16: Sidebar Navigation Update
Files:
-
Modify:
APP/src/app/portals/user-portal/components/user-navbar/user-navbar.component.ts -
Modify:
APP/src/app/portals/user-portal/components/user-navbar/user-navbar.component.html -
Modify:
APP/src/app/portals/user-portal/user-portal.component.ts -
Step 1: Update
user-navbar.component.ts— add admin nav items + role check
Replace the component class (keep existing imports, add AuthService):
import { Component, OnInit, OnDestroy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Router, NavigationEnd, RouterModule } from '@angular/router';
import { LayoutModule } from '@progress/kendo-angular-layout';
import { ButtonsModule } from '@progress/kendo-angular-buttons';
import { IconsModule } from '@progress/kendo-angular-icons';
import { SVGIcon, homeIcon, calendarIcon, userIcon, gearIcon, groupIcon } from '@progress/kendo-svg-icons';
import { LayoutService } from '../../../../layout/services/layout.service';
import { AuthService } from '../../../../shared/services/auth.service';
import { Subject, takeUntil, filter } from 'rxjs';
interface NavItem {
text: string;
icon: SVGIcon;
path: string;
active?: boolean;
}
@Component({
selector: 'app-user-navbar',
standalone: true,
imports: [CommonModule, RouterModule, LayoutModule, ButtonsModule, IconsModule],
templateUrl: './user-navbar.component.html',
styleUrls: ['./user-navbar.component.scss']
})
export class UserNavbarComponent implements OnInit, OnDestroy {
public homeIcon: SVGIcon = homeIcon;
public groupIcon: SVGIcon = groupIcon;
public userIcon: SVGIcon = userIcon;
public gearIcon: SVGIcon = gearIcon;
public mainNavItems: NavItem[] = [
{ text: 'Dashboard', icon: homeIcon, path: '/user-portal/dashboard' },
];
public adminNavItems: NavItem[] = [
{ text: 'Members', icon: groupIcon, path: '/user-portal/admin/members' },
{ text: 'Users', icon: userIcon, path: '/user-portal/admin/users' },
];
showAdminSection = false;
private destroy$ = new Subject<void>();
constructor(
public layoutService: LayoutService,
private router: Router,
private authService: AuthService,
) {}
ngOnInit(): void {
this.router.events.pipe(
filter(e => e instanceof NavigationEnd),
takeUntil(this.destroy$)
).subscribe((e: NavigationEnd) => this.updateActiveStates(e.url));
this.updateActiveStates(this.router.url);
// Show admin section for super_admin or secretary
this.authService.currentUser$.pipe(takeUntil(this.destroy$)).subscribe(user => {
this.showAdminSection = !!user?.roles?.some(
r => r === 'super_admin' || r === 'secretary'
);
});
}
ngOnDestroy(): void { this.destroy$.next(); this.destroy$.complete(); }
public navigateTo(path: string): void {
this.router.navigate([path]);
this.layoutService.closeDrawer();
}
private updateActiveStates(url: string): void {
[...this.mainNavItems, ...this.adminNavItems].forEach(i => i.active = false);
const active = [...this.mainNavItems, ...this.adminNavItems].find(i => url.startsWith(i.path));
if (active) active.active = true;
}
}
- Step 2: Update
user-navbar.component.html— add Administration section
Replace the full template (adapt to existing styles — keep whatever button/list pattern is already in the file, and add the admin section):
<div class="nav-menu">
<!-- Main -->
<div class="nav-section">
<button *ngFor="let item of mainNavItems" class="nav-item"
[class.active]="item.active" (click)="navigateTo(item.path)">
<kendo-svg-icon [icon]="item.icon"></kendo-svg-icon>
<span>{{ item.text }}</span>
</button>
</div>
<!-- Administration (role-guarded) -->
<div class="nav-section" *ngIf="showAdminSection">
<div class="nav-section-label">Administration</div>
<button *ngFor="let item of adminNavItems" class="nav-item"
[class.active]="item.active" (click)="navigateTo(item.path)">
<kendo-svg-icon [icon]="item.icon"></kendo-svg-icon>
<span>{{ item.text }}</span>
</button>
</div>
</div>
Note: Preserve any existing CSS classes from the current
user-navbar.component.html. Only add the new administration section block — do not remove existing nav items.
- Step 3: Update
user-portal.component.ts— add page titles for admin routes
In getPageTitle(), add:
'admin/members': 'Member Management',
'admin/users': 'User Management',
So the full titles map becomes:
const titles: { [key: string]: string } = {
'dashboard': 'Dashboard',
'admin': 'Administration', // fallback
// parse the second segment for admin sub-pages
};
Actually, since updatePageTitle() uses segments[1], and the paths are /user-portal/admin/members, segments[1] will be admin. Instead, fix updatePageTitle to join the last two segments:
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',
'admin/members': 'Member Management',
'admin/users': 'User Management',
};
return titles[page] ?? 'Dashboard';
}
- Step 4: Build and run
cd APP && ng build --configuration development
Expected: Compiled successfully.
Start dev server and verify:
ng serve
-
Login as
admin@rolac.org / Admin1234! -
Sidebar shows Administration section with Members and Users links
-
Navigate to
/user-portal/admin/members— grid loads (empty) -
Click + Add Member — 3-tab dialog opens
-
Add a member — grid refreshes with the new member
-
Click + Account on the member — create-user dialog opens
-
Enter email + role → submit → temp password is displayed with Copy button
-
Step 5: Commit
git add APP/src/app/portals/user-portal/
git commit -m "feat: add Administration section to sidebar with role-gated Member/User nav"
All 3 parts complete. 🎉
Summary of what was built:
- Backend:
AuditableEntity/SoftDeleteEntitybases,AuditSaveChangesInterceptor,Member/FamilyUnitentities, full EF migration, all DTOs,MemberService+UserManagementServicewith tests,MembersController+UsersController - Frontend: Angular models + services,
MemberFormDialogComponent(3-tab),CreateUserAccountDialogComponent(with temp-password reveal),MembersPageComponent,EditUserDialogComponent,UsersPageComponent, sidebar navigation update