Browse Source

Task/improve coupon management in admin control panel (#6794)

* Improve coupon management
pull/6802/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
404ef252c7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      apps/client/src/app/components/access-table/access-table.component.html
  2. 4
      apps/client/src/app/components/admin-jobs/admin-jobs.html
  3. 4
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  4. 62
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  5. 142
      apps/client/src/app/components/admin-overview/admin-overview.html
  6. 4
      apps/client/src/app/components/admin-users/admin-users.html

4
apps/client/src/app/components/access-table/access-table.component.html

@ -53,9 +53,9 @@
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th> <th *matHeaderCellDef class="px-1 text-right" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell> <td *matCellDef="let element" class="px-1 text-right" mat-cell>
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button

4
apps/client/src/app/components/admin-jobs/admin-jobs.html

@ -188,7 +188,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 py-2" mat-header-cell> <th *matHeaderCellDef class="px-1 py-2 text-right" mat-header-cell>
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
@ -203,7 +203,7 @@
</button> </button>
</mat-menu> </mat-menu>
</th> </th>
<td *matCellDef="let element" class="px-1 py-2" mat-cell> <td *matCellDef="let element" class="px-1 py-2 text-right" mat-cell>
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button

4
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -206,7 +206,7 @@
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell> <th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
@ -243,7 +243,7 @@
</button> </button>
</mat-menu> </mat-menu>
</th> </th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell> <td *matCellDef="let element" class="px-1 text-right" mat-cell>
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button

62
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -24,6 +24,7 @@ import { NotificationService } from '@ghostfolio/ui/notifications';
import { AdminService, DataService } from '@ghostfolio/ui/services'; import { AdminService, DataService } from '@ghostfolio/ui/services';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
@ -42,6 +43,7 @@ import {
MatSlideToggleModule MatSlideToggleModule
} from '@angular/material/slide-toggle'; } from '@angular/material/slide-toggle';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { import {
@ -62,6 +64,7 @@ import ms, { StringValue } from 'ms';
@Component({ @Component({
imports: [ imports: [
ClipboardModule,
CommonModule, CommonModule,
FormsModule, FormsModule,
GfValueComponent, GfValueComponent,
@ -72,6 +75,7 @@ import ms, { StringValue } from 'ms';
MatSelectModule, MatSelectModule,
MatSnackBarModule, MatSnackBarModule,
MatSlideToggleModule, MatSlideToggleModule,
MatTableModule,
ReactiveFormsModule, ReactiveFormsModule,
RouterModule RouterModule
], ],
@ -82,7 +86,8 @@ import ms, { StringValue } from 'ms';
export class GfAdminOverviewComponent implements OnInit { export class GfAdminOverviewComponent implements OnInit {
public activitiesCount: number; public activitiesCount: number;
public couponDuration: StringValue = '14 days'; public couponDuration: StringValue = '14 days';
public coupons: Coupon[]; public couponsDataSource = new MatTableDataSource<Coupon>();
public couponsDisplayedColumns = ['code', 'duration', 'actions'];
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public hasPermissionForSystemMessage: boolean; public hasPermissionForSystemMessage: boolean;
public hasPermissionToSyncDemoUserAccount: boolean; public hasPermissionToSyncDemoUserAccount: boolean;
@ -99,6 +104,7 @@ export class GfAdminOverviewComponent implements OnInit {
private adminService: AdminService, private adminService: AdminService,
private cacheService: CacheService, private cacheService: CacheService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private clipboard: Clipboard,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private notificationService: NotificationService, private notificationService: NotificationService,
@ -188,14 +194,14 @@ export class GfAdminOverviewComponent implements OnInit {
} }
public onAddCoupon() { public onAddCoupon() {
const coupons = [ const newCoupon: Coupon = {
...this.coupons,
{
code: `${ghostfolioPrefix}${this.generateCouponCode(14)}`, code: `${ghostfolioPrefix}${this.generateCouponCode(14)}`,
duration: this.couponDuration duration: this.couponDuration
} };
];
this.putAdminSetting({ key: PROPERTY_COUPONS, value: coupons }); const coupons = [...this.couponsDataSource.data, newCoupon];
this.saveCoupons({ coupons, codeToCopy: newCoupon.code });
} }
public onChangeCouponDuration(aCouponDuration: StringValue) { public onChangeCouponDuration(aCouponDuration: StringValue) {
@ -205,10 +211,11 @@ export class GfAdminOverviewComponent implements OnInit {
public onDeleteCoupon(aCouponCode: string) { public onDeleteCoupon(aCouponCode: string) {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
const coupons = this.coupons.filter((coupon) => { const coupons = this.couponsDataSource.data.filter(({ code }) => {
return coupon.code !== aCouponCode; return code !== aCouponCode;
}); });
this.putAdminSetting({ key: PROPERTY_COUPONS, value: coupons });
this.saveCoupons({ coupons });
}, },
confirmType: ConfirmationDialogType.Warn, confirmType: ConfirmationDialogType.Warn,
title: $localize`Do you really want to delete this coupon?` title: $localize`Do you really want to delete this coupon?`
@ -307,9 +314,13 @@ export class GfAdminOverviewComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ activitiesCount, settings, userCount, version }) => { .subscribe(({ activitiesCount, settings, userCount, version }) => {
this.activitiesCount = activitiesCount; this.activitiesCount = activitiesCount;
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
this.couponsDataSource.data =
(settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
this.isDataGatheringEnabled = this.isDataGatheringEnabled =
settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true; settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true;
this.systemMessage = settings[PROPERTY_SYSTEM_MESSAGE] as SystemMessage; this.systemMessage = settings[PROPERTY_SYSTEM_MESSAGE] as SystemMessage;
this.userCount = userCount; this.userCount = userCount;
this.version = version; this.version = version;
@ -343,4 +354,33 @@ export class GfAdminOverviewComponent implements OnInit {
}, 300); }, 300);
}); });
} }
private saveCoupons({
codeToCopy,
coupons
}: {
codeToCopy?: string;
coupons: Coupon[];
}) {
this.dataService
.putAdminSetting(PROPERTY_COUPONS, {
value: JSON.stringify(coupons)
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.couponsDataSource.data = coupons;
if (codeToCopy) {
this.clipboard.copy(codeToCopy);
this.snackBar.open(
'✅ ' + $localize`${codeToCopy} has been copied to the clipboard`,
undefined,
{ duration: ms('3 seconds') }
);
}
this.changeDetectorRef.markForCheck();
});
}
} }

142
apps/client/src/app/components/admin-overview/admin-overview.html

@ -113,24 +113,84 @@
</div> </div>
</div> </div>
} }
@if (hasPermissionForSubscription) { <div class="d-flex my-3">
<div class="d-flex my-3 subscription"> <div class="w-50" i18n>Housekeeping</div>
<div class="w-50" i18n>Coupons</div>
<div class="w-50"> <div class="w-50">
<table> <div class="align-items-start d-flex flex-column">
@for (coupon of coupons; track coupon) { @if (hasPermissionToSyncDemoUserAccount) {
<tr> <button
<td> class="mb-2"
color="accent"
mat-flat-button
(click)="onSyncDemoUserAccount()"
>
<ion-icon class="mr-1" name="sync-outline" />
<span i18n>Sync Demo User Account</span>
</button>
}
<button color="warn" mat-flat-button (click)="onFlushCache()">
<ion-icon class="mr-1" name="close-circle-outline" />
<span i18n>Flush Cache</span>
</button>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
</div>
@if (hasPermissionForSubscription) {
<div class="row">
<div class="col">
<mat-card appearance="outlined">
<mat-card-header>
<mat-card-title i18n>Coupons</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="overflow-x-auto">
<table
class="gf-table w-100"
mat-table
[dataSource]="couponsDataSource"
>
<ng-container matColumnDef="code">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Code</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<gf-value <gf-value
class="text-monospace" class="text-monospace"
[enableCopyToClipboardButton]="true" [enableCopyToClipboardButton]="true"
[value]="coupon.code" [value]="element.code"
/> />
</td> </td>
<td class="pl-2 text-right"> </ng-container>
{{ formatStringValue(coupon.duration) }}
<ng-container matColumnDef="duration">
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
<ng-container i18n>Duration</ng-container>
</th>
<td
*matCellDef="let element"
class="px-1 text-right"
mat-cell
>
{{ formatStringValue(element.duration) }}
</td> </td>
<td> </ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th
*matHeaderCellDef
class="px-1 text-right"
mat-header-cell
></th>
<td
*matCellDef="let element"
class="px-1 text-right"
mat-cell
>
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
@ -141,12 +201,12 @@
</button> </button>
<mat-menu <mat-menu
#couponActionsMenu="matMenu" #couponActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2" class="no-max-width"
xPosition="before" xPosition="before"
> >
<button <button
mat-menu-item mat-menu-item
(click)="onDeleteCoupon(coupon.code)" (click)="onDeleteCoupon(element.code)"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" /> <ion-icon class="mr-2" name="trash-outline" />
@ -155,15 +215,22 @@
</button> </button>
</mat-menu> </mat-menu>
</td> </td>
</tr> </ng-container>
}
<tr
*matHeaderRowDef="couponsDisplayedColumns"
mat-header-row
></tr>
<tr
*matRowDef="let row; columns: couponsDisplayedColumns"
mat-row
></tr>
</table> </table>
<div class="mt-2"> </div>
<form #couponForm="ngForm" class="align-items-center d-flex"> </mat-card-content>
<mat-form-field <mat-card-actions align="end">
appearance="outline" <form #couponForm="ngForm" class="align-items-stretch d-flex">
class="mr-2 without-hint" <mat-form-field appearance="outline" class="mr-1 without-hint">
>
<mat-select <mat-select
name="duration" name="duration"
[value]="couponDuration" [value]="couponDuration"
@ -190,42 +257,17 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<button <button
class="mt-1" class="h-auto rounded"
color="primary" color="primary"
mat-flat-button mat-flat-button
(click)="onAddCoupon()" (click)="onAddCoupon()"
> >
<span i18n>Add</span> <ng-container i18n>Add</ng-container>
</button> </button>
</form> </form>
</div> </mat-card-actions>
</div>
</div>
}
<div class="d-flex my-3">
<div class="w-50" i18n>Housekeeping</div>
<div class="w-50">
<div class="align-items-start d-flex flex-column">
@if (hasPermissionToSyncDemoUserAccount) {
<button
class="mb-2"
color="accent"
mat-flat-button
(click)="onSyncDemoUserAccount()"
>
<ion-icon class="mr-1" name="sync-outline" />
<span i18n>Sync Demo User Account</span>
</button>
}
<button color="warn" mat-flat-button (click)="onFlushCache()">
<ion-icon class="mr-1" name="close-circle-outline" />
<span i18n>Flush Cache</span>
</button>
</div>
</div>
</div>
</mat-card-content>
</mat-card> </mat-card>
</div> </div>
</div> </div>
}
</div> </div>

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

@ -198,12 +198,12 @@
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th <th
*matHeaderCellDef *matHeaderCellDef
class="mat-mdc-header-cell px-1 py-2" class="mat-mdc-header-cell px-1 py-2 text-right"
mat-header-cell mat-header-cell
></th> ></th>
<td <td
*matCellDef="let element" *matCellDef="let element"
class="mat-mdc-cell px-1 py-2" class="mat-mdc-cell px-1 py-2 text-right"
mat-cell mat-cell
> >
<button <button

Loading…
Cancel
Save