From 3a5b5721e4ee238d7380a788f0983f2f63a3a835 Mon Sep 17 00:00:00 2001 From: Chris Chen Date: Wed, 27 May 2026 14:20:52 -0700 Subject: [PATCH] feat: add MembersPageComponent with Kendo Grid and routing Also adds stub UsersPageComponent for route compilation, and fixes pre-existing kendo-textbox type="email" build errors in dialog templates. Co-Authored-By: Claude Sonnet 4.6 --- APP/src/app/app.routes.ts | 8 +- .../create-user-dialog.component.html | 2 +- .../member-form-dialog.component.html | 2 +- .../members-page/members-page.component.html | 96 ++++++++++++ .../members-page/members-page.component.scss | 27 ++++ .../members-page/members-page.component.ts | 137 ++++++++++++++++++ .../pages/users-page/users-page.component.ts | 10 ++ 7 files changed, 278 insertions(+), 4 deletions(-) create mode 100644 APP/src/app/features/members/pages/members-page/members-page.component.html create mode 100644 APP/src/app/features/members/pages/members-page/members-page.component.scss create mode 100644 APP/src/app/features/members/pages/members-page/members-page.component.ts create mode 100644 APP/src/app/features/users/pages/users-page/users-page.component.ts diff --git a/APP/src/app/app.routes.ts b/APP/src/app/app.routes.ts index 768dba1..5e21e57 100644 --- a/APP/src/app/app.routes.ts +++ b/APP/src/app/app.routes.ts @@ -3,6 +3,8 @@ import { DashboardComponent } from './portals/user-portal/pages/dashboard/dashbo 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 = [ // Public routes @@ -14,8 +16,10 @@ export const routes: Routes = [ component: UserPortalComponent, canActivate: [AuthGuard], children: [ - { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, - { path: 'dashboard', component: DashboardComponent } + { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, + { path: 'dashboard', component: DashboardComponent }, + { path: 'admin/members', component: MembersPageComponent }, + { path: 'admin/users', component: UsersPageComponent }, ] }, diff --git a/APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.html b/APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.html index 5d502c1..fbfc1a2 100644 --- a/APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.html +++ b/APP/src/app/features/members/components/create-user-dialog/create-user-dialog.component.html @@ -8,7 +8,7 @@ - + Email is required. Invalid email address. diff --git a/APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html b/APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html index b22485b..0ee6d62 100644 --- a/APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html +++ b/APP/src/app/features/members/components/member-form-dialog/member-form-dialog.component.html @@ -72,7 +72,7 @@ - + diff --git a/APP/src/app/features/members/pages/members-page/members-page.component.html b/APP/src/app/features/members/pages/members-page/members-page.component.html new file mode 100644 index 0000000..8ad813f --- /dev/null +++ b/APP/src/app/features/members/pages/members-page/members-page.component.html @@ -0,0 +1,96 @@ +
+ + +
+

Member Management

+ +
+ + +
+ + + + + +
+ + + + + + +
+ {{ memberDisplayName(row) }} + + ({{ row.lastName_zh }}{{ row.firstName_zh }}) + +
+
+ Legal: {{ row.firstName_en }} +
+
+
+ + + + {{ row.status }} + + + + + + + + + + + {{ row.linkedUserId ? 'User' : '--' }} + + + + + + +
+ + + +
+
+
+ +
+
+ + + + + + + + diff --git a/APP/src/app/features/members/pages/members-page/members-page.component.scss b/APP/src/app/features/members/pages/members-page/members-page.component.scss new file mode 100644 index 0000000..bd8957e --- /dev/null +++ b/APP/src/app/features/members/pages/members-page/members-page.component.scss @@ -0,0 +1,27 @@ +:host { + display: block; + height: 100%; +} + +.status-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 500; + + &[data-status="Member"] { background: #d4edda; color: #155724; } + &[data-status="Visitor"] { background: #cce5ff; color: #004085; } + &[data-status="Inactive"] { background: #e2e3e5; color: #383d41; } + &[data-status="Former"] { background: #ffeeba; color: #856404; } +} + +.account-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + + &.has-account { background: #d4edda; color: #155724; } + &.no-account { background: #e2e3e5; color: #383d41; } +} diff --git a/APP/src/app/features/members/pages/members-page/members-page.component.ts b/APP/src/app/features/members/pages/members-page/members-page.component.ts new file mode 100644 index 0000000..777f141 --- /dev/null +++ b/APP/src/app/features/members/pages/members-page/members-page.component.ts @@ -0,0 +1,137 @@ +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(); + } +} diff --git a/APP/src/app/features/users/pages/users-page/users-page.component.ts b/APP/src/app/features/users/pages/users-page/users-page.component.ts new file mode 100644 index 0000000..16127ba --- /dev/null +++ b/APP/src/app/features/users/pages/users-page/users-page.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +@Component({ + selector: 'app-users-page', + standalone: true, + imports: [CommonModule], + template: '

User Management

Loading...

', +}) +export class UsersPageComponent {}