WIP
This commit is contained in:
+2
-1
@@ -26,7 +26,8 @@
|
||||
<kendo-label text="Language"></kendo-label>
|
||||
<kendo-dropdownlist
|
||||
formControlName="languagePreference"
|
||||
[data]="langOptions" textField="text" valueField="value">
|
||||
[data]="langOptions" textField="text" valueField="value"
|
||||
[valuePrimitive]="true">
|
||||
</kendo-dropdownlist>
|
||||
</kendo-formfield>
|
||||
|
||||
|
||||
+3
-1
@@ -38,6 +38,7 @@
|
||||
formControlName="gender"
|
||||
[data]="genderOptions"
|
||||
textField="text" valueField="value"
|
||||
[valuePrimitive]="true"
|
||||
[defaultItem]="{ text: '-- Select --', value: null }">
|
||||
</kendo-dropdownlist>
|
||||
</kendo-formfield>
|
||||
@@ -57,7 +58,8 @@
|
||||
<kendo-label text="Language"></kendo-label>
|
||||
<kendo-dropdownlist
|
||||
formControlName="languagePreference"
|
||||
[data]="langOptions" textField="text" valueField="value">
|
||||
[data]="langOptions" textField="text" valueField="value"
|
||||
[valuePrimitive]="true">
|
||||
</kendo-dropdownlist>
|
||||
</kendo-formfield>
|
||||
|
||||
|
||||
+19
-1
@@ -76,10 +76,28 @@ export class MemberFormDialogComponent implements OnInit {
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
|
||||
this.saved.emit(this.form.value as CreateMemberRequest);
|
||||
const v = this.form.value;
|
||||
const payload: CreateMemberRequest = {
|
||||
...v,
|
||||
dateOfBirth: toDateOnlyString(v.dateOfBirth),
|
||||
baptismDate: toDateOnlyString(v.baptismDate),
|
||||
joinDate: toDateOnlyString(v.joinDate),
|
||||
};
|
||||
this.saved.emit(payload);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.cancelled.emit();
|
||||
}
|
||||
}
|
||||
|
||||
// Kendo DatePicker emits a JS Date; the API expects DateOnly ("yyyy-MM-dd").
|
||||
// Use local components so the date the user picked isn't shifted by UTC offset.
|
||||
function toDateOnlyString(d: Date | string | null | undefined): string | null {
|
||||
if (d == null || d === '') return null;
|
||||
if (typeof d === 'string') return d.length >= 10 ? d.substring(0, 10) : d;
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<kendo-dialog title="Add New User" (close)="onCancel()" [minWidth]="460" [width]="500">
|
||||
<form [formGroup]="form" class="k-form k-form-vertical k-p-2">
|
||||
|
||||
<kendo-formfield>
|
||||
<kendo-label text="Email *"></kendo-label>
|
||||
<kendo-textbox formControlName="email"></kendo-textbox>
|
||||
<kendo-formerror *ngIf="form.get('email')?.errors?.['required']">Required.</kendo-formerror>
|
||||
<kendo-formerror *ngIf="form.get('email')?.errors?.['email']">Invalid email.</kendo-formerror>
|
||||
</kendo-formfield>
|
||||
|
||||
<kendo-formfield class="k-mt-3">
|
||||
<kendo-label text="Roles *"></kendo-label>
|
||||
<kendo-multiselect formControlName="roles" [data]="roleOptions"
|
||||
placeholder="Select roles"></kendo-multiselect>
|
||||
<kendo-formerror>At least one role is required.</kendo-formerror>
|
||||
</kendo-formfield>
|
||||
|
||||
<kendo-formfield class="k-mt-3">
|
||||
<kendo-label text="Language"></kendo-label>
|
||||
<kendo-dropdownlist formControlName="languagePreference"
|
||||
[data]="langOptions" textField="text" valueField="value"
|
||||
[valuePrimitive]="true">
|
||||
</kendo-dropdownlist>
|
||||
</kendo-formfield>
|
||||
|
||||
<kendo-formfield class="k-mt-3">
|
||||
<kendo-label text="Member ID (optional)"></kendo-label>
|
||||
<kendo-numerictextbox formControlName="memberId" [format]="'0'"
|
||||
placeholder="Link to a member record"></kendo-numerictextbox>
|
||||
</kendo-formfield>
|
||||
|
||||
</form>
|
||||
|
||||
<kendo-dialog-actions>
|
||||
<button kendoButton (click)="onCancel()">Cancel</button>
|
||||
<button kendoButton themeColor="primary" (click)="onSubmit()" [disabled]="form.invalid">
|
||||
Create User
|
||||
</button>
|
||||
</kendo-dialog-actions>
|
||||
</kendo-dialog>
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { DialogsModule } from '@progress/kendo-angular-dialog';
|
||||
import { InputsModule } from '@progress/kendo-angular-inputs';
|
||||
import { LabelModule } from '@progress/kendo-angular-label';
|
||||
import { DropDownsModule } from '@progress/kendo-angular-dropdowns';
|
||||
import { ButtonsModule } from '@progress/kendo-angular-buttons';
|
||||
import { CreateUserRequest, ALL_ROLES } from '../../models/user.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-user-dialog',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule, ReactiveFormsModule, DialogsModule,
|
||||
InputsModule, LabelModule, DropDownsModule, ButtonsModule
|
||||
],
|
||||
templateUrl: './create-user-dialog.component.html',
|
||||
})
|
||||
export class CreateUserDialogComponent {
|
||||
@Output() saved = new EventEmitter<CreateUserRequest>();
|
||||
@Output() cancelled = new EventEmitter<void>();
|
||||
|
||||
form: FormGroup;
|
||||
readonly roleOptions: string[] = [...ALL_ROLES];
|
||||
readonly langOptions = [
|
||||
{ text: 'English', value: 'en' },
|
||||
{ text: '中文', value: 'zh-TW' },
|
||||
];
|
||||
|
||||
constructor(private fb: FormBuilder) {
|
||||
this.form = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
roles: [[], Validators.required],
|
||||
languagePreference: ['en'],
|
||||
memberId: [null],
|
||||
});
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.form.invalid) { this.form.markAllAsTouched(); return; }
|
||||
const val = this.form.value;
|
||||
this.saved.emit({
|
||||
email: val.email,
|
||||
roles: val.roles,
|
||||
languagePreference: val.languagePreference,
|
||||
memberId: val.memberId,
|
||||
});
|
||||
}
|
||||
|
||||
onCancel(): void { this.cancelled.emit(); }
|
||||
}
|
||||
+2
-1
@@ -17,7 +17,8 @@
|
||||
<kendo-formfield class="k-mt-3">
|
||||
<kendo-label text="Language"></kendo-label>
|
||||
<kendo-dropdownlist formControlName="languagePreference"
|
||||
[data]="langOptions" textField="text" valueField="value">
|
||||
[data]="langOptions" textField="text" valueField="value"
|
||||
[valuePrimitive]="true">
|
||||
</kendo-dropdownlist>
|
||||
</kendo-formfield>
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface UserListItemDto {
|
||||
export type UserDto = UserListItemDto;
|
||||
|
||||
export interface CreateUserRequest {
|
||||
memberId: number;
|
||||
memberId: number | null;
|
||||
email: string;
|
||||
roles: string[];
|
||||
languagePreference: string;
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
<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 class="k-d-flex k-gap-2">
|
||||
<button kendoButton themeColor="primary" (click)="openCreateDialog()">+ Add New User</button>
|
||||
<button kendoButton (click)="testAuth()">Test Auth</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auth test result (dev only) -->
|
||||
<div *ngIf="authTestResult" class="k-mb-3 k-p-2" style="background:#f0f4ff;border-radius:4px;font-size:12px">
|
||||
<strong>Auth test:</strong> <pre style="margin:0;white-space:pre-wrap">{{ authTestResult }}</pre>
|
||||
<button kendoButton size="small" (click)="authTestResult=null">Clear</button>
|
||||
</div>
|
||||
|
||||
<!-- Reset password result banner -->
|
||||
@@ -69,6 +79,13 @@
|
||||
</kendo-grid>
|
||||
</div>
|
||||
|
||||
<!-- Create User Dialog -->
|
||||
<app-create-user-dialog
|
||||
*ngIf="showCreateDialog"
|
||||
(saved)="onUserCreated($event)"
|
||||
(cancelled)="closeCreateDialog()">
|
||||
</app-create-user-dialog>
|
||||
|
||||
<!-- Edit User Dialog -->
|
||||
<app-edit-user-dialog
|
||||
*ngIf="showEditDialog && editingUser"
|
||||
|
||||
@@ -5,18 +5,21 @@ 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 { HttpClient } from '@angular/common/http';
|
||||
import { UserApiService } from '../../services/user-api.service';
|
||||
import { EditUserDialogComponent } from '../../components/edit-user-dialog/edit-user-dialog.component';
|
||||
import { CreateUserDialogComponent } from '../../components/create-user-dialog/create-user-dialog.component';
|
||||
import {
|
||||
UserListItemDto, UserDto, UpdateUserRequest, PagedResult
|
||||
UserListItemDto, UserDto, CreateUserRequest, UpdateUserRequest, PagedResult
|
||||
} from '../../models/user.model';
|
||||
import { ApiConfigService } from '../../../../core/services/api-config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-users-page',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule, FormsModule, GridModule, InputsModule,
|
||||
ButtonsModule, IndicatorsModule, EditUserDialogComponent,
|
||||
ButtonsModule, IndicatorsModule, EditUserDialogComponent, CreateUserDialogComponent,
|
||||
],
|
||||
templateUrl: './users-page.component.html',
|
||||
styleUrls: ['./users-page.component.scss'],
|
||||
@@ -29,6 +32,9 @@ export class UsersPageComponent implements OnInit {
|
||||
isLoading = false;
|
||||
searchText = '';
|
||||
|
||||
// Create dialog
|
||||
showCreateDialog = false;
|
||||
|
||||
// Edit dialog
|
||||
showEditDialog = false;
|
||||
editingUser: UserDto | null = null;
|
||||
@@ -36,7 +42,14 @@ export class UsersPageComponent implements OnInit {
|
||||
// Reset password result
|
||||
resetPasswordResult: { userId: string; tempPassword: string } | null = null;
|
||||
|
||||
constructor(private userApi: UserApiService) {}
|
||||
// Auth test result (dev only)
|
||||
authTestResult: string | null = null;
|
||||
|
||||
constructor(
|
||||
private userApi: UserApiService,
|
||||
private http: HttpClient,
|
||||
private apiConfig: ApiConfigService,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void { this.loadData(); }
|
||||
|
||||
@@ -61,6 +74,25 @@ export class UsersPageComponent implements OnInit {
|
||||
|
||||
onSearch(): void { this.page = 1; this.loadData(); }
|
||||
|
||||
/** Dev-only: hits /api/auth/me and shows the parsed claims so we can see what the JWT contains. */
|
||||
testAuth(): void {
|
||||
this.http.get(`${this.apiConfig.authUrl}/me`).subscribe({
|
||||
next: (result) => { this.authTestResult = JSON.stringify(result, null, 2); },
|
||||
error: (err) => { this.authTestResult = `ERROR ${err.status}: ${JSON.stringify(err.error)}`; },
|
||||
});
|
||||
}
|
||||
|
||||
openCreateDialog(): void { this.showCreateDialog = true; }
|
||||
closeCreateDialog(): void { this.showCreateDialog = false; }
|
||||
|
||||
onUserCreated(request: CreateUserRequest): void {
|
||||
this.userApi.createUser(request).subscribe(result => {
|
||||
this.closeCreateDialog();
|
||||
this.resetPasswordResult = { userId: result.userId, tempPassword: result.tempPassword };
|
||||
this.loadData();
|
||||
});
|
||||
}
|
||||
|
||||
openEditDialog(user: UserListItemDto): void {
|
||||
this.userApi.getById(user.id).subscribe(dto => {
|
||||
this.editingUser = dto;
|
||||
|
||||
Reference in New Issue
Block a user