# Member & User Management — Part 3: Angular Frontend (Tasks 10–16) > **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`, 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 { 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 ): 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> { 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>(this.endpoint, { params: p }); } getById(id: number): Observable { return this.http.get(`${this.endpoint}/${id}`); } create(request: CreateMemberRequest): Observable<{ id: number }> { return this.http.post<{ id: number }>(this.endpoint, request); } update(id: number, request: UpdateMemberRequest): Observable { return this.http.put(`${this.endpoint}/${id}`, request); } delete(id: number): Observable { return this.http.delete(`${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 { 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> { 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>(this.endpoint, { params: p }); } getById(id: string): Observable { return this.http.get(`${this.endpoint}/${id}`); } createUser(request: CreateUserRequest): Observable { return this.http.post(this.endpoint, request); } update(id: string, request: UpdateUserRequest): Observable { return this.http.put(`${this.endpoint}/${id}`, request); } deactivate(id: string): Observable { return this.http.delete(`${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(); @Output() cancelled = new EventEmitter(); 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
``` - [ ] **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(); @Output() cancelled = new EventEmitter(); 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

Creating account for {{ memberName }}

Email is required. Invalid email address.

{{ errorMessage }}

✅ Account created!

Share this temporary password with {{ memberName }}.

{{ tempPassword }}

⚠️ This password will not be shown again.

``` - [ ] **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) => { 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

Member Management

{{ memberDisplayName(row) }} ({{ row.lastName_zh }}{{ row.firstName_zh }})
Legal: {{ row.firstName_en }}
{{ row.status }} {{ row.linkedUserId ? '✓ User' : '—' }}
``` - [ ] **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(); @Output() cancelled = new EventEmitter(); 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
Required.
``` - [ ] **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) => { 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

User Management

New temporary password: {{ resetPasswordResult.tempPassword }} ⚠️ This will not be shown again.
{{ row.memberDisplayName ?? '—' }} {{ role }} {{ row.isActive ? 'Active' : 'Inactive' }} {{ row.lastLoginAt ? (row.lastLoginAt | date:'MM/dd/yyyy HH:mm') : '—' }}
``` - [ ] **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(); 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 ``` > **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