Browse Source

feat: add user detail dialog and integrate with admin users component

pull/5819/head
HydrallHarsh 2 weeks ago
parent
commit
d60fe7e7ec
  1. 59
      apps/client/src/app/components/admin-users/admin-users.component.ts
  2. 11
      apps/client/src/app/components/admin-users/admin-users.html
  3. 8
      apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts
  4. 24
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.scss
  5. 89
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  6. 36
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

59
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -19,6 +19,7 @@ import {
ViewChild
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu';
import {
MatPaginator,
@ -26,6 +27,7 @@ import {
PageEvent
} from '@angular/material/paginator';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import {
differenceInSeconds,
@ -37,8 +39,10 @@ import {
contractOutline,
ellipsisHorizontal,
keyOutline,
personOutline,
trashOutline
} from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -49,6 +53,8 @@ import { AdminService } from '../../services/admin.service';
import { DataService } from '../../services/data.service';
import { ImpersonationStorageService } from '../../services/impersonation-storage.service';
import { UserService } from '../../services/user/user.service';
import { UserDetailDialogParams } from '../user-detail-dialog/interfaces/interfaces';
import { GfUserDetailDialogComponent } from '../user-detail-dialog/user-detail-dialog.component';
@Component({
imports: [
@ -72,7 +78,9 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
public dataSource = new MatTableDataSource<AdminUsers['users'][0]>();
public defaultDateFormat: string;
public displayedColumns: string[] = [];
public deviceType: string;
public getEmojiFlag = getEmojiFlag;
public hasImpersonationId: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToImpersonateAllUsers: boolean;
public info: InfoItem;
@ -87,6 +95,10 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private route: ActivatedRoute,
private router: Router,
private impersonationStorageService: ImpersonationStorageService,
private notificationService: NotificationService,
private tokenStorageService: TokenStorageService,
@ -138,13 +150,58 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
}
});
addIcons({ contractOutline, ellipsisHorizontal, keyOutline, trashOutline });
addIcons({
contractOutline,
ellipsisHorizontal,
personOutline,
keyOutline,
trashOutline
});
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.hasImpersonationId = !!this.impersonationStorageService.getId();
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['userId'] && params['userDetailDialog']) {
this.openUserDetailDialog(params['userId']);
}
});
}
public ngOnInit() {
this.fetchUsers();
}
public onOpenUserDetailDialog(userId: string) {
this.router.navigate([], {
queryParams: { userId, userDetailDialog: true }
});
}
private openUserDetailDialog(userId: string) {
// Find the user data from the current dataSource
const userData = this.dataSource.data.find((user) => user.id === userId);
const dialogRef = this.dialog.open(GfUserDetailDialogComponent, {
autoFocus: false,
data: {
userId: userId,
deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId,
userData: userData // Pass the user data
} as UserDetailDialogParams,
height: this.deviceType === 'mobile' ? '80vh' : '60vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.fetchUsers(); // Refresh the users list
this.router.navigate(['.'], { relativeTo: this.route }); // Clear query params
});
}
public formatDistanceToNow(aDateString: string) {
if (aDateString) {
const distanceString = formatDistanceToNowStrict(parseISO(aDateString), {

11
apps/client/src/app/components/admin-users/admin-users.html

@ -216,6 +216,15 @@
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #userMenu="matMenu" xPosition="before">
<button
mat-menu-item
(click)="onOpenUserDetailDialog(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="person-outline" />
<span i18n>View Details</span>
</span>
</button>
@if (hasPermissionToImpersonateAllUsers) {
<button mat-menu-item (click)="onImpersonateUser(element.id)">
<span class="align-items-center d-flex">
@ -257,6 +266,8 @@
*matRowDef="let row; columns: displayedColumns"
class="mat-mdc-row"
mat-row
style="cursor: pointer"
(click)="onOpenUserDetailDialog(row.id)"
></tr>
</table>
</div>

8
apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts

@ -0,0 +1,8 @@
import { AdminUsers } from '@ghostfolio/common/interfaces';
export interface UserDetailDialogParams {
userId: string;
deviceType: string;
hasImpersonationId: boolean;
userData?: AdminUsers['users'][0];
}

24
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.scss

@ -0,0 +1,24 @@
:host {
display: block;
.mat-mdc-dialog-content {
max-height: unset;
}
.text-monospace {
font-family:
'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 0.875rem;
}
small {
font-size: 0.75rem;
font-weight: 500;
}
h5 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
}

89
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts

@ -0,0 +1,89 @@
import { GfDialogFooterComponent } from '@ghostfolio/client/components/dialog-footer/dialog-footer.component';
import { GfDialogHeaderComponent } from '@ghostfolio/client/components/dialog-header/dialog-header.component';
import { User } from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
CUSTOM_ELEMENTS_SCHEMA,
Inject,
OnDestroy,
OnInit,
ChangeDetectorRef
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatDialogModule } from '@angular/material/dialog';
import { addIcons } from 'ionicons';
import { personOutline } from 'ionicons/icons';
import { Subject } from 'rxjs';
import { UserDetailDialogParams } from './interfaces/interfaces';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfDialogFooterComponent,
GfDialogHeaderComponent,
MatButtonModule,
MatDialogModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-user-detail-dialog',
styleUrls: ['./user-detail-dialog.component.scss'],
templateUrl: './user-detail-dialog.html'
})
export class GfUserDetailDialogComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
public user: User;
public userId: string;
public registrationDate: Date;
public isLoading = false;
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams,
public dialogRef: MatDialogRef<GfUserDetailDialogComponent>
) {
this.userId = this.data.userId;
addIcons({ personOutline });
}
public ngOnInit(): void {
this.initialize();
}
public onClose(): void {
this.dialogRef.close();
}
private initialize(): void {
this.fetchUserDetails();
}
private fetchUserDetails(): void {
this.isLoading = true;
// Use the user data passed from the admin users list if available
if (this.data.userData) {
this.userId = this.data.userData.id;
this.registrationDate = this.data.userData.createdAt;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
} else {
// Fallback: use the user ID and set a placeholder registration date
this.userId = this.data.userId;
this.registrationDate = new Date(); // Placeholder
this.isLoading = false;
this.changeDetectorRef.markForCheck();
}
}
public ngOnDestroy(): void {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

36
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html

@ -0,0 +1,36 @@
<gf-dialog-header
position="center"
[deviceType]="data.deviceType"
[title]="userId"
(closeButtonClicked)="onClose()"
/>
<div class="flex-grow-1" mat-dialog-content>
<div class="container p-0">
<div class="row">
<div class="col-12 mb-3">
<h5 i18n>User Information</h5>
</div>
</div>
<div class="mb-3 row">
<div class="col-6 mb-3">
<div class="d-flex flex-column">
<small class="text-muted mb-1" i18n>User ID</small>
<span class="text-monospace">{{ userId }}</span>
</div>
</div>
<div class="col-6 mb-3">
<div class="d-flex flex-column">
<small class="text-muted mb-1" i18n>Registration Date</small>
<span>{{ registrationDate | date: 'medium' }}</span>
</div>
</div>
</div>
</div>
</div>
<gf-dialog-footer
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
/>
Loading…
Cancel
Save