Feature/add frontend for watchlist (#4604)
* Add frontend for watchlist * Update changelog --------- Co-authored-by: Thomas Kaul <4159106+dtslvr@users.noreply.github.com>
This commit is contained in:
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Introduced a watchlist to follow assets (experimental)
|
||||||
|
|
||||||
## 2.156.0 - 2025-04-27
|
## 2.156.0 - 2025-04-27
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -2,7 +2,7 @@ import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorat
|
|||||||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
|
||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
|
||||||
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
|
import { WatchlistResponse } from '@ghostfolio/common/interfaces';
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
|
|
||||||
@ -53,13 +53,13 @@ export class WatchlistController {
|
|||||||
@Param('dataSource') dataSource: DataSource,
|
@Param('dataSource') dataSource: DataSource,
|
||||||
@Param('symbol') symbol: string
|
@Param('symbol') symbol: string
|
||||||
) {
|
) {
|
||||||
const watchlistItem = await this.watchlistService
|
const watchlistItems = await this.watchlistService.getWatchlistItems(
|
||||||
.getWatchlistItems(this.request.user.id)
|
this.request.user.id
|
||||||
.then((items) => {
|
);
|
||||||
return items.find((item) => {
|
|
||||||
return item.dataSource === dataSource && item.symbol === symbol;
|
const watchlistItem = watchlistItems.find((item) => {
|
||||||
});
|
return item.dataSource === dataSource && item.symbol === symbol;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!watchlistItem) {
|
if (!watchlistItem) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@ -79,7 +79,13 @@ export class WatchlistController {
|
|||||||
@HasPermission(permissions.readWatchlist)
|
@HasPermission(permissions.readWatchlist)
|
||||||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getWatchlistItems(): Promise<AssetProfileIdentifier[]> {
|
public async getWatchlistItems(): Promise<WatchlistResponse> {
|
||||||
return this.watchlistService.getWatchlistItems(this.request.user.id);
|
const watchlist = await this.watchlistService.getWatchlistItems(
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
watchlist
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
|
||||||
|
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
AbstractControl,
|
||||||
|
FormBuilder,
|
||||||
|
FormControl,
|
||||||
|
FormGroup,
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
ValidationErrors,
|
||||||
|
Validators
|
||||||
|
} from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
host: { class: 'h-100' },
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
GfSymbolAutocompleteComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
MatDialogModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
selector: 'gf-create-watchlist-item-dialog',
|
||||||
|
styleUrls: ['./create-watchlist-item-dialog.component.scss'],
|
||||||
|
templateUrl: 'create-watchlist-item-dialog.html'
|
||||||
|
})
|
||||||
|
export class CreateWatchlistItemDialogComponent implements OnInit, OnDestroy {
|
||||||
|
public createWatchlistItemForm: FormGroup;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public readonly dialogRef: MatDialogRef<CreateWatchlistItemDialogComponent>,
|
||||||
|
public readonly formBuilder: FormBuilder
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.createWatchlistItemForm = this.formBuilder.group(
|
||||||
|
{
|
||||||
|
searchSymbol: new FormControl(null, [Validators.required])
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validators: this.validator
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onCancel() {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSubmit() {
|
||||||
|
this.dialogRef.close({
|
||||||
|
dataSource:
|
||||||
|
this.createWatchlistItemForm.get('searchSymbol').value.dataSource,
|
||||||
|
symbol: this.createWatchlistItemForm.get('searchSymbol').value.symbol
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private validator(control: AbstractControl): ValidationErrors {
|
||||||
|
const searchSymbolControl = control.get('searchSymbol');
|
||||||
|
|
||||||
|
if (
|
||||||
|
searchSymbolControl.valid &&
|
||||||
|
searchSymbolControl.value.dataSource &&
|
||||||
|
searchSymbolControl.value.symbol
|
||||||
|
) {
|
||||||
|
return { incomplete: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { incomplete: true };
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<form
|
||||||
|
class="d-flex flex-column h-100"
|
||||||
|
[formGroup]="createWatchlistItemForm"
|
||||||
|
(keyup.enter)="createWatchlistItemForm.valid && onSubmit()"
|
||||||
|
(ngSubmit)="onSubmit()"
|
||||||
|
>
|
||||||
|
<h1 i18n mat-dialog-title>Add asset to watchlist</h1>
|
||||||
|
<div class="flex-grow-1 py-3" mat-dialog-content>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Name, symbol or ISIN</mat-label>
|
||||||
|
<gf-symbol-autocomplete formControlName="searchSymbol" />
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end" mat-dialog-actions>
|
||||||
|
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
<button
|
||||||
|
color="primary"
|
||||||
|
mat-flat-button
|
||||||
|
type="submit"
|
||||||
|
[disabled]="createWatchlistItemForm.hasError('incomplete')"
|
||||||
|
>
|
||||||
|
<ng-container i18n>Save</ng-container>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
@ -0,0 +1,4 @@
|
|||||||
|
export interface CreateWatchlistItemDialogParams {
|
||||||
|
deviceType: string;
|
||||||
|
locale: string;
|
||||||
|
}
|
@ -0,0 +1,145 @@
|
|||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { Benchmark, User } from '@ghostfolio/common/interfaces';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
|
||||||
|
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||||
|
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
CUSTOM_ELEMENTS_SCHEMA,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { CreateWatchlistItemDialogComponent } from './create-watchlist-item-dialog/create-watchlist-item-dialog.component';
|
||||||
|
import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/interfaces/interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfBenchmarkComponent,
|
||||||
|
GfPremiumIndicatorComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
RouterModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
|
selector: 'gf-home-watchlist',
|
||||||
|
styleUrls: ['./home-watchlist.scss'],
|
||||||
|
templateUrl: './home-watchlist.html'
|
||||||
|
})
|
||||||
|
export class HomeWatchlistComponent implements OnDestroy, OnInit {
|
||||||
|
public deviceType: string;
|
||||||
|
public hasPermissionToCreateWatchlistItem: boolean;
|
||||||
|
public user: User;
|
||||||
|
public watchlist: Benchmark[];
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private dataService: DataService,
|
||||||
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private userService: UserService
|
||||||
|
) {
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (params['createWatchlistItemDialog']) {
|
||||||
|
this.openCreateWatchlistItemDialog();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.userService.stateChanged
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((state) => {
|
||||||
|
if (state?.user) {
|
||||||
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToCreateWatchlistItem = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.createWatchlistItem
|
||||||
|
);
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
this.loadWatchlistData();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadWatchlistData() {
|
||||||
|
this.dataService
|
||||||
|
.fetchWatchlist()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ watchlist }) => {
|
||||||
|
this.watchlist = watchlist.map(({ dataSource, symbol }) => ({
|
||||||
|
dataSource,
|
||||||
|
symbol,
|
||||||
|
marketCondition: null,
|
||||||
|
name: symbol,
|
||||||
|
performances: null,
|
||||||
|
trend50d: 'UNKNOWN',
|
||||||
|
trend200d: 'UNKNOWN'
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private openCreateWatchlistItemDialog() {
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(CreateWatchlistItemDialogComponent, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: {
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
locale: this.user?.settings?.locale
|
||||||
|
} as CreateWatchlistItemDialogParams,
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ dataSource, symbol } = {}) => {
|
||||||
|
if (dataSource && symbol) {
|
||||||
|
this.dataService
|
||||||
|
.postWatchlistItem({ dataSource, symbol })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe({
|
||||||
|
next: () => this.loadWatchlistData()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h1 class="d-none d-sm-block h3 mb-4">
|
||||||
|
<span class="align-items-center d-flex justify-content-center">
|
||||||
|
<span i18n>Watchlist</span>
|
||||||
|
@if (user?.subscription?.type === 'Basic') {
|
||||||
|
<gf-premium-indicator class="ml-1" />
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
<div class="col-xs-12 col-md-8 offset-md-2">
|
||||||
|
<gf-benchmark
|
||||||
|
[benchmarks]="watchlist"
|
||||||
|
[deviceType]="deviceType"
|
||||||
|
[locale]="user?.settings?.locale || undefined"
|
||||||
|
[user]="user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if (hasPermissionToCreateWatchlistItem) {
|
||||||
|
<div class="fab-container">
|
||||||
|
<a
|
||||||
|
class="align-items-center d-flex justify-content-center"
|
||||||
|
color="primary"
|
||||||
|
mat-fab
|
||||||
|
[queryParams]="{ createWatchlistItemDialog: true }"
|
||||||
|
[routerLink]="[]"
|
||||||
|
>
|
||||||
|
<ion-icon name="add-outline" size="large" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -2,6 +2,7 @@ import { HomeHoldingsComponent } from '@ghostfolio/client/components/home-holdin
|
|||||||
import { HomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component';
|
import { HomeMarketComponent } from '@ghostfolio/client/components/home-market/home-market.component';
|
||||||
import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component';
|
import { HomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component';
|
||||||
import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component';
|
import { HomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component';
|
||||||
|
import { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
|
||||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
@ -36,6 +37,11 @@ const routes: Routes = [
|
|||||||
path: 'market',
|
path: 'market',
|
||||||
component: HomeMarketComponent,
|
component: HomeMarketComponent,
|
||||||
title: $localize`Markets`
|
title: $localize`Markets`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'watchlist',
|
||||||
|
component: HomeWatchlistComponent,
|
||||||
|
title: $localize`Watchlist`
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
component: HomePageComponent,
|
component: HomePageComponent,
|
||||||
|
@ -52,6 +52,12 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
|||||||
iconName: 'newspaper-outline',
|
iconName: 'newspaper-outline',
|
||||||
label: $localize`Markets`,
|
label: $localize`Markets`,
|
||||||
path: ['/home', 'market']
|
path: ['/home', 'market']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
iconName: 'star-outline',
|
||||||
|
label: $localize`Watchlist`,
|
||||||
|
path: ['/home', 'watchlist'],
|
||||||
|
showCondition: this.user?.settings?.isExperimentalFeatures
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
@ -2,6 +2,7 @@ import { GfHomeHoldingsModule } from '@ghostfolio/client/components/home-holding
|
|||||||
import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module';
|
import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module';
|
||||||
import { GfHomeOverviewModule } from '@ghostfolio/client/components/home-overview/home-overview.module';
|
import { GfHomeOverviewModule } from '@ghostfolio/client/components/home-overview/home-overview.module';
|
||||||
import { GfHomeSummaryModule } from '@ghostfolio/client/components/home-summary/home-summary.module';
|
import { GfHomeSummaryModule } from '@ghostfolio/client/components/home-summary/home-summary.module';
|
||||||
|
import { HomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
|
||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
@ -20,6 +21,7 @@ import { HomePageComponent } from './home-page.component';
|
|||||||
GfHomeOverviewModule,
|
GfHomeOverviewModule,
|
||||||
GfHomeSummaryModule,
|
GfHomeSummaryModule,
|
||||||
HomePageRoutingModule,
|
HomePageRoutingModule,
|
||||||
|
HomeWatchlistComponent,
|
||||||
MatTabsModule,
|
MatTabsModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
|
@ -6,6 +6,7 @@ import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto
|
|||||||
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
|
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
|
||||||
import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
|
import { CreateTagDto } from '@ghostfolio/api/app/endpoints/tags/create-tag.dto';
|
||||||
import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
|
import { UpdateTagDto } from '@ghostfolio/api/app/endpoints/tags/update-tag.dto';
|
||||||
|
import { CreateWatchlistItemDto } from '@ghostfolio/api/app/endpoints/watchlist/create-watchlist-item.dto';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import {
|
import {
|
||||||
Activities,
|
Activities,
|
||||||
@ -44,7 +45,8 @@ import {
|
|||||||
PortfolioPerformanceResponse,
|
PortfolioPerformanceResponse,
|
||||||
PortfolioReportResponse,
|
PortfolioReportResponse,
|
||||||
PublicPortfolioResponse,
|
PublicPortfolioResponse,
|
||||||
User
|
User,
|
||||||
|
WatchlistResponse
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||||
import type {
|
import type {
|
||||||
@ -686,6 +688,10 @@ export class DataService {
|
|||||||
return this.http.get<Tag[]>('/api/v1/tags');
|
return this.http.get<Tag[]>('/api/v1/tags');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchWatchlist() {
|
||||||
|
return this.http.get<WatchlistResponse>('/api/v1/watchlist');
|
||||||
|
}
|
||||||
|
|
||||||
public generateAccessToken(aUserId: string) {
|
public generateAccessToken(aUserId: string) {
|
||||||
return this.http.post<AccessTokenResponse>(
|
return this.http.post<AccessTokenResponse>(
|
||||||
`/api/v1/user/${aUserId}/access-token`,
|
`/api/v1/user/${aUserId}/access-token`,
|
||||||
@ -748,6 +754,10 @@ export class DataService {
|
|||||||
return this.http.post<UserItem>('/api/v1/user', {});
|
return this.http.post<UserItem>('/api/v1/user', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public postWatchlistItem(watchlistItem: CreateWatchlistItemDto) {
|
||||||
|
return this.http.post('/api/v1/watchlist', watchlistItem);
|
||||||
|
}
|
||||||
|
|
||||||
public putAccount(aAccount: UpdateAccountDto) {
|
public putAccount(aAccount: UpdateAccountDto) {
|
||||||
return this.http.put<UserItem>(`/api/v1/account/${aAccount.id}`, aAccount);
|
return this.http.put<UserItem>(`/api/v1/account/${aAccount.id}`, aAccount);
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,7 @@ import type { PortfolioPerformanceResponse } from './responses/portfolio-perform
|
|||||||
import type { PortfolioReportResponse } from './responses/portfolio-report.interface';
|
import type { PortfolioReportResponse } from './responses/portfolio-report.interface';
|
||||||
import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface';
|
import type { PublicPortfolioResponse } from './responses/public-portfolio-response.interface';
|
||||||
import type { QuotesResponse } from './responses/quotes-response.interface';
|
import type { QuotesResponse } from './responses/quotes-response.interface';
|
||||||
|
import type { WatchlistResponse } from './responses/watchlist-response.interface';
|
||||||
import type { ScraperConfiguration } from './scraper-configuration.interface';
|
import type { ScraperConfiguration } from './scraper-configuration.interface';
|
||||||
import type { Statistics } from './statistics.interface';
|
import type { Statistics } from './statistics.interface';
|
||||||
import type { SubscriptionOffer } from './subscription-offer.interface';
|
import type { SubscriptionOffer } from './subscription-offer.interface';
|
||||||
@ -135,5 +136,6 @@ export {
|
|||||||
ToggleOption,
|
ToggleOption,
|
||||||
User,
|
User,
|
||||||
UserSettings,
|
UserSettings,
|
||||||
|
WatchlistResponse,
|
||||||
XRayRulesSettings
|
XRayRulesSettings
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces';
|
||||||
|
|
||||||
|
export interface WatchlistResponse {
|
||||||
|
watchlist: AssetProfileIdentifier[];
|
||||||
|
}
|
@ -66,11 +66,13 @@
|
|||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
|
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
<gf-value
|
@if (element?.performances?.allTimeHigh?.date) {
|
||||||
[isDate]="true"
|
<gf-value
|
||||||
[locale]="locale"
|
[isDate]="true"
|
||||||
[value]="element?.performances?.allTimeHigh?.date"
|
[locale]="locale"
|
||||||
/>
|
[value]="element?.performances?.allTimeHigh?.date"
|
||||||
|
/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
@ -83,18 +85,20 @@
|
|||||||
<span class="d-block d-sm-none text-nowrap" i18n>from ATH</span>
|
<span class="d-block d-sm-none text-nowrap" i18n>from ATH</span>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-2 text-right" mat-cell>
|
<td *matCellDef="let element" class="px-2 text-right" mat-cell>
|
||||||
<gf-value
|
@if (isNumber(element?.performances?.allTimeHigh?.performancePercent)) {
|
||||||
class="d-inline-block justify-content-end"
|
<gf-value
|
||||||
[isPercent]="true"
|
class="d-inline-block justify-content-end"
|
||||||
[locale]="locale"
|
[isPercent]="true"
|
||||||
[ngClass]="{
|
[locale]="locale"
|
||||||
'text-danger':
|
[ngClass]="{
|
||||||
element?.performances?.allTimeHigh?.performancePercent < 0,
|
'text-danger':
|
||||||
'text-success':
|
element?.performances?.allTimeHigh?.performancePercent < 0,
|
||||||
element?.performances?.allTimeHigh?.performancePercent === 0
|
'text-success':
|
||||||
}"
|
element?.performances?.allTimeHigh?.performancePercent === 0
|
||||||
[value]="element?.performances?.allTimeHigh?.performancePercent"
|
}"
|
||||||
/>
|
[value]="element?.performances?.allTimeHigh?.performancePercent"
|
||||||
|
/>
|
||||||
|
}
|
||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
|
import { isNumber } from 'lodash';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
import { Subject, takeUntil } from 'rxjs';
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
|
|||||||
|
|
||||||
public displayedColumns = ['name', 'date', 'change', 'marketCondition'];
|
public displayedColumns = ['name', 'date', 'change', 'marketCondition'];
|
||||||
public isLoading = true;
|
public isLoading = true;
|
||||||
|
public isNumber = isNumber;
|
||||||
public resolveMarketCondition = resolveMarketCondition;
|
public resolveMarketCondition = resolveMarketCondition;
|
||||||
public translate = translate;
|
public translate = translate;
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user