Feature/add account detail dialog (#1047)
* Add account detail dialog * Update changelog
This commit is contained in:
parent
8e2b235b1f
commit
c0f19d56ec
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added an account detail dialog
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Improved the label of the (symbol) search
|
- Improved the label of the (symbol) search
|
||||||
|
@ -7,7 +7,10 @@ import {
|
|||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
import { Accounts } from '@ghostfolio/common/interfaces';
|
import { Accounts } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type {
|
||||||
|
AccountWithValue,
|
||||||
|
RequestWithUser
|
||||||
|
} from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
@ -123,13 +126,45 @@ export class AccountController {
|
|||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getAccountById(@Param('id') id: string): Promise<AccountModel> {
|
public async getAccountById(
|
||||||
return this.accountService.account({
|
@Headers('impersonation-id') impersonationId,
|
||||||
id_userId: {
|
@Param('id') id: string
|
||||||
id,
|
): Promise<AccountWithValue> {
|
||||||
userId: this.request.user.id
|
const impersonationUserId =
|
||||||
}
|
await this.impersonationService.validateImpersonationId(
|
||||||
});
|
impersonationId,
|
||||||
|
this.request.user.id
|
||||||
|
);
|
||||||
|
|
||||||
|
let accountsWithAggregations =
|
||||||
|
await this.portfolioService.getAccountsWithAggregations(
|
||||||
|
impersonationUserId || this.request.user.id,
|
||||||
|
[{ id, type: 'ACCOUNT' }]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
impersonationUserId ||
|
||||||
|
this.userService.isRestrictedView(this.request.user)
|
||||||
|
) {
|
||||||
|
accountsWithAggregations = {
|
||||||
|
...nullifyValuesInObject(accountsWithAggregations, [
|
||||||
|
'totalBalanceInBaseCurrency',
|
||||||
|
'totalValueInBaseCurrency'
|
||||||
|
]),
|
||||||
|
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
|
||||||
|
'balance',
|
||||||
|
'balanceInBaseCurrency',
|
||||||
|
'convertedBalance',
|
||||||
|
'fee',
|
||||||
|
'quantity',
|
||||||
|
'unitPrice',
|
||||||
|
'value',
|
||||||
|
'valueInBaseCurrency'
|
||||||
|
])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return accountsWithAggregations.accounts[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
|
@ -4,6 +4,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
|
|||||||
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
|
||||||
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
|
||||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||||
|
import { Filter } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
@ -17,6 +18,7 @@ import {
|
|||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
|
Query,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
UseInterceptors
|
UseInterceptors
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@ -66,8 +68,36 @@ export class OrderController {
|
|||||||
@UseInterceptors(RedactValuesInResponseInterceptor)
|
@UseInterceptors(RedactValuesInResponseInterceptor)
|
||||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||||
public async getAllOrders(
|
public async getAllOrders(
|
||||||
@Headers('impersonation-id') impersonationId
|
@Headers('impersonation-id') impersonationId,
|
||||||
|
@Query('accounts') filterByAccounts?: string,
|
||||||
|
@Query('assetClasses') filterByAssetClasses?: string,
|
||||||
|
@Query('tags') filterByTags?: string
|
||||||
): Promise<Activities> {
|
): Promise<Activities> {
|
||||||
|
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||||
|
const assetClasses = filterByAssetClasses?.split(',') ?? [];
|
||||||
|
const tagIds = filterByTags?.split(',') ?? [];
|
||||||
|
|
||||||
|
const filters: Filter[] = [
|
||||||
|
...accountIds.map((accountId) => {
|
||||||
|
return <Filter>{
|
||||||
|
id: accountId,
|
||||||
|
type: 'ACCOUNT'
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
...assetClasses.map((assetClass) => {
|
||||||
|
return <Filter>{
|
||||||
|
id: assetClass,
|
||||||
|
type: 'ASSET_CLASS'
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
...tagIds.map((tagId) => {
|
||||||
|
return <Filter>{
|
||||||
|
id: tagId,
|
||||||
|
type: 'TAG'
|
||||||
|
};
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
const impersonationUserId =
|
const impersonationUserId =
|
||||||
await this.impersonationService.validateImpersonationId(
|
await this.impersonationService.validateImpersonationId(
|
||||||
impersonationId,
|
impersonationId,
|
||||||
@ -76,6 +106,7 @@ export class OrderController {
|
|||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
|
|
||||||
let activities = await this.orderService.getOrders({
|
let activities = await this.orderService.getOrders({
|
||||||
|
filters,
|
||||||
userCurrency,
|
userCurrency,
|
||||||
includeDrafts: true,
|
includeDrafts: true,
|
||||||
userId: impersonationUserId || this.request.user.id
|
userId: impersonationUserId || this.request.user.id
|
||||||
|
@ -50,6 +50,7 @@ import { REQUEST } from '@nestjs/core';
|
|||||||
import {
|
import {
|
||||||
AssetClass,
|
AssetClass,
|
||||||
DataSource,
|
DataSource,
|
||||||
|
Prisma,
|
||||||
Tag,
|
Tag,
|
||||||
Type as TypeOfOrder
|
Type as TypeOfOrder
|
||||||
} from '@prisma/client';
|
} from '@prisma/client';
|
||||||
@ -100,14 +101,23 @@ export class PortfolioService {
|
|||||||
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
|
public async getAccounts(
|
||||||
|
aUserId: string,
|
||||||
|
aFilters?: Filter[]
|
||||||
|
): Promise<AccountWithValue[]> {
|
||||||
|
const where: Prisma.AccountWhereInput = { userId: aUserId };
|
||||||
|
|
||||||
|
if (aFilters?.[0].id && aFilters?.[0].type === 'ACCOUNT') {
|
||||||
|
where.id = aFilters[0].id;
|
||||||
|
}
|
||||||
|
|
||||||
const [accounts, details] = await Promise.all([
|
const [accounts, details] = await Promise.all([
|
||||||
this.accountService.accounts({
|
this.accountService.accounts({
|
||||||
|
where,
|
||||||
include: { Order: true, Platform: true },
|
include: { Order: true, Platform: true },
|
||||||
orderBy: { name: 'asc' },
|
orderBy: { name: 'asc' }
|
||||||
where: { userId: aUserId }
|
|
||||||
}),
|
}),
|
||||||
this.getDetails(aUserId, aUserId)
|
this.getDetails(aUserId, aUserId, undefined, aFilters)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
@ -145,8 +155,11 @@ export class PortfolioService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
|
public async getAccountsWithAggregations(
|
||||||
const accounts = await this.getAccounts(aUserId);
|
aUserId: string,
|
||||||
|
aFilters?: Filter[]
|
||||||
|
): Promise<Accounts> {
|
||||||
|
const accounts = await this.getAccounts(aUserId, aFilters);
|
||||||
let totalBalanceInBaseCurrency = new Big(0);
|
let totalBalanceInBaseCurrency = new Big(0);
|
||||||
let totalValueInBaseCurrency = new Big(0);
|
let totalValueInBaseCurrency = new Big(0);
|
||||||
let transactionCount = 0;
|
let transactionCount = 0;
|
||||||
@ -1290,6 +1303,10 @@ export class PortfolioService {
|
|||||||
|
|
||||||
if (filters.length === 0) {
|
if (filters.length === 0) {
|
||||||
currentAccounts = await this.accountService.getAccounts(userId);
|
currentAccounts = await this.accountService.getAccounts(userId);
|
||||||
|
} else if (filters.length === 1 && filters[0].type === 'ACCOUNT') {
|
||||||
|
currentAccounts = await this.accountService.accounts({
|
||||||
|
where: { id: filters[0].id }
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const accountIds = uniq(
|
const accountIds = uniq(
|
||||||
orders.map(({ accountId }) => {
|
orders.map(({ accountId }) => {
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Platform } from '@angular/cdk/platform';
|
import { Platform } from '@angular/cdk/platform';
|
||||||
import { HttpClientModule } from '@angular/common/http';
|
import { HttpClientModule } from '@angular/common/http';
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import {
|
import {
|
||||||
DateAdapter,
|
DateAdapter,
|
||||||
MAT_DATE_FORMATS,
|
MAT_DATE_FORMATS,
|
||||||
@ -38,6 +40,8 @@ export function NgxStripeFactory(): string {
|
|||||||
GfHeaderModule,
|
GfHeaderModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
MarkdownModule.forRoot(),
|
MarkdownModule.forRoot(),
|
||||||
|
MatAutocompleteModule,
|
||||||
|
MatChipsModule,
|
||||||
MaterialCssVarsModule.forRoot({
|
MaterialCssVarsModule.forRoot({
|
||||||
darkThemeClass: 'is-dark-theme',
|
darkThemeClass: 'is-dark-theme',
|
||||||
isAutoContrast: true,
|
isAutoContrast: true,
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
@ -0,0 +1,112 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
|
import { AccountType } from '@prisma/client';
|
||||||
|
import { format, parseISO } from 'date-fns';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { AccountDetailDialogParams } from './interfaces/interfaces';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'd-flex flex-column h-100' },
|
||||||
|
selector: 'gf-account-detail-dialog',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: 'account-detail-dialog.html',
|
||||||
|
styleUrls: ['./account-detail-dialog.component.scss']
|
||||||
|
})
|
||||||
|
export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||||
|
public accountType: AccountType;
|
||||||
|
public name: string;
|
||||||
|
public orders: OrderWithAccount[];
|
||||||
|
public platformName: string;
|
||||||
|
public user: User;
|
||||||
|
public valueInBaseCurrency: number;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
|
||||||
|
private dataService: DataService,
|
||||||
|
public dialogRef: MatDialogRef<AccountDetailDialog>,
|
||||||
|
private userService: UserService
|
||||||
|
) {
|
||||||
|
this.userService.stateChanged
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((state) => {
|
||||||
|
if (state?.user) {
|
||||||
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
|
this.dataService
|
||||||
|
.fetchAccount(this.data.accountId)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
|
||||||
|
this.accountType = accountType;
|
||||||
|
this.name = name;
|
||||||
|
this.platformName = Platform?.name;
|
||||||
|
this.valueInBaseCurrency = valueInBaseCurrency;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchActivities({
|
||||||
|
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ activities }) => {
|
||||||
|
this.orders = activities;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public onClose(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onExport() {
|
||||||
|
this.dataService
|
||||||
|
.fetchExport(
|
||||||
|
this.orders.map((order) => {
|
||||||
|
return order.id;
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((data) => {
|
||||||
|
downloadAsFile({
|
||||||
|
content: data,
|
||||||
|
fileName: `ghostfolio-export-${this.name
|
||||||
|
.replace(/\s+/g, '-')
|
||||||
|
.toLowerCase()}-${format(
|
||||||
|
parseISO(data.meta.date),
|
||||||
|
'yyyyMMddHHmm'
|
||||||
|
)}.json`,
|
||||||
|
format: 'json'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,65 @@
|
|||||||
|
<gf-dialog-header
|
||||||
|
mat-dialog-title
|
||||||
|
position="center"
|
||||||
|
[deviceType]="data.deviceType"
|
||||||
|
[title]="name"
|
||||||
|
(closeButtonClicked)="onClose()"
|
||||||
|
></gf-dialog-header>
|
||||||
|
|
||||||
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
|
<div class="container p-0">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12 d-flex justify-content-center mb-3">
|
||||||
|
<gf-value
|
||||||
|
size="large"
|
||||||
|
[currency]="user?.settings?.baseCurrency"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[value]="valueInBaseCurrency"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<gf-value
|
||||||
|
label="Account Type"
|
||||||
|
size="medium"
|
||||||
|
[value]="accountType"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<gf-value
|
||||||
|
label="Platform"
|
||||||
|
size="medium"
|
||||||
|
[value]="platformName"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="orders?.length > 0" class="row">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<div class="h5 mb-0" i18n>Activities</div>
|
||||||
|
<gf-activities-table
|
||||||
|
[activities]="orders"
|
||||||
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
|
[deviceType]="data.deviceType"
|
||||||
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||||
|
[hasPermissionToFilter]="false"
|
||||||
|
[hasPermissionToImportActivities]="false"
|
||||||
|
[hasPermissionToOpenDetails]="false"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[showActions]="false"
|
||||||
|
[showSymbolColumn]="false"
|
||||||
|
(export)="onExport()"
|
||||||
|
></gf-activities-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<gf-dialog-footer
|
||||||
|
mat-dialog-actions
|
||||||
|
[deviceType]="data.deviceType"
|
||||||
|
(closeButtonClicked)="onClose()"
|
||||||
|
></gf-dialog-footer>
|
@ -0,0 +1,29 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
|
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { AccountDetailDialog } from './account-detail-dialog.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [AccountDetailDialog],
|
||||||
|
exports: [],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
GfActivitiesTableModule,
|
||||||
|
GfDialogFooterModule,
|
||||||
|
GfDialogHeaderModule,
|
||||||
|
GfValueModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatDialogModule,
|
||||||
|
NgxSkeletonLoaderModule
|
||||||
|
],
|
||||||
|
providers: [],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfAccountDetailDialogModule {}
|
@ -0,0 +1,5 @@
|
|||||||
|
export interface AccountDetailDialogParams {
|
||||||
|
accountId: string;
|
||||||
|
deviceType: string;
|
||||||
|
hasImpersonationId: boolean;
|
||||||
|
}
|
@ -65,7 +65,7 @@
|
|||||||
<ng-container matColumnDef="transactions">
|
<ng-container matColumnDef="transactions">
|
||||||
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
|
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
|
||||||
<span class="d-block d-sm-none">#</span>
|
<span class="d-block d-sm-none">#</span>
|
||||||
<span class="d-none d-sm-block" i18n>Transactions</span>
|
<span class="d-none d-sm-block" i18n>Activities</span>
|
||||||
</th>
|
</th>
|
||||||
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
|
||||||
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
|
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
|
||||||
@ -212,7 +212,12 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
|
||||||
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
|
<tr
|
||||||
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
|
class="cursor-pointer"
|
||||||
|
mat-row
|
||||||
|
(click)="onOpenAccountDetailDialog(row.id)"
|
||||||
|
></tr>
|
||||||
<tr
|
<tr
|
||||||
*matFooterRowDef="displayedColumns"
|
*matFooterRowDef="displayedColumns"
|
||||||
mat-footer-row
|
mat-footer-row
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
import { Account as AccountModel } from '@prisma/client';
|
import { Account as AccountModel } from '@prisma/client';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
|
|
||||||
@ -39,7 +40,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor() {}
|
public constructor(private router: Router) {}
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
@ -75,6 +76,12 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onOpenAccountDetailDialog(accountId: string) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { accountId, accountDetailDialog: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public onUpdateAccount(aAccount: AccountModel) {
|
public onUpdateAccount(aAccount: AccountModel) {
|
||||||
this.accountToUpdate.emit(aAccount);
|
this.accountToUpdate.emit(aAccount);
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="align-items-center d-flex mt-4 py-1">
|
<div class="align-items-center d-flex mt-4 py-1">
|
||||||
<div class="pr-1 w-50" i18n>ID</div>
|
<div class="pr-1 w-50" i18n>User ID</div>
|
||||||
<div class="pl-1 w-50">{{ user?.id }}</div>
|
<div class="pl-1 w-50">{{ user?.id }}</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
@ -3,6 +3,8 @@ import { MatDialog } from '@angular/material/dialog';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
|
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
||||||
|
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -48,12 +50,17 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.route.queryParams
|
this.route.queryParams
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (params['createDialog'] && this.hasPermissionToCreateAccount) {
|
if (params['accountId'] && params['accountDetailDialog']) {
|
||||||
|
this.openAccountDetailDialog(params['accountId']);
|
||||||
|
} else if (
|
||||||
|
params['createDialog'] &&
|
||||||
|
this.hasPermissionToCreateAccount
|
||||||
|
) {
|
||||||
this.openCreateAccountDialog();
|
this.openCreateAccountDialog();
|
||||||
} else if (params['editDialog']) {
|
} else if (params['editDialog']) {
|
||||||
if (this.accounts) {
|
if (this.accounts) {
|
||||||
const account = this.accounts.find((account) => {
|
const account = this.accounts.find((account) => {
|
||||||
return account.id === params['transactionId'];
|
return account.id === params['accountId'];
|
||||||
});
|
});
|
||||||
|
|
||||||
this.openUpdateAccountDialog(account);
|
this.openUpdateAccountDialog(account);
|
||||||
@ -139,7 +146,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
public onUpdateAccount(aAccount: AccountModel) {
|
public onUpdateAccount(aAccount: AccountModel) {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { editDialog: true, transactionId: aAccount.id }
|
queryParams: { accountId: aAccount.id, editDialog: true }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,6 +204,26 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openAccountDetailDialog(aAccountId: string) {
|
||||||
|
const dialogRef = this.dialog.open(AccountDetailDialog, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: <AccountDetailDialogParams>{
|
||||||
|
accountId: aAccountId,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
hasImpersonationId: this.hasImpersonationId
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private openCreateAccountDialog(): void {
|
private openCreateAccountDialog(): void {
|
||||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||||
data: {
|
data: {
|
||||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
|
||||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||||
|
|
||||||
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
|
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
|
||||||
@ -10,16 +11,15 @@ import { GfCreateOrUpdateAccountDialogModule } from './create-or-update-account-
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [AccountsPageComponent],
|
declarations: [AccountsPageComponent],
|
||||||
exports: [],
|
|
||||||
imports: [
|
imports: [
|
||||||
AccountsPageRoutingModule,
|
AccountsPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfAccountDetailDialogModule,
|
||||||
GfAccountsTableModule,
|
GfAccountsTableModule,
|
||||||
GfCreateOrUpdateAccountDialogModule,
|
GfCreateOrUpdateAccountDialogModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
providers: [],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class AccountsPageModule {}
|
export class AccountsPageModule {}
|
||||||
|
@ -50,6 +50,17 @@
|
|||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div *ngIf="data.account.id">
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Account ID</mat-label>
|
||||||
|
<input
|
||||||
|
disabled
|
||||||
|
matInput
|
||||||
|
name="accountId"
|
||||||
|
[(ngModel)]="data.account.id"
|
||||||
|
/>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="justify-content-end" mat-dialog-actions>
|
<div class="justify-content-end" mat-dialog-actions>
|
||||||
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
<button i18n mat-button (click)="onCancel()">Cancel</button>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
||||||
|
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
||||||
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
|
||||||
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
@ -99,7 +101,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.routeQueryParams = route.queryParams
|
this.routeQueryParams = route.queryParams
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (
|
if (params['accountId'] && params['accountDetailDialog']) {
|
||||||
|
this.openAccountDetailDialog(params['accountId']);
|
||||||
|
} else if (
|
||||||
params['dataSource'] &&
|
params['dataSource'] &&
|
||||||
params['positionDetailDialog'] &&
|
params['positionDetailDialog'] &&
|
||||||
params['symbol']
|
params['symbol']
|
||||||
@ -379,13 +383,21 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.markets.otherMarkets.value / marketsTotal;
|
this.markets.otherMarkets.value / marketsTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onAccountChartClicked({ symbol }: UniqueAsset) {
|
||||||
|
if (symbol) {
|
||||||
|
this.router.navigate([], {
|
||||||
|
queryParams: { accountId: symbol, accountDetailDialog: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onChangePeriod(aValue: string) {
|
public onChangePeriod(aValue: string) {
|
||||||
this.period = aValue;
|
this.period = aValue;
|
||||||
|
|
||||||
this.initializeAnalysisData(this.period);
|
this.initializeAnalysisData(this.period);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onProportionChartClicked({ dataSource, symbol }: UniqueAsset) {
|
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
|
||||||
if (dataSource && symbol) {
|
if (dataSource && symbol) {
|
||||||
this.router.navigate([], {
|
this.router.navigate([], {
|
||||||
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
queryParams: { dataSource, symbol, positionDetailDialog: true }
|
||||||
@ -398,6 +410,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openAccountDetailDialog(aAccountId: string) {
|
||||||
|
const dialogRef = this.dialog.open(AccountDetailDialog, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: <AccountDetailDialogParams>{
|
||||||
|
accountId: aAccountId,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
hasImpersonationId: this.hasImpersonationId
|
||||||
|
},
|
||||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
|
||||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef
|
||||||
|
.afterClosed()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private openPositionDialog({
|
private openPositionDialog({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
|
@ -24,11 +24,13 @@
|
|||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-portfolio-proportion-chart
|
<gf-portfolio-proportion-chart
|
||||||
|
cursor="pointer"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[keys]="['id']"
|
[keys]="['id']"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="accounts"
|
[positions]="accounts"
|
||||||
|
(proportionChartClicked)="onAccountChartClicked($event)"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -116,7 +118,7 @@
|
|||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="symbols"
|
[positions]="symbols"
|
||||||
[showLabels]="deviceType !== 'mobile'"
|
[showLabels]="deviceType !== 'mobile'"
|
||||||
(proportionChartClicked)="onProportionChartClicked($event)"
|
(proportionChartClicked)="onSymbolChartClicked($event)"
|
||||||
></gf-portfolio-proportion-chart>
|
></gf-portfolio-proportion-chart>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -111,12 +111,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.fetchOrders();
|
this.fetchActivities();
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchOrders() {
|
public fetchActivities() {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchOrders()
|
.fetchActivities({})
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(({ activities }) => {
|
.subscribe(({ activities }) => {
|
||||||
this.activities = activities;
|
this.activities = activities;
|
||||||
@ -139,7 +139,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetchOrders();
|
this.fetchActivities();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -298,7 +298,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetchOrders();
|
this.fetchActivities();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -332,7 +332,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleImportSuccess() {
|
private handleImportSuccess() {
|
||||||
this.fetchOrders();
|
this.fetchActivities();
|
||||||
|
|
||||||
this.snackBar.open('✅ Import has been completed', undefined, {
|
this.snackBar.open('✅ Import has been completed', undefined, {
|
||||||
duration: 3000
|
duration: 3000
|
||||||
@ -376,7 +376,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.dataService.postOrder(transaction).subscribe({
|
this.dataService.postOrder(transaction).subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.fetchOrders();
|
this.fetchActivities();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { cloneDeep, groupBy } from 'lodash';
|
import { cloneDeep, groupBy } from 'lodash';
|
||||||
@ -59,10 +59,75 @@ export class DataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchAccount(aAccountId: string) {
|
||||||
|
return this.http.get<AccountWithValue>(`/api/v1/account/${aAccountId}`);
|
||||||
|
}
|
||||||
|
|
||||||
public fetchAccounts() {
|
public fetchAccounts() {
|
||||||
return this.http.get<Accounts>('/api/v1/account');
|
return this.http.get<Accounts>('/api/v1/account');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchActivities({
|
||||||
|
filters
|
||||||
|
}: {
|
||||||
|
filters?: Filter[];
|
||||||
|
}): Observable<Activities> {
|
||||||
|
let params = new HttpParams();
|
||||||
|
|
||||||
|
if (filters?.length > 0) {
|
||||||
|
const {
|
||||||
|
ACCOUNT: filtersByAccount,
|
||||||
|
ASSET_CLASS: filtersByAssetClass,
|
||||||
|
TAG: filtersByTag
|
||||||
|
} = groupBy(filters, (filter) => {
|
||||||
|
return filter.type;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filtersByAccount) {
|
||||||
|
params = params.append(
|
||||||
|
'accounts',
|
||||||
|
filtersByAccount
|
||||||
|
.map(({ id }) => {
|
||||||
|
return id;
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtersByAssetClass) {
|
||||||
|
params = params.append(
|
||||||
|
'assetClasses',
|
||||||
|
filtersByAssetClass
|
||||||
|
.map(({ id }) => {
|
||||||
|
return id;
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filtersByTag) {
|
||||||
|
params = params.append(
|
||||||
|
'tags',
|
||||||
|
filtersByTag
|
||||||
|
.map(({ id }) => {
|
||||||
|
return id;
|
||||||
|
})
|
||||||
|
.join(',')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.http.get<any>('/api/v1/order', { params }).pipe(
|
||||||
|
map(({ activities }) => {
|
||||||
|
for (const activity of activities) {
|
||||||
|
activity.createdAt = parseISO(activity.createdAt);
|
||||||
|
activity.date = parseISO(activity.date);
|
||||||
|
}
|
||||||
|
return { activities };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public fetchAdminData() {
|
public fetchAdminData() {
|
||||||
return this.http.get<AdminData>('/api/v1/admin');
|
return this.http.get<AdminData>('/api/v1/admin');
|
||||||
}
|
}
|
||||||
@ -179,18 +244,6 @@ export class DataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchOrders(): Observable<Activities> {
|
|
||||||
return this.http.get<any>('/api/v1/order').pipe(
|
|
||||||
map(({ activities }) => {
|
|
||||||
for (const activity of activities) {
|
|
||||||
activity.createdAt = parseISO(activity.createdAt);
|
|
||||||
activity.date = parseISO(activity.date);
|
|
||||||
}
|
|
||||||
return { activities };
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
|
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { Account as AccountModel } from '@prisma/client';
|
import { Account as AccountModel, Platform } from '@prisma/client';
|
||||||
|
|
||||||
export type AccountWithValue = AccountModel & {
|
export type AccountWithValue = AccountModel & {
|
||||||
balanceInBaseCurrency: number;
|
balanceInBaseCurrency: number;
|
||||||
|
Platform?: Platform;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
value: number;
|
value: number;
|
||||||
valueInBaseCurrency: number;
|
valueInBaseCurrency: number;
|
||||||
|
@ -276,12 +276,14 @@ export class PortfolioProportionChartComponent
|
|||||||
padding: this.showLabels === true ? 100 : 0
|
padding: this.showLabels === true ? 100 : 0
|
||||||
},
|
},
|
||||||
onClick: (event, activeElements) => {
|
onClick: (event, activeElements) => {
|
||||||
const dataIndex = activeElements[0].index;
|
try {
|
||||||
const symbol: string = event.chart.data.labels[dataIndex];
|
const dataIndex = activeElements[0].index;
|
||||||
|
const symbol: string = event.chart.data.labels[dataIndex];
|
||||||
|
|
||||||
const dataSource = this.positions[symbol]?.dataSource;
|
const dataSource = this.positions[symbol]?.dataSource;
|
||||||
|
|
||||||
this.proportionChartClicked.emit({ dataSource, symbol });
|
this.proportionChartClicked.emit({ dataSource, symbol });
|
||||||
|
} catch {}
|
||||||
},
|
},
|
||||||
onHover: (event, chartElement) => {
|
onHover: (event, chartElement) => {
|
||||||
if (this.cursor) {
|
if (this.cursor) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user