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 <noreply@anthropic.com>
This commit is contained in:
Chris Chen
2026-05-27 14:20:52 -07:00
parent 07e0270599
commit 3a5b5721e4
7 changed files with 278 additions and 4 deletions
+5 -1
View File
@@ -3,6 +3,8 @@ import { DashboardComponent } from './portals/user-portal/pages/dashboard/dashbo
import { LoginPage } from './features/login-page/login-page'; import { LoginPage } from './features/login-page/login-page';
import { UserPortalComponent } from './portals/user-portal/user-portal.component'; import { UserPortalComponent } from './portals/user-portal/user-portal.component';
import { AuthGuard } from './core/guards/auth.guard'; 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 = [ export const routes: Routes = [
// Public routes // Public routes
@@ -15,7 +17,9 @@ export const routes: Routes = [
canActivate: [AuthGuard], canActivate: [AuthGuard],
children: [ children: [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent } { path: 'dashboard', component: DashboardComponent },
{ path: 'admin/members', component: MembersPageComponent },
{ path: 'admin/users', component: UsersPageComponent },
] ]
}, },
@@ -8,7 +8,7 @@
<kendo-formfield> <kendo-formfield>
<kendo-label text="Login Email *"></kendo-label> <kendo-label text="Login Email *"></kendo-label>
<kendo-textbox formControlName="email" type="email"></kendo-textbox> <kendo-textbox formControlName="email"></kendo-textbox>
<kendo-formerror *ngIf="form.get('email')?.errors?.['required']">Email is required.</kendo-formerror> <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-formerror *ngIf="form.get('email')?.errors?.['email']">Invalid email address.</kendo-formerror>
</kendo-formfield> </kendo-formfield>
@@ -72,7 +72,7 @@
<kendo-formfield> <kendo-formfield>
<kendo-label text="Email"></kendo-label> <kendo-label text="Email"></kendo-label>
<kendo-textbox formControlName="email" type="email"></kendo-textbox> <kendo-textbox formControlName="email"></kendo-textbox>
</kendo-formfield> </kendo-formfield>
<kendo-formfield> <kendo-formfield>
@@ -0,0 +1,96 @@
<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">
({{ row.lastName_zh }}{{ row.firstName_zh }})
</span>
</div>
<div *ngIf="row.nickName && row.nickName !== row.firstName_en" class="k-font-size-sm">
Legal: {{ row.firstName_en }}
</div>
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Status" [width]="100">
<ng-template kendoGridCellTemplate let-row>
<span class="status-badge" [attr.data-status]="row.status">{{ row.status }}</span>
</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>
<span [class]="row.linkedUserId ? 'account-badge has-account' : 'account-badge no-account'">
{{ row.linkedUserId ? 'User' : '--' }}
</span>
</ng-template>
</kendo-grid-column>
<kendo-grid-column title="Actions" [width]="210">
<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>
@@ -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; }
}
@@ -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<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();
}
}
@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-users-page',
standalone: true,
imports: [CommonModule],
template: '<div class="k-p-4"><h2>User Management</h2><p>Loading...</p></div>',
})
export class UsersPageComponent {}