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:
@@ -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 },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -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>
|
||||||
|
|||||||
+1
-1
@@ -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 {}
|
||||||
Reference in New Issue
Block a user