mirror of https://github.com/ghostfolio/ghostfolio
Thomas Kaul
3 years ago
committed by
GitHub
14 changed files with 556 additions and 361 deletions
@ -0,0 +1,150 @@ |
|||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; |
|||
import { AdminService } from '@ghostfolio/client/services/admin.service'; |
|||
import { CacheService } from '@ghostfolio/client/services/cache.service'; |
|||
import { DataService } from '@ghostfolio/client/services/data.service'; |
|||
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
|||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; |
|||
import { User } from '@ghostfolio/common/interfaces'; |
|||
import { |
|||
differenceInSeconds, |
|||
formatDistanceToNowStrict, |
|||
isValid, |
|||
parseISO |
|||
} from 'date-fns'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'gf-admin-overview', |
|||
styleUrls: ['./admin-overview.scss'], |
|||
templateUrl: './admin-overview.html' |
|||
}) |
|||
export class AdminOverviewComponent implements OnDestroy, OnInit { |
|||
public dataGatheringInProgress: boolean; |
|||
public dataGatheringProgress: number; |
|||
public defaultDateFormat = DEFAULT_DATE_FORMAT; |
|||
public exchangeRates: { label1: string; label2: string; value: number }[]; |
|||
public lastDataGathering: string; |
|||
public transactionCount: number; |
|||
public userCount: number; |
|||
public user: User; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
/** |
|||
* @constructor |
|||
*/ |
|||
public constructor( |
|||
private adminService: AdminService, |
|||
private cacheService: CacheService, |
|||
private changeDetectorRef: ChangeDetectorRef, |
|||
private dataService: DataService, |
|||
private userService: UserService |
|||
) {} |
|||
|
|||
/** |
|||
* Initializes the controller |
|||
*/ |
|||
public ngOnInit() { |
|||
this.fetchAdminData(); |
|||
|
|||
this.userService.stateChanged |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((state) => { |
|||
if (state?.user) { |
|||
this.user = state.user; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public onFlushCache() { |
|||
this.cacheService |
|||
.flush() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(() => { |
|||
setTimeout(() => { |
|||
window.location.reload(); |
|||
}, 300); |
|||
}); |
|||
} |
|||
|
|||
public onGatherMax() { |
|||
const confirmation = confirm( |
|||
'This action may take some time. Do you want to proceed?' |
|||
); |
|||
|
|||
if (confirmation === true) { |
|||
this.adminService |
|||
.gatherMax() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(() => { |
|||
setTimeout(() => { |
|||
window.location.reload(); |
|||
}, 300); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public onGatherProfileData() { |
|||
this.adminService |
|||
.gatherProfileData() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(() => {}); |
|||
} |
|||
|
|||
public formatDistanceToNow(aDateString: string) { |
|||
if (aDateString) { |
|||
const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { |
|||
addSuffix: true |
|||
}); |
|||
|
|||
return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < |
|||
60 |
|||
? 'just now' |
|||
: distanceString; |
|||
} |
|||
|
|||
return ''; |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
|
|||
private fetchAdminData() { |
|||
this.dataService |
|||
.fetchAdminData() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe( |
|||
({ |
|||
dataGatheringProgress, |
|||
exchangeRates, |
|||
lastDataGathering, |
|||
transactionCount, |
|||
userCount |
|||
}) => { |
|||
this.dataGatheringProgress = dataGatheringProgress; |
|||
this.exchangeRates = exchangeRates; |
|||
|
|||
if (isValid(parseISO(lastDataGathering?.toString()))) { |
|||
this.lastDataGathering = formatDistanceToNowStrict( |
|||
new Date(lastDataGathering), |
|||
{ |
|||
addSuffix: true |
|||
} |
|||
); |
|||
} else if (lastDataGathering === 'IN_PROGRESS') { |
|||
this.dataGatheringInProgress = true; |
|||
} else { |
|||
this.lastDataGathering = 'Starting soon...'; |
|||
} |
|||
|
|||
this.transactionCount = transactionCount; |
|||
this.userCount = userCount; |
|||
|
|||
this.changeDetectorRef.markForCheck(); |
|||
} |
|||
); |
|||
} |
|||
} |
@ -0,0 +1,107 @@ |
|||
<div class="container"> |
|||
<div class="mb-5 row"> |
|||
<div class="col"> |
|||
<mat-card class="mb-3"> |
|||
<mat-card-content> |
|||
<div |
|||
*ngIf="exchangeRates?.length > 0" |
|||
class="align-items-start d-flex my-3" |
|||
> |
|||
<div class="w-50" i18n>Exchange Rates</div> |
|||
<div class="w-50"> |
|||
<table> |
|||
<tr *ngFor="let exchangeRate of exchangeRates"> |
|||
<td class="d-flex"> |
|||
<gf-value |
|||
[locale]="user?.settings?.locale" |
|||
[value]="1" |
|||
></gf-value> |
|||
</td> |
|||
<td class="pl-1">{{ exchangeRate.label1 }}</td> |
|||
<td class="px-1">=</td> |
|||
<td class="d-flex justify-content-end"> |
|||
<gf-value |
|||
[locale]="user?.settings?.locale" |
|||
[precision]="4" |
|||
[value]="exchangeRate.value" |
|||
></gf-value> |
|||
</td> |
|||
<td class="pl-1">{{ exchangeRate.label2 }}</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
<div class="d-flex my-3"> |
|||
<div class="w-50" i18n>Data Gathering</div> |
|||
<div class="w-50"> |
|||
<div> |
|||
<ng-container *ngIf="lastDataGathering" |
|||
>{{ lastDataGathering }}</ng-container |
|||
> |
|||
<ng-container *ngIf="dataGatheringInProgress" i18n |
|||
>In Progress ({{ dataGatheringProgress | percent : '1.2-2' |
|||
}})</ng-container |
|||
> |
|||
</div> |
|||
<div class="mt-2 overflow-hidden"> |
|||
<div class="mb-2"> |
|||
<button |
|||
class="mw-100" |
|||
color="accent" |
|||
mat-flat-button |
|||
(click)="onFlushCache()" |
|||
> |
|||
<ion-icon |
|||
class="mr-1" |
|||
name="close-circle-outline" |
|||
></ion-icon> |
|||
<span i18n>Reset Data Gathering</span> |
|||
</button> |
|||
</div> |
|||
<div class="mb-2"> |
|||
<button |
|||
class="mw-100" |
|||
color="warn" |
|||
mat-flat-button |
|||
[disabled]="dataGatheringInProgress" |
|||
(click)="onGatherMax()" |
|||
> |
|||
<ion-icon class="mr-1" name="warning-outline"></ion-icon> |
|||
<span i18n>Gather All Data</span> |
|||
</button> |
|||
</div> |
|||
<div> |
|||
<button |
|||
class="mb-2 mr-2 mw-100" |
|||
color="accent" |
|||
mat-flat-button |
|||
(click)="onGatherProfileData()" |
|||
> |
|||
<ion-icon |
|||
class="mr-1" |
|||
name="cloud-download-outline" |
|||
></ion-icon> |
|||
<span i18n>Gather Profile Data</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="d-flex my-3"> |
|||
<div class="w-50" i18n>User Count</div> |
|||
<div class="w-50">{{ userCount }}</div> |
|||
</div> |
|||
<div class="d-flex my-3"> |
|||
<div class="w-50" i18n>Transaction Count</div> |
|||
<div class="w-50"> |
|||
<ng-container *ngIf="transactionCount"> |
|||
{{ transactionCount }} ({{ transactionCount / userCount | number |
|||
: '1.2-2' }} <span i18n>per User</span>) |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
</div> |
|||
</div> |
|||
</div> |
@ -0,0 +1,17 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { MatCardModule } from '@angular/material/card'; |
|||
import { CacheService } from '@ghostfolio/client/services/cache.service'; |
|||
import { GfValueModule } from '@ghostfolio/ui/value'; |
|||
|
|||
import { AdminOverviewComponent } from './admin-overview.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [AdminOverviewComponent], |
|||
exports: [], |
|||
imports: [CommonModule, GfValueModule, MatButtonModule, MatCardModule], |
|||
providers: [CacheService], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
|||
}) |
|||
export class AdminOverviewModule {} |
@ -0,0 +1,17 @@ |
|||
@import '~apps/client/src/styles/ghostfolio-style'; |
|||
|
|||
:host { |
|||
display: block; |
|||
|
|||
.mat-flat-button { |
|||
::ng-deep { |
|||
.mat-button-wrapper { |
|||
display: block; |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
white-space: nowrap; |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,82 @@ |
|||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; |
|||
import { DataService } from '@ghostfolio/client/services/data.service'; |
|||
import { AdminData } from '@ghostfolio/common/interfaces'; |
|||
import { |
|||
differenceInSeconds, |
|||
formatDistanceToNowStrict, |
|||
parseISO |
|||
} from 'date-fns'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
selector: 'gf-admin-users', |
|||
styleUrls: ['./admin-users.scss'], |
|||
templateUrl: './admin-users.html' |
|||
}) |
|||
export class AdminUsersComponent implements OnDestroy, OnInit { |
|||
public users: AdminData['users']; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
/** |
|||
* @constructor |
|||
*/ |
|||
public constructor( |
|||
private changeDetectorRef: ChangeDetectorRef, |
|||
private dataService: DataService |
|||
) {} |
|||
|
|||
/** |
|||
* Initializes the controller |
|||
*/ |
|||
public ngOnInit() { |
|||
this.fetchAdminData(); |
|||
} |
|||
|
|||
public formatDistanceToNow(aDateString: string) { |
|||
if (aDateString) { |
|||
const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { |
|||
addSuffix: true |
|||
}); |
|||
|
|||
return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < |
|||
60 |
|||
? 'just now' |
|||
: distanceString; |
|||
} |
|||
|
|||
return ''; |
|||
} |
|||
|
|||
public onDeleteUser(aId: string) { |
|||
const confirmation = confirm('Do you really want to delete this user?'); |
|||
|
|||
if (confirmation) { |
|||
this.dataService |
|||
.deleteUser(aId) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe({ |
|||
next: () => { |
|||
this.fetchAdminData(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
|
|||
private fetchAdminData() { |
|||
this.dataService |
|||
.fetchAdminData() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(({ users }) => { |
|||
this.users = users; |
|||
|
|||
this.changeDetectorRef.markForCheck(); |
|||
}); |
|||
} |
|||
} |
@ -0,0 +1,86 @@ |
|||
<div class="container"> |
|||
<div class="row"> |
|||
<div class="col"> |
|||
<div class="users"> |
|||
<table class="gf-table"> |
|||
<thead> |
|||
<tr class="mat-header-row"> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th> |
|||
<th class="mat-header-cell px-1 py-2" i18n>User</th> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n> |
|||
Registration |
|||
</th> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n> |
|||
Accounts |
|||
</th> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n> |
|||
Transactions |
|||
</th> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n> |
|||
Engagement per Day |
|||
</th> |
|||
<th class="mat-header-cell px-1 py-2" i18n>Last Activitiy</th> |
|||
<th class="mat-header-cell px-1 py-2"></th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr *ngFor="let userItem of users; let i = index" class="mat-row"> |
|||
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td> |
|||
<td class="mat-cell px-1 py-2"> |
|||
<div class="d-flex align-items-center"> |
|||
<span class="d-none d-sm-inline-block" |
|||
>{{ userItem.alias || userItem.id }}</span |
|||
> |
|||
<span class="d-inline-block d-sm-none" |
|||
>{{ userItem.alias || (userItem.id | slice:0:5) + |
|||
'...' }}</span |
|||
> |
|||
<ion-icon |
|||
*ngIf="userItem?.subscription?.type === 'Premium'" |
|||
class="ml-1 text-muted" |
|||
name="diamond-outline" |
|||
></ion-icon> |
|||
</div> |
|||
</td> |
|||
<td class="mat-cell px-1 py-2 text-right"> |
|||
{{ formatDistanceToNow(userItem.createdAt) }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2 text-right"> |
|||
{{ userItem.accountCount }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2 text-right"> |
|||
{{ userItem.transactionCount }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2 text-right"> |
|||
{{ userItem.engagement | number: '1.0-0' }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2"> |
|||
{{ formatDistanceToNow(userItem.lastActivity) }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2"> |
|||
<button |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="accountMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-vertical"></ion-icon> |
|||
</button> |
|||
<mat-menu #accountMenu="matMenu" xPosition="before"> |
|||
<button |
|||
i18n |
|||
mat-menu-item |
|||
[disabled]="userItem.id === user?.id" |
|||
(click)="onDeleteUser(userItem.id)" |
|||
> |
|||
Delete |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
@ -0,0 +1,14 @@ |
|||
import { CommonModule } from '@angular/common'; |
|||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
|||
import { MatButtonModule } from '@angular/material/button'; |
|||
import { MatMenuModule } from '@angular/material/menu'; |
|||
|
|||
import { AdminUsersComponent } from './admin-users.component'; |
|||
|
|||
@NgModule({ |
|||
declarations: [AdminUsersComponent], |
|||
exports: [], |
|||
imports: [CommonModule, MatButtonModule, MatMenuModule], |
|||
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
|||
}) |
|||
export class AdminUsersModule {} |
@ -0,0 +1,18 @@ |
|||
@import '~apps/client/src/styles/ghostfolio-style'; |
|||
|
|||
:host { |
|||
display: block; |
|||
|
|||
.users { |
|||
overflow-x: auto; |
|||
|
|||
table { |
|||
min-width: 100%; |
|||
|
|||
.mat-row, |
|||
.mat-header-row { |
|||
width: 100%; |
|||
} |
|||
} |
|||
} |
|||
} |
@ -1,169 +1,26 @@ |
|||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; |
|||
import { AdminService } from '@ghostfolio/client/services/admin.service'; |
|||
import { CacheService } from '@ghostfolio/client/services/cache.service'; |
|||
import { DataService } from '@ghostfolio/client/services/data.service'; |
|||
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
|||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; |
|||
import { AdminData, User } from '@ghostfolio/common/interfaces'; |
|||
import { |
|||
differenceInSeconds, |
|||
formatDistanceToNowStrict, |
|||
isValid, |
|||
parseISO |
|||
} from 'date-fns'; |
|||
import { Component, OnDestroy, OnInit } from '@angular/core'; |
|||
import { Subject } from 'rxjs'; |
|||
import { takeUntil } from 'rxjs/operators'; |
|||
|
|||
@Component({ |
|||
host: { class: 'mb-5' }, |
|||
selector: 'gf-admin-page', |
|||
styleUrls: ['./admin-page.scss'], |
|||
templateUrl: './admin-page.html' |
|||
}) |
|||
export class AdminPageComponent implements OnDestroy, OnInit { |
|||
public dataGatheringInProgress: boolean; |
|||
public dataGatheringProgress: number; |
|||
public defaultDateFormat = DEFAULT_DATE_FORMAT; |
|||
public exchangeRates: { label1: string; label2: string; value: number }[]; |
|||
public lastDataGathering: string; |
|||
public transactionCount: number; |
|||
public userCount: number; |
|||
public user: User; |
|||
public users: AdminData['users']; |
|||
|
|||
private unsubscribeSubject = new Subject<void>(); |
|||
|
|||
/** |
|||
* @constructor |
|||
*/ |
|||
public constructor( |
|||
private adminService: AdminService, |
|||
private cacheService: CacheService, |
|||
private changeDetectorRef: ChangeDetectorRef, |
|||
private dataService: DataService, |
|||
private userService: UserService |
|||
) {} |
|||
public constructor() {} |
|||
|
|||
/** |
|||
* Initializes the controller |
|||
*/ |
|||
public ngOnInit() { |
|||
this.fetchAdminData(); |
|||
|
|||
this.userService.stateChanged |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe((state) => { |
|||
if (state?.user) { |
|||
this.user = state.user; |
|||
} |
|||
}); |
|||
} |
|||
|
|||
public onFlushCache() { |
|||
this.cacheService |
|||
.flush() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(() => { |
|||
setTimeout(() => { |
|||
window.location.reload(); |
|||
}, 300); |
|||
}); |
|||
} |
|||
|
|||
public onGatherMax() { |
|||
const confirmation = confirm( |
|||
'This action may take some time. Do you want to proceed?' |
|||
); |
|||
|
|||
if (confirmation === true) { |
|||
this.adminService |
|||
.gatherMax() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(() => { |
|||
setTimeout(() => { |
|||
window.location.reload(); |
|||
}, 300); |
|||
}); |
|||
} |
|||
} |
|||
|
|||
public onGatherProfileData() { |
|||
this.adminService |
|||
.gatherProfileData() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe(() => {}); |
|||
} |
|||
|
|||
public formatDistanceToNow(aDateString: string) { |
|||
if (aDateString) { |
|||
const distanceString = formatDistanceToNowStrict(parseISO(aDateString), { |
|||
addSuffix: true |
|||
}); |
|||
|
|||
return Math.abs(differenceInSeconds(parseISO(aDateString), new Date())) < |
|||
60 |
|||
? 'just now' |
|||
: distanceString; |
|||
} |
|||
|
|||
return ''; |
|||
} |
|||
|
|||
public onDeleteUser(aId: string) { |
|||
const confirmation = confirm('Do you really want to delete this user?'); |
|||
|
|||
if (confirmation) { |
|||
this.dataService |
|||
.deleteUser(aId) |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe({ |
|||
next: () => { |
|||
this.fetchAdminData(); |
|||
} |
|||
}); |
|||
} |
|||
} |
|||
public ngOnInit() {} |
|||
|
|||
public ngOnDestroy() { |
|||
this.unsubscribeSubject.next(); |
|||
this.unsubscribeSubject.complete(); |
|||
} |
|||
|
|||
private fetchAdminData() { |
|||
this.dataService |
|||
.fetchAdminData() |
|||
.pipe(takeUntil(this.unsubscribeSubject)) |
|||
.subscribe( |
|||
({ |
|||
dataGatheringProgress, |
|||
exchangeRates, |
|||
lastDataGathering, |
|||
transactionCount, |
|||
userCount, |
|||
users |
|||
}) => { |
|||
this.dataGatheringProgress = dataGatheringProgress; |
|||
this.exchangeRates = exchangeRates; |
|||
this.users = users; |
|||
|
|||
if (isValid(parseISO(lastDataGathering?.toString()))) { |
|||
this.lastDataGathering = formatDistanceToNowStrict( |
|||
new Date(lastDataGathering), |
|||
{ |
|||
addSuffix: true |
|||
} |
|||
); |
|||
} else if (lastDataGathering === 'IN_PROGRESS') { |
|||
this.dataGatheringInProgress = true; |
|||
} else { |
|||
this.lastDataGathering = 'Starting soon...'; |
|||
} |
|||
|
|||
this.transactionCount = transactionCount; |
|||
this.userCount = userCount; |
|||
|
|||
this.changeDetectorRef.markForCheck(); |
|||
} |
|||
); |
|||
} |
|||
} |
|||
|
@ -1,195 +1,17 @@ |
|||
<div class="container"> |
|||
<div class="mb-5 row"> |
|||
<div class="col"> |
|||
<h3 class="d-flex justify-content-center mb-3" i18n> |
|||
Admin Control Panel |
|||
</h3> |
|||
<mat-card class="mb-3"> |
|||
<mat-card-content> |
|||
<div |
|||
*ngIf="exchangeRates?.length > 0" |
|||
class="align-items-start d-flex my-3" |
|||
> |
|||
<div class="w-50" i18n>Exchange Rates</div> |
|||
<div class="w-50"> |
|||
<table> |
|||
<tr *ngFor="let exchangeRate of exchangeRates"> |
|||
<td class="d-flex"> |
|||
<gf-value |
|||
[locale]="user?.settings?.locale" |
|||
[value]="1" |
|||
></gf-value> |
|||
</td> |
|||
<td class="pl-1">{{ exchangeRate.label1 }}</td> |
|||
<td class="px-1">=</td> |
|||
<td class="d-flex justify-content-end"> |
|||
<gf-value |
|||
[locale]="user?.settings?.locale" |
|||
[precision]="4" |
|||
[value]="exchangeRate.value" |
|||
></gf-value> |
|||
</td> |
|||
<td class="pl-1">{{ exchangeRate.label2 }}</td> |
|||
</tr> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
<div class="d-flex my-3"> |
|||
<div class="w-50" i18n>Data Gathering</div> |
|||
<div class="w-50"> |
|||
<div> |
|||
<ng-container *ngIf="lastDataGathering" |
|||
>{{ lastDataGathering }}</ng-container |
|||
> |
|||
<ng-container *ngIf="dataGatheringInProgress" i18n |
|||
>In Progress ({{ dataGatheringProgress | percent : '1.2-2' |
|||
}})</ng-container |
|||
> |
|||
</div> |
|||
<div class="mt-2 overflow-hidden"> |
|||
<div class="mb-2"> |
|||
<button |
|||
class="mw-100" |
|||
color="accent" |
|||
mat-flat-button |
|||
(click)="onFlushCache()" |
|||
> |
|||
<ion-icon |
|||
class="mr-1" |
|||
name="close-circle-outline" |
|||
></ion-icon> |
|||
<span i18n>Reset Data Gathering</span> |
|||
</button> |
|||
</div> |
|||
<div class="mb-2"> |
|||
<button |
|||
class="mw-100" |
|||
color="warn" |
|||
mat-flat-button |
|||
[disabled]="dataGatheringInProgress" |
|||
(click)="onGatherMax()" |
|||
> |
|||
<ion-icon class="mr-1" name="warning-outline"></ion-icon> |
|||
<span i18n>Gather All Data</span> |
|||
</button> |
|||
</div> |
|||
<div> |
|||
<button |
|||
class="mb-2 mr-2 mw-100" |
|||
color="accent" |
|||
mat-flat-button |
|||
(click)="onGatherProfileData()" |
|||
> |
|||
<ion-icon |
|||
class="mr-1" |
|||
name="cloud-download-outline" |
|||
></ion-icon> |
|||
<span i18n>Gather Profile Data</span> |
|||
</button> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="d-flex my-3"> |
|||
<div class="w-50" i18n>User Count</div> |
|||
<div class="w-50">{{ userCount }}</div> |
|||
</div> |
|||
<div class="d-flex my-3"> |
|||
<div class="w-50" i18n>Transaction Count</div> |
|||
<div class="w-50"> |
|||
<ng-container *ngIf="transactionCount"> |
|||
{{ transactionCount }} ({{ transactionCount / userCount | number |
|||
: '1.2-2' }} <span i18n>per User</span>) |
|||
</ng-container> |
|||
</div> |
|||
</div> |
|||
</mat-card-content> |
|||
</mat-card> |
|||
</div> |
|||
</div> |
|||
<div class="row"> |
|||
<div class="col"> |
|||
<h3 class="mb-3 text-center" i18n>Users</h3> |
|||
<div class="users"> |
|||
<table class="gf-table"> |
|||
<thead> |
|||
<tr class="mat-header-row"> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n>#</th> |
|||
<th class="mat-header-cell px-1 py-2" i18n>User</th> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n> |
|||
Registration |
|||
</th> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n> |
|||
Accounts |
|||
</th> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n> |
|||
Transactions |
|||
</th> |
|||
<th class="mat-header-cell px-1 py-2 text-right" i18n> |
|||
Engagement per Day |
|||
</th> |
|||
<th class="mat-header-cell px-1 py-2" i18n>Last Activitiy</th> |
|||
<th class="mat-header-cell px-1 py-2"></th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr *ngFor="let userItem of users; let i = index" class="mat-row"> |
|||
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td> |
|||
<td class="mat-cell px-1 py-2"> |
|||
<div class="d-flex align-items-center"> |
|||
<span class="d-none d-sm-inline-block" |
|||
>{{ userItem.alias || userItem.id }}</span |
|||
> |
|||
<span class="d-inline-block d-sm-none" |
|||
>{{ userItem.alias || (userItem.id | slice:0:5) + |
|||
'...' }}</span |
|||
> |
|||
<ion-icon |
|||
*ngIf="userItem?.subscription?.type === 'Premium'" |
|||
class="ml-1 text-muted" |
|||
name="diamond-outline" |
|||
></ion-icon> |
|||
</div> |
|||
</td> |
|||
<td class="mat-cell px-1 py-2 text-right"> |
|||
{{ formatDistanceToNow(userItem.createdAt) }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2 text-right"> |
|||
{{ userItem.accountCount }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2 text-right"> |
|||
{{ userItem.transactionCount }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2 text-right"> |
|||
{{ userItem.engagement | number: '1.0-0' }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2"> |
|||
{{ formatDistanceToNow(userItem.lastActivity) }} |
|||
</td> |
|||
<td class="mat-cell px-1 py-2"> |
|||
<button |
|||
class="mx-1 no-min-width px-2" |
|||
mat-button |
|||
[matMenuTriggerFor]="accountMenu" |
|||
(click)="$event.stopPropagation()" |
|||
> |
|||
<ion-icon name="ellipsis-vertical"></ion-icon> |
|||
</button> |
|||
<mat-menu #accountMenu="matMenu" xPosition="before"> |
|||
<button |
|||
i18n |
|||
mat-menu-item |
|||
[disabled]="userItem.id === user?.id" |
|||
(click)="onDeleteUser(userItem.id)" |
|||
> |
|||
Delete |
|||
</button> |
|||
</mat-menu> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<router-outlet></router-outlet> |
|||
|
|||
<nav mat-align-tabs="center" mat-tab-nav-bar> |
|||
<a |
|||
*ngFor="let link of [ |
|||
{ iconName: 'reader-outline', path: 'overview' }, |
|||
{ iconName: 'people-outline', path: 'users' } |
|||
]" |
|||
#rla="routerLinkActive" |
|||
mat-tab-link |
|||
routerLinkActive |
|||
[active]="rla.isActive" |
|||
[routerLink]="link.path" |
|||
> |
|||
<ion-icon size="large" [name]="link.iconName"></ion-icon> |
|||
</a> |
|||
</nav> |
|||
|
Loading…
Reference in new issue