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:
+1
-1
@@ -8,7 +8,7 @@
|
||||
|
||||
<kendo-formfield>
|
||||
<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?.['email']">Invalid email address.</kendo-formerror>
|
||||
</kendo-formfield>
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@
|
||||
|
||||
<kendo-formfield>
|
||||
<kendo-label text="Email"></kendo-label>
|
||||
<kendo-textbox formControlName="email" type="email"></kendo-textbox>
|
||||
<kendo-textbox formControlName="email"></kendo-textbox>
|
||||
</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