feat: add UsersPageComponent with Kendo Grid + edit/deactivate/reset-password
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
<div class="k-p-4">
|
||||||
|
<div class="k-d-flex k-justify-content-between k-align-items-center k-mb-4">
|
||||||
|
<h2 class="k-m-0">User Management</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Reset password result banner -->
|
||||||
|
<div *ngIf="resetPasswordResult"
|
||||||
|
class="reset-password-banner k-d-flex k-align-items-center k-gap-3 k-p-3 k-mb-4">
|
||||||
|
<span>New temporary password: <strong><code>{{ resetPasswordResult.tempPassword }}</code></strong></span>
|
||||||
|
<button kendoButton size="small" (click)="copyResetPassword()">Copy</button>
|
||||||
|
<button kendoButton size="small" (click)="dismissResetResult()">X</button>
|
||||||
|
<span class="k-ml-2">Warning: This will not be shown again.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search -->
|
||||||
|
<div class="k-d-flex k-gap-3 k-mb-4">
|
||||||
|
<kendo-textbox [(ngModel)]="searchText" placeholder="Search email or member name..."
|
||||||
|
(keyup.enter)="onSearch()" style="width:300px"></kendo-textbox>
|
||||||
|
<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] }"
|
||||||
|
(pageChange)="onPageChange($event)">
|
||||||
|
|
||||||
|
<kendo-grid-column field="email" title="Email" [width]="220"></kendo-grid-column>
|
||||||
|
|
||||||
|
<kendo-grid-column title="Member" [width]="180">
|
||||||
|
<ng-template kendoGridCellTemplate let-row>
|
||||||
|
{{ row.memberDisplayName ?? '--' }}
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
|
||||||
|
<kendo-grid-column title="Roles" [width]="200">
|
||||||
|
<ng-template kendoGridCellTemplate let-row>
|
||||||
|
<span *ngFor="let role of row.roles" class="role-badge k-mr-1">{{ role }}</span>
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
|
||||||
|
<kendo-grid-column title="Active" [width]="90">
|
||||||
|
<ng-template kendoGridCellTemplate let-row>
|
||||||
|
<span [class]="row.isActive ? 'status-active' : 'status-inactive'">
|
||||||
|
{{ row.isActive ? 'Active' : 'Inactive' }}
|
||||||
|
</span>
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
|
||||||
|
<kendo-grid-column title="Last Login" [width]="160">
|
||||||
|
<ng-template kendoGridCellTemplate let-row>
|
||||||
|
{{ row.lastLoginAt ? (row.lastLoginAt | date:'MM/dd/yyyy HH:mm') : '--' }}
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
|
||||||
|
<kendo-grid-column title="Actions" [width]="240">
|
||||||
|
<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" (click)="resetPassword(row)">Reset Pwd</button>
|
||||||
|
<button *ngIf="row.isActive" kendoButton size="small" themeColor="warning"
|
||||||
|
(click)="deactivateUser(row)">Deactivate</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</kendo-grid-column>
|
||||||
|
|
||||||
|
</kendo-grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit User Dialog -->
|
||||||
|
<app-edit-user-dialog
|
||||||
|
*ngIf="showEditDialog && editingUser"
|
||||||
|
[user]="editingUser"
|
||||||
|
(saved)="onUserSaved($event)"
|
||||||
|
(cancelled)="closeEditDialog()">
|
||||||
|
</app-edit-user-dialog>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
:host { display: block; height: 100%; }
|
||||||
|
|
||||||
|
.reset-password-banner {
|
||||||
|
background: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 6px;
|
||||||
|
background: #cce5ff;
|
||||||
|
color: #004085;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active { color: #155724; font-weight: 500; }
|
||||||
|
.status-inactive { color: #721c24; font-weight: 500; }
|
||||||
@@ -1,10 +1,103 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
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({
|
@Component({
|
||||||
selector: 'app-users-page',
|
selector: 'app-users-page',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule],
|
imports: [
|
||||||
template: '<div class="k-p-4"><h2>User Management</h2><p>Loading...</p></div>',
|
CommonModule, FormsModule, GridModule, InputsModule,
|
||||||
|
ButtonsModule, IndicatorsModule, EditUserDialogComponent,
|
||||||
|
],
|
||||||
|
templateUrl: './users-page.component.html',
|
||||||
|
styleUrls: ['./users-page.component.scss'],
|
||||||
})
|
})
|
||||||
export class UsersPageComponent {}
|
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<UserListItemDto>) => {
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user