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

54 KiB
Raw Blame History

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

// 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/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