Files
ROLAC/docs/superpowers/plans/2026-05-27-member-user-mgmt-part3-frontend.md
T
2026-05-27 13:18:27 -07:00

1656 lines
54 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Member & User Management — Part 3: Angular Frontend (Tasks 1016)
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` or `superpowers:executing-plans` to implement task-by-task.
> **Prerequisite:** Parts 1 & 2 complete (API running at configured `environment.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`**
```typescript
// 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`**
```typescript
// 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`**
```typescript
// 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`**
```typescript
// 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**
```bash
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`**
```typescript
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`**
```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**
```bash
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`**
```typescript
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`**
```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**
```bash
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`**
```typescript
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`**
```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`**
```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:
```typescript
// 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:
```typescript
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**
```bash
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`**
```typescript
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`**
```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**
```bash
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`**
```typescript
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`**
```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`**
```scss
:host { display: block; height: 100%; }
```
- [ ] **Step 4: Build**
```
cd APP && ng build --configuration development
```
Expected: Compiled successfully.
- [ ] **Step 5: Commit**
```bash
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`):
```typescript
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):
```html
<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:
```typescript
'admin/members': 'Member Management',
'admin/users': 'User Management',
```
So the full `titles` map becomes:
```typescript
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:
```typescript
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**
```bash
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`/`SoftDeleteEntity` bases, `AuditSaveChangesInterceptor`, `Member`/`FamilyUnit` entities, full EF migration, all DTOs, `MemberService` + `UserManagementService` with tests, `MembersController` + `UsersController`
- Frontend: Angular models + services, `MemberFormDialogComponent` (3-tab), `CreateUserAccountDialogComponent` (with temp-password reveal), `MembersPageComponent`, `EditUserDialogComponent`, `UsersPageComponent`, sidebar navigation update