Compare commits
54 Commits
Author | SHA1 | Date | |
---|---|---|---|
0953c072fe | |||
d152187ee8 | |||
3c5affce88 | |||
f27e21f9a0 | |||
337ca328c3 | |||
beb9e2c43f | |||
4d79df90a7 | |||
aa72d9b730 | |||
80e899a5d3 | |||
7c33120546 | |||
7f3c86038f | |||
c1446f8559 | |||
88d5dfe435 | |||
7dc8f80fdf | |||
96f90c7259 | |||
a10d9cb6ba | |||
4547c5da1d | |||
28706d7b26 | |||
492bc5e17b | |||
6c37737051 | |||
8677d20c2c | |||
4d905065ad | |||
5599b41b83 | |||
8d5a60d777 | |||
695acf4f3f | |||
67dbef3b7a | |||
0e94112dc7 | |||
b22edff16b | |||
ffb7cbff50 | |||
25424ad280 | |||
a768902b00 | |||
2c7ece50fe | |||
51a0ede3e4 | |||
531964636b | |||
e461fff1d7 | |||
4f9a5f0340 | |||
8d80e840b8 | |||
833982a9de | |||
c85966e5ed | |||
43f67ba832 | |||
cbea8ac9d3 | |||
d4c939e41d | |||
c1f129501a | |||
377ba75e4c | |||
77b13b88f0 | |||
813e73a0a3 | |||
1d796a9597 | |||
4eedf64a3c | |||
ed4dd79c72 | |||
6f4fd0826c | |||
8e3a144a37 | |||
07b0a2c40a | |||
c5dc3d4272 | |||
73e69273b4 |
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -6,7 +6,13 @@ labels: ''
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
The Issue tracker is **ONLY** used for reporting bugs. New features should be discussed in our [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) community or in [Discussions](https://github.com/ghostfolio/ghostfolio/discussions).
|
||||
**Important Notice**
|
||||
|
||||
The issue tracker is **ONLY** used for reporting bugs. New features should be discussed in our [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) community or in [Discussions](https://github.com/ghostfolio/ghostfolio/discussions).
|
||||
|
||||
Incomplete or non-reproducible issues may be closed, but we are here to help! If you encounter difficulties reproducing the bug or need assistance, please reach out to our community channels mentioned above.
|
||||
|
||||
Thank you for your understanding and cooperation!
|
||||
|
||||
**Bug Description**
|
||||
|
||||
@ -36,8 +42,9 @@ The Issue tracker is **ONLY** used for reporting bugs. New features should be di
|
||||
|
||||
<!-- Please complete the following information -->
|
||||
|
||||
- Cloud or Self-hosted
|
||||
- Ghostfolio Version X.Y.Z
|
||||
- Cloud or Self-hosted
|
||||
- Experimental Features enabled or disabled
|
||||
- Browser
|
||||
- OS
|
||||
|
||||
|
98
CHANGELOG.md
98
CHANGELOG.md
@ -5,7 +5,101 @@ 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/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 2.27.0 - 2023-11-24
|
||||
## 2.32.0 - 2023-12-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added support to search for an asset profile by `id` as an administrator
|
||||
|
||||
### Changed
|
||||
|
||||
- Set the select column of the lazy-loaded activities table to stick at the end (experimental)
|
||||
- Dropped the activity id in the activities import
|
||||
- Improved the validation of the currency management in the admin control panel
|
||||
- Improved the performance of the value redaction interceptor for the impersonation mode by eliminating `cloneDeep`
|
||||
- Modernized the `Nx` executors
|
||||
- `@nx/eslint:lint`
|
||||
- `@nx/webpack:webpack`
|
||||
- Upgraded `prettier` from version `3.1.0` to `3.1.1`
|
||||
- Upgraded `prisma` from version `5.7.0` to `5.7.1`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reset the letter spacing in buttons
|
||||
|
||||
## 2.31.0 - 2023-12-16
|
||||
|
||||
### Changed
|
||||
|
||||
- Introduced the lazy-loaded activities table to the account detail dialog (experimental)
|
||||
- Introduced the lazy-loaded activities table to the import activities dialog (experimental)
|
||||
- Introduced the lazy-loaded activities table to the position detail dialog (experimental)
|
||||
- Improved the font weight in the value component
|
||||
- Improved the language localization for Türkçe (`tr`)
|
||||
- Upgraded `angular` from version `17.0.4` to `17.0.7`
|
||||
- Upgraded to _Inter_ 4 font family
|
||||
- Upgraded `Nx` from version `17.0.2` to `17.2.5`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the loading state in the lazy-loaded activities table on the portfolio activities page (experimental)
|
||||
- Fixed the edit of activity in the lazy-loaded activities table on the portfolio activities page (experimental)
|
||||
|
||||
## 2.30.0 - 2023-12-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for column sorting to the lazy-loaded activities table on the portfolio activities page (experimental)
|
||||
- Extended the benchmarks of the markets overview by the current market condition (all time high)
|
||||
|
||||
### Changed
|
||||
|
||||
- Adjusted the threshold to skip the data enhancement (_Trackinsight_) if data is inaccurate
|
||||
- Upgraded `prisma` from version `5.6.0` to `5.7.0`
|
||||
|
||||
## 2.29.0 - 2023-12-09
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced a lazy-loaded activities table on the portfolio activities page (experimental)
|
||||
|
||||
### Changed
|
||||
|
||||
- Set the actions columns of various tables to stick at the end
|
||||
- Increased the height of the tabs on mobile
|
||||
- Improved the language localization for German (`de`)
|
||||
- Improved the language localization for Türkçe (`tr`)
|
||||
- Upgraded `marked` from version `4.2.12` to `9.1.6`
|
||||
- Upgraded `ngx-markdown` from version `15.1.0` to `17.1.1`
|
||||
- Upgraded `ng-extract-i18n-merge` from version `2.8.3` to `2.9.0`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the biometric authentication registration
|
||||
|
||||
## 2.28.0 - 2023-12-02
|
||||
|
||||
### Added
|
||||
|
||||
- Added a historical cash balances table to the account detail dialog
|
||||
- Introduced a `HasPermission` annotation for endpoints
|
||||
|
||||
### Changed
|
||||
|
||||
- Relaxed the check for duplicates in the preview step of the activities import (allow same day)
|
||||
- Respected the `withExcludedAccounts` flag in the account balance time series
|
||||
|
||||
### Fixed
|
||||
|
||||
- Changed the mechanism of the `INTRADAY` data gathering to operate synchronously avoiding database deadlocks
|
||||
|
||||
## 2.27.1 - 2023-11-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Reverted `Nx` from version `17.1.3` to `17.0.2`
|
||||
|
||||
## 2.27.0 - 2023-11-26
|
||||
|
||||
### Changed
|
||||
|
||||
@ -149,7 +243,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the check for duplicates in the preview step of the activities import (allow different accounts)
|
||||
- Relaxed the check for duplicates in the preview step of the activities import (allow different accounts)
|
||||
- Improved the usability and validation in the cash balance transfer from one to another account
|
||||
- Changed the checkboxes to slide toggles in the overview of the admin control panel
|
||||
- Switched from the deprecated (`PUT`) to the new endpoint (`POST`) to manage historical market data in the asset profile details dialog of the admin control panel
|
||||
|
@ -7,14 +7,15 @@
|
||||
"generators": {},
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nrwl/webpack:webpack",
|
||||
"executor": "@nx/webpack:webpack",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/api",
|
||||
"main": "apps/api/src/main.ts",
|
||||
"tsConfig": "apps/api/tsconfig.app.json",
|
||||
"assets": ["apps/api/src/assets"],
|
||||
"target": "node",
|
||||
"compiler": "tsc"
|
||||
"compiler": "tsc",
|
||||
"webpackConfig": "apps/api/webpack.config.js"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@ -39,7 +40,7 @@
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"executor": "@nx/eslint:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["apps/api/**/*.ts"]
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { Access as AccessModel } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { AccessModule } from './access.module';
|
||||
import { AccessService } from './access.service';
|
||||
import { CreateAccessDto } from './create-access.dto';
|
||||
|
||||
@ -83,7 +82,7 @@ export class AccessController {
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async deleteAccess(@Param('id') id: string): Promise<AccessModule> {
|
||||
public async deleteAccess(@Param('id') id: string): Promise<AccessModel> {
|
||||
const access = await this.accessService.access({ id });
|
||||
|
||||
if (
|
||||
|
@ -0,0 +1,52 @@
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Delete,
|
||||
HttpException,
|
||||
Inject,
|
||||
Param,
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { AccountBalance } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { AccountBalanceService } from './account-balance.service';
|
||||
|
||||
@Controller('account-balance')
|
||||
export class AccountBalanceController {
|
||||
public constructor(
|
||||
private readonly accountBalanceService: AccountBalanceService,
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
|
||||
@Delete(':id')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async deleteAccountBalance(
|
||||
@Param('id') id: string
|
||||
): Promise<AccountBalance> {
|
||||
const accountBalance = await this.accountBalanceService.accountBalance({
|
||||
id
|
||||
});
|
||||
|
||||
if (
|
||||
!hasPermission(
|
||||
this.request.user.permissions,
|
||||
permissions.deleteAccountBalance
|
||||
) ||
|
||||
!accountBalance ||
|
||||
accountBalance.userId !== this.request.user.id
|
||||
) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return this.accountBalanceService.deleteAccountBalance({
|
||||
id
|
||||
});
|
||||
}
|
||||
}
|
@ -1,9 +1,12 @@
|
||||
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||
import { PrismaModule } from '@ghostfolio/api/services/prisma/prisma.module';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { AccountBalanceController } from './account-balance.controller';
|
||||
import { AccountBalanceService } from './account-balance.service';
|
||||
|
||||
@Module({
|
||||
controllers: [AccountBalanceController],
|
||||
exports: [AccountBalanceService],
|
||||
imports: [ExchangeRateDataModule, PrismaModule],
|
||||
providers: [AccountBalanceService]
|
@ -12,6 +12,17 @@ export class AccountBalanceService {
|
||||
private readonly prismaService: PrismaService
|
||||
) {}
|
||||
|
||||
public async accountBalance(
|
||||
accountBalanceWhereInput: Prisma.AccountBalanceWhereInput
|
||||
): Promise<AccountBalance | null> {
|
||||
return this.prismaService.accountBalance.findFirst({
|
||||
include: {
|
||||
Account: true
|
||||
},
|
||||
where: accountBalanceWhereInput
|
||||
});
|
||||
}
|
||||
|
||||
public async createAccountBalance(
|
||||
data: Prisma.AccountBalanceCreateInput
|
||||
): Promise<AccountBalance> {
|
||||
@ -20,12 +31,22 @@ export class AccountBalanceService {
|
||||
});
|
||||
}
|
||||
|
||||
public async deleteAccountBalance(
|
||||
where: Prisma.AccountBalanceWhereUniqueInput
|
||||
): Promise<AccountBalance> {
|
||||
return this.prismaService.accountBalance.delete({
|
||||
where
|
||||
});
|
||||
}
|
||||
|
||||
public async getAccountBalances({
|
||||
filters,
|
||||
user
|
||||
user,
|
||||
withExcludedAccounts
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
user: UserWithSettings;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<AccountBalancesResponse> {
|
||||
const where: Prisma.AccountBalanceWhereInput = { userId: user.id };
|
||||
|
||||
@ -37,6 +58,10 @@ export class AccountBalanceService {
|
||||
where.accountId = accountFilter.id;
|
||||
}
|
||||
|
||||
if (withExcludedAccounts === false) {
|
||||
where.Account = { isExcluded: false };
|
||||
}
|
||||
|
||||
const balances = await this.prismaService.accountBalance.findMany({
|
||||
where,
|
||||
orderBy: {
|
@ -1,6 +1,6 @@
|
||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
|
||||
import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response.interceptor';
|
||||
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||
import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config';
|
||||
import {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
|
||||
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { AccountBalanceModule } from '@ghostfolio/api/services/account-balance/account-balance.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
|
||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.module';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { Filter } from '@ghostfolio/common/interfaces';
|
||||
|
@ -176,6 +176,7 @@ export class AdminService {
|
||||
|
||||
if (searchQuery) {
|
||||
where.OR = [
|
||||
{ id: { mode: 'insensitive', startsWith: searchQuery } },
|
||||
{ isin: { mode: 'insensitive', startsWith: searchQuery } },
|
||||
{ name: { mode: 'insensitive', startsWith: searchQuery } },
|
||||
{ symbol: { mode: 'insensitive', startsWith: searchQuery } }
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
calculateBenchmarkTrend
|
||||
} from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Benchmark,
|
||||
BenchmarkMarketDataDetails,
|
||||
BenchmarkProperty,
|
||||
BenchmarkResponse,
|
||||
@ -339,7 +340,15 @@ export class BenchmarkService {
|
||||
};
|
||||
}
|
||||
|
||||
private getMarketCondition(aPerformanceInPercent: number) {
|
||||
return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET';
|
||||
private getMarketCondition(
|
||||
aPerformanceInPercent: number
|
||||
): Benchmark['marketCondition'] {
|
||||
if (aPerformanceInPercent === 0) {
|
||||
return 'ALL_TIME_HIGH';
|
||||
} else if (aPerformanceInPercent <= -0.2) {
|
||||
return 'BEAR_MARKET';
|
||||
} else {
|
||||
return 'NEUTRAL_MARKET';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ export class ExportController {
|
||||
): Promise<Export> {
|
||||
return this.exportService.export({
|
||||
activityIds,
|
||||
userCurrency: this.request.user.Settings.settings.baseCurrency,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
}
|
||||
|
@ -13,9 +13,11 @@ export class ExportService {
|
||||
|
||||
public async export({
|
||||
activityIds,
|
||||
userCurrency,
|
||||
userId
|
||||
}: {
|
||||
activityIds?: string[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<Export> {
|
||||
const accounts = (
|
||||
@ -39,10 +41,13 @@ export class ExportService {
|
||||
}
|
||||
);
|
||||
|
||||
let activities = await this.orderService.orders({
|
||||
include: { SymbolProfile: true },
|
||||
orderBy: { date: 'desc' },
|
||||
where: { userId }
|
||||
let { activities } = await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
includeDrafts: true,
|
||||
sortColumn: 'date',
|
||||
sortDirection: 'asc',
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
if (activityIds) {
|
||||
|
@ -26,7 +26,7 @@ import {
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { endOfToday, format, isAfter, isSameDay, parseISO } from 'date-fns';
|
||||
import { endOfToday, format, isAfter, isSameSecond, parseISO } from 'date-fns';
|
||||
import { uniqBy } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@ -83,12 +83,13 @@ export class ImportService {
|
||||
|
||||
const value = new Big(quantity).mul(marketPrice).toNumber();
|
||||
|
||||
const date = parseDate(dateString);
|
||||
const isDuplicate = orders.some((activity) => {
|
||||
return (
|
||||
activity.accountId === Account?.id &&
|
||||
activity.SymbolProfile.currency === assetProfile.currency &&
|
||||
activity.SymbolProfile.dataSource === assetProfile.dataSource &&
|
||||
isSameDay(activity.date, parseDate(dateString)) &&
|
||||
isSameSecond(activity.date, date) &&
|
||||
activity.quantity === quantity &&
|
||||
activity.SymbolProfile.symbol === assetProfile.symbol &&
|
||||
activity.type === 'DIVIDEND' &&
|
||||
@ -102,6 +103,7 @@ export class ImportService {
|
||||
|
||||
return {
|
||||
Account,
|
||||
date,
|
||||
error,
|
||||
quantity,
|
||||
value,
|
||||
@ -109,7 +111,6 @@ export class ImportService {
|
||||
accountUserId: undefined,
|
||||
comment: undefined,
|
||||
createdAt: undefined,
|
||||
date: parseDate(dateString),
|
||||
fee: 0,
|
||||
feeInBaseCurrency: 0,
|
||||
id: assetProfile.id,
|
||||
@ -235,6 +236,7 @@ export class ImportService {
|
||||
|
||||
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
|
||||
activitiesDto,
|
||||
userCurrency,
|
||||
userId
|
||||
});
|
||||
|
||||
@ -458,15 +460,18 @@ export class ImportService {
|
||||
|
||||
private async extendActivitiesWithErrors({
|
||||
activitiesDto,
|
||||
userCurrency,
|
||||
userId
|
||||
}: {
|
||||
activitiesDto: Partial<CreateOrderDto>[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<Partial<Activity>[]> {
|
||||
const existingActivities = await this.orderService.orders({
|
||||
include: { SymbolProfile: true },
|
||||
orderBy: { date: 'desc' },
|
||||
where: { userId }
|
||||
let { activities: existingActivities } = await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
includeDrafts: true,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
return activitiesDto.map(
|
||||
@ -482,13 +487,13 @@ export class ImportService {
|
||||
type,
|
||||
unitPrice
|
||||
}) => {
|
||||
const date = parseISO(<string>(<unknown>dateString));
|
||||
const date = parseISO(dateString);
|
||||
const isDuplicate = existingActivities.some((activity) => {
|
||||
return (
|
||||
activity.accountId === accountId &&
|
||||
activity.SymbolProfile.currency === currency &&
|
||||
activity.SymbolProfile.dataSource === dataSource &&
|
||||
isSameDay(activity.date, date) &&
|
||||
isSameSecond(activity.date, date) &&
|
||||
activity.fee === fee &&
|
||||
activity.quantity === quantity &&
|
||||
activity.SymbolProfile.symbol === symbol &&
|
||||
|
@ -2,6 +2,7 @@ import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
|
||||
export interface Activities {
|
||||
activities: Activity[];
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface Activity extends OrderWithAccount {
|
||||
|
@ -24,7 +24,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Order as OrderModel } from '@prisma/client';
|
||||
import { Order as OrderModel, Prisma } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
@ -90,6 +90,8 @@ export class OrderController {
|
||||
@Query('accounts') filterByAccounts?: string,
|
||||
@Query('assetClasses') filterByAssetClasses?: string,
|
||||
@Query('skip') skip?: number,
|
||||
@Query('sortColumn') sortColumn?: string,
|
||||
@Query('sortDirection') sortDirection?: Prisma.SortOrder,
|
||||
@Query('tags') filterByTags?: string,
|
||||
@Query('take') take?: number
|
||||
): Promise<Activities> {
|
||||
@ -103,8 +105,10 @@ export class OrderController {
|
||||
await this.impersonationService.validateImpersonationId(impersonationId);
|
||||
const userCurrency = this.request.user.Settings.settings.baseCurrency;
|
||||
|
||||
const activities = await this.orderService.getOrders({
|
||||
const { activities, count } = await this.orderService.getOrders({
|
||||
filters,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
userCurrency,
|
||||
includeDrafts: true,
|
||||
skip: isNaN(skip) ? undefined : skip,
|
||||
@ -113,7 +117,7 @@ export class OrderController {
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
return { activities };
|
||||
return { activities, count };
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
|
||||
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||
|
@ -25,7 +25,7 @@ import { endOfToday, isAfter } from 'date-fns';
|
||||
import { groupBy } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Activity } from './interfaces/activities.interface';
|
||||
import { Activities } from './interfaces/activities.interface';
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
@ -37,34 +37,6 @@ export class OrderService {
|
||||
private readonly symbolProfileService: SymbolProfileService
|
||||
) {}
|
||||
|
||||
public async order(
|
||||
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
|
||||
): Promise<Order | null> {
|
||||
return this.prismaService.order.findUnique({
|
||||
where: orderWhereUniqueInput
|
||||
});
|
||||
}
|
||||
|
||||
public async orders(params: {
|
||||
include?: Prisma.OrderInclude;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
cursor?: Prisma.OrderWhereUniqueInput;
|
||||
where?: Prisma.OrderWhereInput;
|
||||
orderBy?: Prisma.OrderOrderByWithRelationInput;
|
||||
}): Promise<OrderWithAccount[]> {
|
||||
const { include, skip, take, cursor, where, orderBy } = params;
|
||||
|
||||
return this.prismaService.order.findMany({
|
||||
cursor,
|
||||
include,
|
||||
orderBy,
|
||||
skip,
|
||||
take,
|
||||
where
|
||||
});
|
||||
}
|
||||
|
||||
public async createOrder(
|
||||
data: Prisma.OrderCreateInput & {
|
||||
accountId?: string;
|
||||
@ -231,6 +203,8 @@ export class OrderService {
|
||||
filters,
|
||||
includeDrafts = false,
|
||||
skip,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
take = Number.MAX_SAFE_INTEGER,
|
||||
types,
|
||||
userCurrency,
|
||||
@ -240,12 +214,17 @@ export class OrderService {
|
||||
filters?: Filter[];
|
||||
includeDrafts?: boolean;
|
||||
skip?: number;
|
||||
sortColumn?: string;
|
||||
sortDirection?: Prisma.SortOrder;
|
||||
take?: number;
|
||||
types?: TypeOfOrder[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<Activity[]> {
|
||||
}): Promise<Activities> {
|
||||
let orderBy: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput> = [
|
||||
{ date: 'asc' }
|
||||
];
|
||||
const where: Prisma.OrderWhereInput = { userId };
|
||||
|
||||
const {
|
||||
@ -307,6 +286,10 @@ export class OrderService {
|
||||
};
|
||||
}
|
||||
|
||||
if (sortColumn) {
|
||||
orderBy = [{ [sortColumn]: sortDirection }];
|
||||
}
|
||||
|
||||
if (types) {
|
||||
where.OR = types.map((type) => {
|
||||
return {
|
||||
@ -317,8 +300,9 @@ export class OrderService {
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
await this.orders({
|
||||
const [orders, count] = await Promise.all([
|
||||
this.orders({
|
||||
orderBy,
|
||||
skip,
|
||||
take,
|
||||
where,
|
||||
@ -332,10 +316,12 @@ export class OrderService {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
SymbolProfile: true,
|
||||
tags: true
|
||||
},
|
||||
orderBy: { date: 'asc' }
|
||||
})
|
||||
)
|
||||
}
|
||||
}),
|
||||
this.prismaService.order.count({ where })
|
||||
]);
|
||||
|
||||
const activities = orders
|
||||
.filter((order) => {
|
||||
return (
|
||||
withExcludedAccounts ||
|
||||
@ -361,6 +347,16 @@ export class OrderService {
|
||||
)
|
||||
};
|
||||
});
|
||||
|
||||
return { activities, count };
|
||||
}
|
||||
|
||||
public async order(
|
||||
orderWhereUniqueInput: Prisma.OrderWhereUniqueInput
|
||||
): Promise<Order | null> {
|
||||
return this.prismaService.order.findUnique({
|
||||
where: orderWhereUniqueInput
|
||||
});
|
||||
}
|
||||
|
||||
public async updateOrder({
|
||||
@ -439,4 +435,24 @@ export class OrderService {
|
||||
where
|
||||
});
|
||||
}
|
||||
|
||||
private async orders(params: {
|
||||
include?: Prisma.OrderInclude;
|
||||
skip?: number;
|
||||
take?: number;
|
||||
cursor?: Prisma.OrderWhereUniqueInput;
|
||||
where?: Prisma.OrderWhereInput;
|
||||
orderBy?: Prisma.Enumerable<Prisma.OrderOrderByWithRelationInput>;
|
||||
}): Promise<OrderWithAccount[]> {
|
||||
const { include, skip, take, cursor, where, orderBy } = params;
|
||||
|
||||
return this.prismaService.order.findMany({
|
||||
cursor,
|
||||
include,
|
||||
orderBy,
|
||||
skip,
|
||||
take,
|
||||
where
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { AccessModule } from '@ghostfolio/api/app/access/access.module';
|
||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { UserModule } from '@ghostfolio/api/app/user/user.module';
|
||||
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
|
||||
import { DataGatheringModule } from '@ghostfolio/api/services/data-gathering/data-gathering.module';
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
@ -12,7 +13,6 @@ import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/ap
|
||||
import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment';
|
||||
import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup';
|
||||
import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment';
|
||||
import { AccountBalanceService } from '@ghostfolio/api/services/account-balance/account-balance.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
|
||||
@ -225,7 +225,7 @@ export class PortfolioService {
|
||||
}): Promise<InvestmentItem[]> {
|
||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||
|
||||
const activities = await this.orderService.getOrders({
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
filters,
|
||||
userId,
|
||||
types: ['DIVIDEND'],
|
||||
@ -679,13 +679,13 @@ export class PortfolioService {
|
||||
const user = await this.userService.user({ id: userId });
|
||||
const userCurrency = this.getUserCurrency(user);
|
||||
|
||||
const orders = (
|
||||
await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts: true
|
||||
})
|
||||
).filter(({ SymbolProfile }) => {
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
const orders = activities.filter(({ SymbolProfile }) => {
|
||||
return (
|
||||
SymbolProfile.dataSource === aDataSource &&
|
||||
SymbolProfile.symbol === aSymbol
|
||||
@ -1075,7 +1075,7 @@ export class PortfolioService {
|
||||
const userCurrency = this.getUserCurrency(user);
|
||||
|
||||
const accountBalances = await this.accountBalanceService.getAccountBalances(
|
||||
{ filters, user }
|
||||
{ filters, user, withExcludedAccounts }
|
||||
);
|
||||
|
||||
let accountBalanceItems: HistoricalDataItem[] = Object.values(
|
||||
@ -1639,18 +1639,18 @@ export class PortfolioService {
|
||||
userId
|
||||
});
|
||||
|
||||
const activities = await this.orderService.getOrders({
|
||||
const { activities } = await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId
|
||||
});
|
||||
|
||||
const excludedActivities = (
|
||||
await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts: true
|
||||
})
|
||||
).filter(({ Account: account }) => {
|
||||
let { activities: excludedActivities } = await this.orderService.getOrders({
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts: true
|
||||
});
|
||||
|
||||
excludedActivities = excludedActivities.filter(({ Account: account }) => {
|
||||
return account?.isExcluded ?? false;
|
||||
});
|
||||
|
||||
@ -1830,7 +1830,7 @@ export class PortfolioService {
|
||||
const userCurrency =
|
||||
this.request.user?.Settings?.settings.baseCurrency ?? DEFAULT_CURRENCY;
|
||||
|
||||
const orders = await this.orderService.getOrders({
|
||||
const { activities, count } = await this.orderService.getOrders({
|
||||
filters,
|
||||
includeDrafts,
|
||||
userCurrency,
|
||||
@ -1839,11 +1839,11 @@ export class PortfolioService {
|
||||
types: ['BUY', 'SELL']
|
||||
});
|
||||
|
||||
if (orders.length <= 0) {
|
||||
if (count <= 0) {
|
||||
return { transactionPoints: [], orders: [], portfolioOrders: [] };
|
||||
}
|
||||
|
||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
||||
const portfolioOrders: PortfolioOrder[] = activities.map((order) => ({
|
||||
currency: order.SymbolProfile.currency,
|
||||
dataSource: order.SymbolProfile.dataSource,
|
||||
date: format(order.date, DATE_FORMAT),
|
||||
@ -1877,8 +1877,8 @@ export class PortfolioService {
|
||||
portfolioCalculator.computeTransactionPoints();
|
||||
|
||||
return {
|
||||
orders,
|
||||
portfolioOrders,
|
||||
orders: activities,
|
||||
transactionPoints: portfolioCalculator.getTransactionPoints()
|
||||
};
|
||||
}
|
||||
@ -1913,13 +1913,14 @@ export class PortfolioService {
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}) {
|
||||
const ordersOfTypeItemOrLiability = await this.orderService.getOrders({
|
||||
filters,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts,
|
||||
types: ['ITEM', 'LIABILITY']
|
||||
});
|
||||
const { activities: ordersOfTypeItemOrLiability } =
|
||||
await this.orderService.getOrders({
|
||||
filters,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts,
|
||||
types: ['ITEM', 'LIABILITY']
|
||||
});
|
||||
|
||||
const accounts: PortfolioDetails['accounts'] = {};
|
||||
const platforms: PortfolioDetails['platforms'] = {};
|
||||
|
@ -60,7 +60,7 @@ export class UserService {
|
||||
PROPERTY_SYSTEM_MESSAGE
|
||||
)) as SystemMessage;
|
||||
|
||||
if (systemMessageProperty?.targetGroups?.includes(subscription.type)) {
|
||||
if (systemMessageProperty?.targetGroups?.includes(subscription?.type)) {
|
||||
systemMessage = systemMessageProperty;
|
||||
}
|
||||
|
||||
|
6
apps/api/src/decorators/has-permission.decorator.ts
Normal file
6
apps/api/src/decorators/has-permission.decorator.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
export const HAS_PERMISSION_KEY = 'has_permission';
|
||||
|
||||
export function HasPermission(permission: string) {
|
||||
return SetMetadata(HAS_PERMISSION_KEY, permission);
|
||||
}
|
55
apps/api/src/guards/has-permission.guard.spec.ts
Normal file
55
apps/api/src/guards/has-permission.guard.spec.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { HttpException } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ExecutionContextHost } from '@nestjs/core/helpers/execution-context-host';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { HasPermissionGuard } from './has-permission.guard';
|
||||
|
||||
describe('HasPermissionGuard', () => {
|
||||
let guard: HasPermissionGuard;
|
||||
let reflector: Reflector;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [HasPermissionGuard, Reflector]
|
||||
}).compile();
|
||||
|
||||
guard = module.get<HasPermissionGuard>(HasPermissionGuard);
|
||||
reflector = module.get<Reflector>(Reflector);
|
||||
});
|
||||
|
||||
function setupReflectorSpy(returnValue: string) {
|
||||
jest.spyOn(reflector, 'get').mockReturnValue(returnValue);
|
||||
}
|
||||
|
||||
function createMockExecutionContext(permissions: string[]) {
|
||||
return new ExecutionContextHost([
|
||||
{
|
||||
user: {
|
||||
permissions // Set user permissions based on the argument
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
it('should deny access if the user does not have any permission', () => {
|
||||
setupReflectorSpy('required-permission');
|
||||
const noPermissions = createMockExecutionContext([]);
|
||||
|
||||
expect(() => guard.canActivate(noPermissions)).toThrow(HttpException);
|
||||
});
|
||||
|
||||
it('should deny access if the user has the wrong permission', () => {
|
||||
setupReflectorSpy('required-permission');
|
||||
const wrongPermission = createMockExecutionContext(['wrong-permission']);
|
||||
|
||||
expect(() => guard.canActivate(wrongPermission)).toThrow(HttpException);
|
||||
});
|
||||
|
||||
it('should allow access if the user has the required permission', () => {
|
||||
setupReflectorSpy('required-permission');
|
||||
const rightPermission = createMockExecutionContext(['required-permission']);
|
||||
|
||||
expect(guard.canActivate(rightPermission)).toBe(true);
|
||||
});
|
||||
});
|
37
apps/api/src/guards/has-permission.guard.ts
Normal file
37
apps/api/src/guards/has-permission.guard.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { HAS_PERMISSION_KEY } from '@ghostfolio/api/decorators/has-permission.decorator';
|
||||
import { hasPermission } from '@ghostfolio/common/permissions';
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
HttpException,
|
||||
Injectable
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
@Injectable()
|
||||
export class HasPermissionGuard implements CanActivate {
|
||||
public constructor(private reflector: Reflector) {}
|
||||
|
||||
public canActivate(context: ExecutionContext): boolean {
|
||||
const requiredPermission = this.reflector.get<string>(
|
||||
HAS_PERMISSION_KEY,
|
||||
context.getHandler()
|
||||
);
|
||||
|
||||
if (!requiredPermission) {
|
||||
return true; // No specific permissions required
|
||||
}
|
||||
|
||||
const { user } = context.switchToHttp().getRequest();
|
||||
|
||||
if (!user || !hasPermission(user.permissions, requiredPermission)) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||
StatusCodes.FORBIDDEN
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -32,9 +32,11 @@ export function nullifyValuesInObjects<T>(aObjects: T[], keys: string[]): T[] {
|
||||
}
|
||||
|
||||
export function redactAttributes({
|
||||
isFirstRun = true,
|
||||
object,
|
||||
options
|
||||
}: {
|
||||
isFirstRun?: boolean;
|
||||
object: any;
|
||||
options: { attribute: string; valueMap: { [key: string]: any } }[];
|
||||
}): any {
|
||||
@ -42,7 +44,10 @@ export function redactAttributes({
|
||||
return object;
|
||||
}
|
||||
|
||||
const redactedObject = cloneDeep(object);
|
||||
// Create deep clone
|
||||
const redactedObject = isFirstRun
|
||||
? JSON.parse(JSON.stringify(object))
|
||||
: object;
|
||||
|
||||
for (const option of options) {
|
||||
if (redactedObject.hasOwnProperty(option.attribute)) {
|
||||
@ -59,7 +64,11 @@ export function redactAttributes({
|
||||
if (isArray(redactedObject[property])) {
|
||||
redactedObject[property] = redactedObject[property].map(
|
||||
(currentObject) => {
|
||||
return redactAttributes({ options, object: currentObject });
|
||||
return redactAttributes({
|
||||
options,
|
||||
isFirstRun: false,
|
||||
object: currentObject
|
||||
});
|
||||
}
|
||||
);
|
||||
} else if (
|
||||
@ -69,6 +78,7 @@ export function redactAttributes({
|
||||
// Recursively call the function on the nested object
|
||||
redactedObject[property] = redactAttributes({
|
||||
options,
|
||||
isFirstRun: false,
|
||||
object: redactedObject[property]
|
||||
});
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
private static countriesMapping = {
|
||||
'Russian Federation': 'Russia'
|
||||
};
|
||||
private static holdingsWeightTreshold = 0.85;
|
||||
private static sectorsMapping = {
|
||||
'Consumer Discretionary': 'Consumer Cyclical',
|
||||
'Consumer Defensive': 'Consumer Staples',
|
||||
@ -113,7 +114,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||
});
|
||||
});
|
||||
|
||||
if (holdings?.weight < 0.95) {
|
||||
if (
|
||||
holdings?.weight < TrackinsightDataEnhancerService.holdingsWeightTreshold
|
||||
) {
|
||||
// Skip if data is inaccurate
|
||||
return response;
|
||||
}
|
||||
|
@ -346,7 +346,7 @@ export class DataProviderService {
|
||||
);
|
||||
|
||||
try {
|
||||
this.marketDataService.updateMany({
|
||||
await this.marketDataService.updateMany({
|
||||
data: Object.keys(response)
|
||||
.filter((symbol) => {
|
||||
return (
|
||||
|
6
apps/api/webpack.config.js
Normal file
6
apps/api/webpack.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
const { composePlugins, withNx } = require('@nx/webpack');
|
||||
|
||||
module.exports = composePlugins(withNx(), (config, { options, context }) => {
|
||||
// Customize webpack config here
|
||||
return config;
|
||||
});
|
@ -150,7 +150,7 @@
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/angular:webpack-dev-server",
|
||||
"executor": "@nx/angular:dev-server",
|
||||
"options": {
|
||||
"proxyConfig": "apps/client/proxy.conf.json",
|
||||
"buildTarget": "client:build"
|
||||
@ -175,7 +175,7 @@
|
||||
"buildTarget": "client:build:development-nl"
|
||||
},
|
||||
"development-pl": {
|
||||
"browserTarget": "client:build:development-pl"
|
||||
"buildTarget": "client:build:development-pl"
|
||||
},
|
||||
"development-pt": {
|
||||
"buildTarget": "client:build:development-pt"
|
||||
@ -191,7 +191,7 @@
|
||||
"extract-i18n": {
|
||||
"executor": "ng-extract-i18n-merge:ng-extract-i18n-merge",
|
||||
"options": {
|
||||
"browserTarget": "client:build",
|
||||
"buildTarget": "client:build",
|
||||
"includeContext": true,
|
||||
"outputPath": "src/locales",
|
||||
"targetFiles": [
|
||||
@ -207,7 +207,7 @@
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"executor": "@nx/eslint:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["apps/client/**/*.ts"]
|
||||
}
|
||||
|
@ -37,7 +37,7 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
|
||||
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
|
@ -7,11 +7,18 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Sort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { HistoricalDataItem, User } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
AccountBalancesResponse,
|
||||
HistoricalDataItem,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import Big from 'big.js';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
@ -29,15 +36,22 @@ import { AccountDetailDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./account-detail-dialog.component.scss']
|
||||
})
|
||||
export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
public accountBalances: AccountBalancesResponse['balances'];
|
||||
public activities: OrderWithAccount[];
|
||||
public balance: number;
|
||||
public currency: string;
|
||||
public dataSource: MatTableDataSource<OrderWithAccount>;
|
||||
public equity: number;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToDeleteAccountBalance: boolean;
|
||||
public historicalDataItems: HistoricalDataItem[];
|
||||
public isLoadingActivities: boolean;
|
||||
public isLoadingChart: boolean;
|
||||
public name: string;
|
||||
public orders: OrderWithAccount[];
|
||||
public platformName: string;
|
||||
public sortColumn = 'date';
|
||||
public sortDirection: SortDirection = 'desc';
|
||||
public totalItems: number;
|
||||
public transactionCount: number;
|
||||
public user: User;
|
||||
public valueInBaseCurrency: number;
|
||||
@ -58,14 +72,17 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.hasPermissionToDeleteAccountBalance = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.deleteAccountBalance
|
||||
);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.isLoadingChart = true;
|
||||
|
||||
this.dataService
|
||||
.fetchAccount(this.data.accountId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -97,16 +114,119 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
);
|
||||
|
||||
this.dataService
|
||||
.fetchActivities({
|
||||
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
|
||||
})
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities }) => {
|
||||
this.orders = activities;
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
|
||||
this.fetchAccountBalances();
|
||||
this.fetchActivities();
|
||||
this.fetchPortfolioPerformance();
|
||||
}
|
||||
|
||||
public onClose() {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onDeleteAccountBalance(aId: string) {
|
||||
this.dataService
|
||||
.deleteAccountBalance(aId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.fetchAccountBalances();
|
||||
this.fetchPortfolioPerformance();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onExport() {
|
||||
let activityIds = [];
|
||||
|
||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||
activityIds = this.dataSource.data.map(({ id }) => {
|
||||
return id;
|
||||
});
|
||||
} else {
|
||||
activityIds = this.activities.map(({ id }) => {
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.fetchExport(activityIds)
|
||||
.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 onSortChanged({ active, direction }: Sort) {
|
||||
this.sortColumn = active;
|
||||
this.sortDirection = direction;
|
||||
|
||||
this.fetchActivities();
|
||||
}
|
||||
|
||||
private fetchAccountBalances() {
|
||||
this.dataService
|
||||
.fetchAccountBalances(this.data.accountId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ balances }) => {
|
||||
this.accountBalances = balances;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
private fetchActivities() {
|
||||
this.isLoadingActivities = true;
|
||||
|
||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||
this.dataService
|
||||
.fetchActivities({
|
||||
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }],
|
||||
sortColumn: this.sortColumn,
|
||||
sortDirection: this.sortDirection
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities, count }) => {
|
||||
this.dataSource = new MatTableDataSource(activities);
|
||||
this.totalItems = count;
|
||||
|
||||
this.isLoadingActivities = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
} else {
|
||||
this.dataService
|
||||
.fetchActivities({
|
||||
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities }) => {
|
||||
this.activities = activities;
|
||||
|
||||
this.isLoadingActivities = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private fetchPortfolioPerformance() {
|
||||
this.isLoadingChart = true;
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioPerformance({
|
||||
@ -137,39 +257,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((impersonationId) => {
|
||||
this.hasImpersonationId = !!impersonationId;
|
||||
});
|
||||
}
|
||||
|
||||
public onClose() {
|
||||
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() {
|
||||
|
@ -31,7 +31,7 @@
|
||||
></gf-investment-chart>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="mb-3 row">
|
||||
<div class="col-6 mb-3">
|
||||
<gf-value
|
||||
i18n
|
||||
@ -64,11 +64,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" [ngClass]="{ 'd-none': !orders?.length }">
|
||||
<div class="col mb-3">
|
||||
<div class="h5 mb-0" i18n>Activities</div>
|
||||
<mat-tab-group
|
||||
animationDuration="0"
|
||||
[mat-stretch-tabs]="false"
|
||||
[ngClass]="{ 'd-none': isLoadingActivities }"
|
||||
>
|
||||
<mat-tab>
|
||||
<ng-template i18n mat-tab-label>Activities</ng-template>
|
||||
<gf-activities-table-lazy
|
||||
*ngIf="user?.settings?.isExperimentalFeatures === true"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[dataSource]="dataSource"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="false"
|
||||
[sortColumn]="sortColumn"
|
||||
[sortDirection]="sortDirection"
|
||||
[totalItems]="totalItems"
|
||||
(export)="onExport()"
|
||||
(sortChanged)="onSortChanged($event)"
|
||||
></gf-activities-table-lazy>
|
||||
<gf-activities-table
|
||||
[activities]="orders"
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="data.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
@ -79,8 +101,18 @@
|
||||
[showActions]="false"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
<mat-tab>
|
||||
<ng-template i18n mat-tab-label>Cash Balances</ng-template>
|
||||
<gf-account-balances
|
||||
[accountBalances]="accountBalances"
|
||||
[accountId]="data.accountId"
|
||||
[locale]="user?.settings?.locale"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
|
||||
(accountBalanceDeleted)="onDeleteAccountBalance($event)"
|
||||
></gf-account-balances>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -2,9 +2,12 @@ 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 { MatTabsModule } from '@angular/material/tabs';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
||||
import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module';
|
||||
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
@ -15,13 +18,16 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
|
||||
declarations: [AccountDetailDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAccountBalancesModule,
|
||||
GfActivitiesTableModule,
|
||||
GfActivitiesTableLazyModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfInvestmentChartModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatTabsModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -241,7 +241,7 @@
|
||||
></td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
|
||||
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
|
||||
<button
|
||||
|
@ -120,7 +120,7 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 py-2" mat-header-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort, Sort } from '@angular/material/sort';
|
||||
import { MatSort, Sort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
@ -19,7 +19,7 @@ import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||
import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
|
||||
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { AssetSubClass, DataSource, Prisma } from '@prisma/client';
|
||||
import { AssetSubClass, DataSource } from '@prisma/client';
|
||||
import { isUUID } from 'class-validator';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
@ -160,7 +160,7 @@ export class AdminMarketDataComponent
|
||||
|
||||
this.loadData({
|
||||
sortColumn,
|
||||
sortDirection: <Prisma.SortOrder>direction,
|
||||
sortDirection: direction,
|
||||
pageIndex: this.paginator.pageIndex
|
||||
});
|
||||
}
|
||||
@ -175,7 +175,7 @@ export class AdminMarketDataComponent
|
||||
this.loadData({
|
||||
pageIndex: page.pageIndex,
|
||||
sortColumn: this.sort.active,
|
||||
sortDirection: <Prisma.SortOrder>this.sort.direction
|
||||
sortDirection: this.sort.direction
|
||||
});
|
||||
}
|
||||
|
||||
@ -262,7 +262,7 @@ export class AdminMarketDataComponent
|
||||
}: {
|
||||
pageIndex: number;
|
||||
sortColumn?: string;
|
||||
sortDirection?: Prisma.SortOrder;
|
||||
sortDirection?: SortDirection;
|
||||
} = { pageIndex: 0 }
|
||||
) {
|
||||
this.isLoading = true;
|
||||
|
@ -129,7 +129,7 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
|
@ -119,8 +119,12 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
const currency = prompt($localize`Please add a currency:`);
|
||||
|
||||
if (currency) {
|
||||
const currencies = uniq([...this.customCurrencies, currency]);
|
||||
this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies });
|
||||
if (currency.length === 3) {
|
||||
const currencies = uniq([...this.customCurrencies, currency]);
|
||||
this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies });
|
||||
} else {
|
||||
alert($localize`${currency} is an invalid currency!`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,7 +68,7 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1 text-center"
|
||||
|
@ -48,7 +48,7 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1 text-center"
|
||||
|
@ -178,7 +178,7 @@
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<ng-container matColumnDef="actions" stickyEnd>
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="mat-mdc-header-cell px-1 py-2"
|
||||
|
@ -15,7 +15,7 @@ import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import {
|
||||
STAY_SIGNED_IN,
|
||||
KEY_STAY_SIGNED_IN,
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
@ -196,7 +196,7 @@ export class HeaderComponent implements OnChanges {
|
||||
public setToken(aToken: string) {
|
||||
this.tokenStorageService.saveToken(
|
||||
aToken,
|
||||
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
|
||||
this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true'
|
||||
);
|
||||
|
||||
this.router.navigate(['/']);
|
||||
|
@ -4,7 +4,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
|
||||
import {
|
||||
STAY_SIGNED_IN,
|
||||
KEY_STAY_SIGNED_IN,
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
@ -31,7 +31,7 @@ export class LoginWithAccessTokenDialog {
|
||||
|
||||
public onChangeStaySignedIn(aValue: MatCheckboxChange) {
|
||||
this.settingsStorageService.setSetting(
|
||||
STAY_SIGNED_IN,
|
||||
KEY_STAY_SIGNED_IN,
|
||||
aValue.checked?.toString()
|
||||
);
|
||||
}
|
||||
|
@ -7,12 +7,16 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { Sort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderInfo,
|
||||
EnhancedSymbolProfile,
|
||||
LineChartItem
|
||||
LineChartItem,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
@ -31,6 +35,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
|
||||
styleUrls: ['./position-detail-dialog.component.scss']
|
||||
})
|
||||
export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
public activities: OrderWithAccount[];
|
||||
public assetClass: string;
|
||||
public assetSubClass: string;
|
||||
public averagePrice: number;
|
||||
@ -39,6 +44,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public dataProviderInfo: DataProviderInfo;
|
||||
public dataSource: MatTableDataSource<OrderWithAccount>;
|
||||
public dividendInBaseCurrency: number;
|
||||
public feeInBaseCurrency: number;
|
||||
public firstBuyDate: string;
|
||||
@ -51,16 +57,19 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
public minPrice: number;
|
||||
public netPerformance: number;
|
||||
public netPerformancePercent: number;
|
||||
public orders: OrderWithAccount[];
|
||||
public quantity: number;
|
||||
public quantityPrecision = 2;
|
||||
public reportDataGlitchMail: string;
|
||||
public sectors: {
|
||||
[name: string]: { name: string; value: number };
|
||||
};
|
||||
public sortColumn = 'date';
|
||||
public sortDirection: SortDirection = 'desc';
|
||||
public SymbolProfile: EnhancedSymbolProfile;
|
||||
public tags: Tag[];
|
||||
public totalItems: number;
|
||||
public transactionCount: number;
|
||||
public user: User;
|
||||
public value: number;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -69,7 +78,8 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<PositionDetailDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams
|
||||
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams,
|
||||
private userService: UserService
|
||||
) {}
|
||||
|
||||
public ngOnInit(): void {
|
||||
@ -102,10 +112,12 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
transactionCount,
|
||||
value
|
||||
}) => {
|
||||
this.activities = orders;
|
||||
this.averagePrice = averagePrice;
|
||||
this.benchmarkDataItems = [];
|
||||
this.countries = {};
|
||||
this.dataProviderInfo = dataProviderInfo;
|
||||
this.dataSource = new MatTableDataSource(orders.reverse());
|
||||
this.dividendInBaseCurrency = dividendInBaseCurrency;
|
||||
this.feeInBaseCurrency = feeInBaseCurrency;
|
||||
this.firstBuyDate = firstBuyDate;
|
||||
@ -130,7 +142,6 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
this.minPrice = minPrice;
|
||||
this.netPerformance = netPerformance;
|
||||
this.netPerformancePercent = netPerformancePercent;
|
||||
this.orders = orders;
|
||||
this.quantity = quantity;
|
||||
this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`;
|
||||
this.sectors = {};
|
||||
@ -142,6 +153,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
};
|
||||
});
|
||||
this.transactionCount = transactionCount;
|
||||
this.totalItems = transactionCount;
|
||||
this.value = value;
|
||||
|
||||
if (SymbolProfile?.assetClass) {
|
||||
@ -239,6 +251,16 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
);
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public onClose(): void {
|
||||
@ -246,12 +268,20 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onExport() {
|
||||
let activityIds = [];
|
||||
|
||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||
activityIds = this.dataSource.data.map(({ id }) => {
|
||||
return id;
|
||||
});
|
||||
} else {
|
||||
activityIds = this.activities.map(({ id }) => {
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
this.dataService
|
||||
.fetchExport(
|
||||
this.orders.map((order) => {
|
||||
return order.id;
|
||||
})
|
||||
)
|
||||
.fetchExport(activityIds)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
downloadAsFile({
|
||||
|
@ -246,11 +246,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" [ngClass]="{ 'd-none': !orders?.length }">
|
||||
<div class="row" [ngClass]="{ 'd-none': !activities?.length }">
|
||||
<div class="col mb-3">
|
||||
<div class="h5 mb-0" i18n>Activities</div>
|
||||
<gf-activities-table-lazy
|
||||
*ngIf="user?.settings?.isExperimentalFeatures === true"
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[dataSource]="dataSource"
|
||||
[deviceType]="data.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data.locale"
|
||||
[showActions]="false"
|
||||
[showNameColumn]="false"
|
||||
[sortColumn]="sortColumn"
|
||||
[sortDirection]="sortDirection"
|
||||
[sortDisabled]="true"
|
||||
[totalItems]="totalItems"
|
||||
(export)="onExport()"
|
||||
></gf-activities-table-lazy>
|
||||
<gf-activities-table
|
||||
[activities]="orders"
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
[baseCurrency]="data.baseCurrency"
|
||||
[deviceType]="data.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
@ -277,7 +296,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
*ngIf="data.hasPermissionToReportDataGlitch === true && orders?.length > 0"
|
||||
*ngIf="activities?.length > 0 && data.hasPermissionToReportDataGlitch === true"
|
||||
class="row"
|
||||
>
|
||||
<div class="col">
|
||||
|
@ -5,6 +5,7 @@ import { MatChipsModule } from '@angular/material/chips';
|
||||
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 { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module';
|
||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||
@ -19,6 +20,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
GfActivitiesTableLazyModule,
|
||||
GfDataProviderCreditsModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
|
@ -8,7 +8,8 @@ import {
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import {
|
||||
STAY_SIGNED_IN,
|
||||
KEY_STAY_SIGNED_IN,
|
||||
KEY_TOKEN,
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
@ -241,7 +242,8 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.settingsStorageService.removeSetting(STAY_SIGNED_IN);
|
||||
this.settingsStorageService.removeSetting(KEY_STAY_SIGNED_IN);
|
||||
this.settingsStorageService.removeSetting(KEY_TOKEN);
|
||||
|
||||
this.update();
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
STAY_SIGNED_IN,
|
||||
KEY_STAY_SIGNED_IN,
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
@ -31,7 +31,7 @@ export class AuthPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.tokenStorageService.saveToken(
|
||||
jwt,
|
||||
this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true'
|
||||
this.settingsStorageService.getSetting(KEY_STAY_SIGNED_IN) === 'true'
|
||||
);
|
||||
|
||||
this.router.navigate(['/']);
|
||||
|
@ -1,5 +1,8 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { PageEvent } from '@angular/material/paginator';
|
||||
import { Sort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
@ -10,6 +13,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
|
||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
@ -30,12 +34,18 @@ import { ImportActivitiesDialogParams } from './import-activities-dialog/interfa
|
||||
})
|
||||
export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
public activities: Activity[];
|
||||
public dataSource: MatTableDataSource<Activity>;
|
||||
public defaultAccountId: string;
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToCreateActivity: boolean;
|
||||
public hasPermissionToDeleteActivity: boolean;
|
||||
public pageIndex = 0;
|
||||
public pageSize = DEFAULT_PAGE_SIZE;
|
||||
public routeQueryParams: Subscription;
|
||||
public sortColumn = 'date';
|
||||
public sortDirection: SortDirection = 'desc';
|
||||
public totalItems: number;
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -62,6 +72,12 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
return id === params['activityId'];
|
||||
});
|
||||
|
||||
this.openUpdateActivityDialog(activity);
|
||||
} else if (this.dataSource) {
|
||||
const activity = this.dataSource.data.find(({ id }) => {
|
||||
return id === params['activityId'];
|
||||
});
|
||||
|
||||
this.openUpdateActivityDialog(activity);
|
||||
} else {
|
||||
this.router.navigate(['.'], { relativeTo: this.route });
|
||||
@ -103,21 +119,48 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public fetchActivities() {
|
||||
this.dataService
|
||||
.fetchActivities({})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities }) => {
|
||||
this.activities = activities;
|
||||
if (this.user?.settings?.isExperimentalFeatures === true) {
|
||||
this.dataService
|
||||
.fetchActivities({
|
||||
skip: this.pageIndex * this.pageSize,
|
||||
sortColumn: this.sortColumn,
|
||||
sortDirection: this.sortDirection,
|
||||
take: this.pageSize
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities, count }) => {
|
||||
this.dataSource = new MatTableDataSource(activities);
|
||||
this.totalItems = count;
|
||||
|
||||
if (
|
||||
this.hasPermissionToCreateActivity &&
|
||||
this.activities?.length <= 0
|
||||
) {
|
||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||
}
|
||||
if (this.hasPermissionToCreateActivity && this.totalItems <= 0) {
|
||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
} else {
|
||||
this.dataService
|
||||
.fetchActivities({})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities }) => {
|
||||
this.activities = activities;
|
||||
|
||||
if (
|
||||
this.hasPermissionToCreateActivity &&
|
||||
this.activities?.length <= 0
|
||||
) {
|
||||
this.router.navigate([], { queryParams: { createDialog: true } });
|
||||
}
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public onChangePage(page: PageEvent) {
|
||||
this.pageIndex = page.pageIndex;
|
||||
|
||||
this.fetchActivities();
|
||||
}
|
||||
|
||||
public onCloneActivity(aActivity: Activity) {
|
||||
@ -225,6 +268,14 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onSortChanged({ active, direction }: Sort) {
|
||||
this.pageIndex = 0;
|
||||
this.sortColumn = active;
|
||||
this.sortDirection = direction;
|
||||
|
||||
this.fetchActivities();
|
||||
}
|
||||
|
||||
public onUpdateActivity(aActivity: OrderModel) {
|
||||
this.router.navigate([], {
|
||||
queryParams: { activityId: aActivity.id, editDialog: true }
|
||||
|
@ -1,8 +1,34 @@
|
||||
<div class="container">
|
||||
<div class="row mb-3">
|
||||
<div class="mb-3 row">
|
||||
<div class="col">
|
||||
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
|
||||
<gf-activities-table-lazy
|
||||
*ngIf="user?.settings?.isExperimentalFeatures === true"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[dataSource]="dataSource"
|
||||
[deviceType]="deviceType"
|
||||
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
|
||||
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||
[locale]="user?.settings?.locale"
|
||||
[pageIndex]="pageIndex"
|
||||
[pageSize]="pageSize"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
|
||||
[sortColumn]="sortColumn"
|
||||
[sortDirection]="sortDirection"
|
||||
[totalItems]="totalItems"
|
||||
(activityDeleted)="onDeleteActivity($event)"
|
||||
(activityToClone)="onCloneActivity($event)"
|
||||
(activityToUpdate)="onUpdateActivity($event)"
|
||||
(deleteAllActivities)="onDeleteAllActivities()"
|
||||
(export)="onExport($event)"
|
||||
(exportDrafts)="onExportDrafts($event)"
|
||||
(import)="onImport()"
|
||||
(importDividends)="onImportDividends()"
|
||||
(pageChanged)="onChangePage($event)"
|
||||
(sortChanged)="onSortChanged($event)"
|
||||
></gf-activities-table-lazy>
|
||||
<gf-activities-table
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="deviceType"
|
||||
|
@ -4,6 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
|
||||
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
|
||||
import { ActivitiesPageRoutingModule } from './activities-page-routing.module';
|
||||
@ -17,6 +18,7 @@ import { GfImportActivitiesDialogModule } from './import-activities-dialog/impor
|
||||
ActivitiesPageRoutingModule,
|
||||
CommonModule,
|
||||
GfActivitiesTableModule,
|
||||
GfActivitiesTableLazyModule,
|
||||
GfCreateOrUpdateActivityDialogModule,
|
||||
GfImportActivitiesDialogModule,
|
||||
MatButtonModule,
|
||||
|
@ -12,7 +12,9 @@ import {
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { MatStepper } from '@angular/material/stepper';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
@ -35,6 +37,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
|
||||
export class ImportActivitiesDialog implements OnDestroy {
|
||||
public accounts: CreateAccountDto[] = [];
|
||||
public activities: Activity[] = [];
|
||||
public dataSource: MatTableDataSource<Activity>;
|
||||
public details: any[] = [];
|
||||
public deviceType: string;
|
||||
public dialogTitle = $localize`Import Activities`;
|
||||
@ -45,7 +48,10 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
public maxSafeInteger = Number.MAX_SAFE_INTEGER;
|
||||
public mode: 'DIVIDEND';
|
||||
public selectedActivities: Activity[] = [];
|
||||
public sortColumn = 'date';
|
||||
public sortDirection: SortDirection = 'desc';
|
||||
public stepperOrientation: StepperOrientation;
|
||||
public totalItems: number;
|
||||
public uniqueAssetForm: FormGroup;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -173,6 +179,8 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ activities }) => {
|
||||
this.activities = activities;
|
||||
this.dataSource = new MatTableDataSource(activities.reverse());
|
||||
this.totalItems = activities.length;
|
||||
|
||||
aStepper.next();
|
||||
|
||||
@ -252,6 +260,14 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
content.activities = content.activities.map((activity) => {
|
||||
if (activity.id) {
|
||||
delete activity.id;
|
||||
}
|
||||
|
||||
return activity;
|
||||
});
|
||||
|
||||
try {
|
||||
const { activities } =
|
||||
await this.importActivitiesService.importJson({
|
||||
@ -260,6 +276,8 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
isDryRun: true
|
||||
});
|
||||
this.activities = activities;
|
||||
this.dataSource = new MatTableDataSource(activities.reverse());
|
||||
this.totalItems = activities.length;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({ error, activities: content.activities });
|
||||
@ -276,6 +294,8 @@ export class ImportActivitiesDialog implements OnDestroy {
|
||||
userAccounts: this.data.user.accounts
|
||||
});
|
||||
this.activities = data.activities;
|
||||
this.dataSource = new MatTableDataSource(data.activities.reverse());
|
||||
this.totalItems = data.activities.length;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.handleImportError({
|
||||
|
@ -119,8 +119,29 @@
|
||||
</ng-template>
|
||||
<div class="pt-3">
|
||||
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
|
||||
<gf-activities-table-lazy
|
||||
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures === true"
|
||||
[baseCurrency]="data?.user?.settings?.baseCurrency"
|
||||
[dataSource]="dataSource"
|
||||
[deviceType]="data?.deviceType"
|
||||
[hasPermissionToCreateActivity]="false"
|
||||
[hasPermissionToExportActivities]="false"
|
||||
[hasPermissionToFilter]="false"
|
||||
[hasPermissionToOpenDetails]="false"
|
||||
[locale]="data?.user?.settings?.locale"
|
||||
[pageSize]="maxSafeInteger"
|
||||
[showActions]="false"
|
||||
[showCheckbox]="true"
|
||||
[showFooter]="false"
|
||||
[showSymbolColumn]="false"
|
||||
[sortColumn]="sortColumn"
|
||||
[sortDirection]="sortDirection"
|
||||
[sortDisabled]="true"
|
||||
[totalItems]="totalItems"
|
||||
(selectedActivities)="updateSelection($event)"
|
||||
></gf-activities-table-lazy>
|
||||
<gf-activities-table
|
||||
*ngIf="importStep === 1"
|
||||
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
[baseCurrency]="data?.user?.settings?.baseCurrency"
|
||||
[deviceType]="data?.deviceType"
|
||||
|
@ -13,6 +13,7 @@ import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-heade
|
||||
import { GfFileDropModule } from '@ghostfolio/client/directives/file-drop/file-drop.module';
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
|
||||
|
||||
import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
||||
|
||||
@ -22,6 +23,7 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfActivitiesTableModule,
|
||||
GfActivitiesTableLazyModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfFileDropModule,
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
|
||||
import { UpdateBulkMarketDataDto } from '@ghostfolio/api/app/admin/update-bulk-market-data.dto';
|
||||
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
|
||||
@ -17,7 +18,7 @@ import {
|
||||
Filter,
|
||||
UniqueAsset
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { DataSource, MarketData, Platform, Prisma, Tag } from '@prisma/client';
|
||||
import { DataSource, MarketData, Platform, Tag } from '@prisma/client';
|
||||
import { JobStatus } from 'bull';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { Observable, map } from 'rxjs';
|
||||
@ -84,7 +85,7 @@ export class AdminService {
|
||||
filters?: Filter[];
|
||||
skip?: number;
|
||||
sortColumn?: string;
|
||||
sortDirection?: Prisma.SortOrder;
|
||||
sortDirection?: SortDirection;
|
||||
take: number;
|
||||
}) {
|
||||
let params = this.dataService.buildFiltersAsQueryParams({ filters });
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
|
||||
@ -18,6 +19,7 @@ import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Access,
|
||||
AccountBalancesResponse,
|
||||
Accounts,
|
||||
BenchmarkMarketDataDetails,
|
||||
BenchmarkResponse,
|
||||
@ -137,28 +139,56 @@ export class DataService {
|
||||
return this.http.get<AccountWithValue>(`/api/v1/account/${aAccountId}`);
|
||||
}
|
||||
|
||||
public fetchAccountBalances(aAccountId: string) {
|
||||
return this.http.get<AccountBalancesResponse>(
|
||||
`/api/v1/account/${aAccountId}/balances`
|
||||
);
|
||||
}
|
||||
|
||||
public fetchAccounts() {
|
||||
return this.http.get<Accounts>('/api/v1/account');
|
||||
}
|
||||
|
||||
public fetchActivities({
|
||||
filters
|
||||
filters,
|
||||
skip,
|
||||
sortColumn,
|
||||
sortDirection,
|
||||
take
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
skip?: number;
|
||||
sortColumn?: string;
|
||||
sortDirection?: SortDirection;
|
||||
take?: number;
|
||||
}): Observable<Activities> {
|
||||
return this.http
|
||||
.get<any>('/api/v1/order', {
|
||||
params: this.buildFiltersAsQueryParams({ filters })
|
||||
let params = this.buildFiltersAsQueryParams({ filters });
|
||||
|
||||
if (skip) {
|
||||
params = params.append('skip', skip);
|
||||
}
|
||||
|
||||
if (sortColumn) {
|
||||
params = params.append('sortColumn', sortColumn);
|
||||
}
|
||||
|
||||
if (sortDirection) {
|
||||
params = params.append('sortDirection', sortDirection);
|
||||
}
|
||||
|
||||
if (take) {
|
||||
params = params.append('take', take);
|
||||
}
|
||||
|
||||
return this.http.get<any>('/api/v1/order', { params }).pipe(
|
||||
map(({ activities, count }) => {
|
||||
for (const activity of activities) {
|
||||
activity.createdAt = parseISO(activity.createdAt);
|
||||
activity.date = parseISO(activity.date);
|
||||
}
|
||||
return { activities, count };
|
||||
})
|
||||
.pipe(
|
||||
map(({ activities }) => {
|
||||
for (const activity of activities) {
|
||||
activity.createdAt = parseISO(activity.createdAt);
|
||||
activity.date = parseISO(activity.date);
|
||||
}
|
||||
return { activities };
|
||||
})
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
public fetchDividends({
|
||||
@ -205,6 +235,10 @@ export class DataService {
|
||||
return this.http.delete<any>(`/api/v1/account/${aId}`);
|
||||
}
|
||||
|
||||
public deleteAccountBalance(aId: string) {
|
||||
return this.http.delete<any>(`/api/v1/account-balance/${aId}`);
|
||||
}
|
||||
|
||||
public deleteAllOrders() {
|
||||
return this.http.delete<any>(`/api/v1/order/`);
|
||||
}
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
export const RANGE = 'range';
|
||||
export const STAY_SIGNED_IN = 'staySignedIn';
|
||||
export const KEY_RANGE = 'range';
|
||||
export const KEY_STAY_SIGNED_IN = 'staySignedIn';
|
||||
export const KEY_TOKEN = 'auth-token';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
|
||||
|
||||
import { KEY_TOKEN } from './settings-storage.service';
|
||||
import { UserService } from './user/user.service';
|
||||
|
||||
const TOKEN_KEY = 'auth-token';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
@ -16,16 +15,16 @@ export class TokenStorageService {
|
||||
|
||||
public getToken(): string {
|
||||
return (
|
||||
window.sessionStorage.getItem(TOKEN_KEY) ||
|
||||
window.localStorage.getItem(TOKEN_KEY)
|
||||
window.sessionStorage.getItem(KEY_TOKEN) ||
|
||||
window.localStorage.getItem(KEY_TOKEN)
|
||||
);
|
||||
}
|
||||
|
||||
public saveToken(token: string, staySignedIn = false): void {
|
||||
if (staySignedIn) {
|
||||
window.localStorage.setItem(TOKEN_KEY, token);
|
||||
window.localStorage.setItem(KEY_TOKEN, token);
|
||||
}
|
||||
window.sessionStorage.setItem(TOKEN_KEY, token);
|
||||
window.sessionStorage.setItem(KEY_TOKEN, token);
|
||||
}
|
||||
|
||||
public signOut(): void {
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-Black.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-Black.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-BlackItalic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-BlackItalic.woff2
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user