Compare commits
25 Commits
2.29.0
...
feature/ch
Author | SHA1 | Date | |
---|---|---|---|
5799b9e71c | |||
188389d26c | |||
80e899a5d3 | |||
7c33120546 | |||
7f3c86038f | |||
c1446f8559 | |||
88d5dfe435 | |||
7dc8f80fdf | |||
96f90c7259 | |||
a10d9cb6ba | |||
4547c5da1d | |||
28706d7b26 | |||
492bc5e17b | |||
6c37737051 | |||
8677d20c2c | |||
4d905065ad | |||
5599b41b83 | |||
8d5a60d777 | |||
695acf4f3f | |||
67dbef3b7a | |||
0e94112dc7 | |||
b22edff16b | |||
ffb7cbff50 | |||
25424ad280 | |||
a768902b00 |
41
CHANGELOG.md
41
CHANGELOG.md
@ -5,6 +5,47 @@ 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).
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changed
|
||||
|
||||
- Set the select column of the lazy-loaded activities table to stick at the end (experimental)
|
||||
- Improved the performance of the value redaction interceptor for the impersonation mode by eliminating `cloneDeep`
|
||||
|
||||
### 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
|
||||
|
@ -14,7 +14,8 @@
|
||||
"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": {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
@ -8,11 +9,11 @@ import {
|
||||
UseGuards
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AccountBalanceService } from './account-balance.service';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
import { AccountBalance } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { AccountBalanceService } from './account-balance.service';
|
||||
|
||||
@Controller('account-balance')
|
||||
export class AccountBalanceController {
|
||||
|
@ -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) {
|
||||
|
@ -236,6 +236,7 @@ export class ImportService {
|
||||
|
||||
const activitiesExtendedWithErrors = await this.extendActivitiesWithErrors({
|
||||
activitiesDto,
|
||||
userCurrency,
|
||||
userId
|
||||
});
|
||||
|
||||
@ -459,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(
|
||||
|
@ -25,7 +25,7 @@ import { endOfToday, isAfter } from 'date-fns';
|
||||
import { groupBy } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Activities, 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.Enumerable<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;
|
||||
@ -379,6 +351,14 @@ 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({
|
||||
data,
|
||||
where
|
||||
@ -455,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,6 +1,11 @@
|
||||
import { TimelineInfoInterface } from '@ghostfolio/api/app/portfolio/interfaces/timeline-info.interface';
|
||||
import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DATE_FORMAT,
|
||||
getAverage,
|
||||
parseDate,
|
||||
resetHours
|
||||
} from '@ghostfolio/common/helper';
|
||||
import {
|
||||
DataProviderInfo,
|
||||
ResponseError,
|
||||
@ -43,9 +48,6 @@ import { TransactionPointSymbol } from './interfaces/transaction-point-symbol.in
|
||||
import { TransactionPoint } from './interfaces/transaction-point.interface';
|
||||
|
||||
export class PortfolioCalculator {
|
||||
private static readonly CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT =
|
||||
true;
|
||||
|
||||
private static readonly ENABLE_LOGGING = false;
|
||||
|
||||
private currency: string;
|
||||
@ -1162,11 +1164,11 @@ export class PortfolioCalculator {
|
||||
order.type === 'BUY'
|
||||
? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
|
||||
: totalUnits.gt(0)
|
||||
? totalInvestment
|
||||
.div(totalUnits)
|
||||
.mul(order.quantity)
|
||||
.mul(this.getFactor(order.type))
|
||||
: new Big(0);
|
||||
? totalInvestment
|
||||
.div(totalUnits)
|
||||
.mul(order.quantity)
|
||||
.mul(this.getFactor(order.type))
|
||||
: new Big(0);
|
||||
|
||||
if (PortfolioCalculator.ENABLE_LOGGING) {
|
||||
console.log('totalInvestment', totalInvestment.toNumber());
|
||||
@ -1266,6 +1268,10 @@ export class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
const averageInvestmentBetweenStartAndEndDate = getAverage(
|
||||
Object.values(investmentValues)
|
||||
);
|
||||
|
||||
const totalGrossPerformance = grossPerformance.minus(
|
||||
grossPerformanceAtStartDate
|
||||
);
|
||||
@ -1274,17 +1280,12 @@ export class PortfolioCalculator {
|
||||
.minus(grossPerformanceAtStartDate)
|
||||
.minus(fees.minus(feesAtStartDate));
|
||||
|
||||
const maxInvestmentBetweenStartAndEndDate = valueAtStartDate.plus(
|
||||
maxTotalInvestment.minus(investmentAtStartDate)
|
||||
);
|
||||
|
||||
const grossPerformancePercentage =
|
||||
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
|
||||
averagePriceAtStartDate.eq(0) ||
|
||||
averagePriceAtEndDate.eq(0) ||
|
||||
orders[indexOfStartOrder].unitPrice.eq(0)
|
||||
? maxInvestmentBetweenStartAndEndDate.gt(0)
|
||||
? totalGrossPerformance.div(maxInvestmentBetweenStartAndEndDate)
|
||||
? averageInvestmentBetweenStartAndEndDate.gt(0)
|
||||
? totalGrossPerformance.div(averageInvestmentBetweenStartAndEndDate)
|
||||
: new Big(0)
|
||||
: // This formula has the issue that buying more units with a price
|
||||
// lower than the average buying price results in a positive
|
||||
@ -1301,12 +1302,11 @@ export class PortfolioCalculator {
|
||||
: new Big(0);
|
||||
|
||||
const netPerformancePercentage =
|
||||
PortfolioCalculator.CALCULATE_PERCENTAGE_PERFORMANCE_WITH_MAX_INVESTMENT ||
|
||||
averagePriceAtStartDate.eq(0) ||
|
||||
averagePriceAtEndDate.eq(0) ||
|
||||
orders[indexOfStartOrder].unitPrice.eq(0)
|
||||
? maxInvestmentBetweenStartAndEndDate.gt(0)
|
||||
? totalNetPerformance.div(maxInvestmentBetweenStartAndEndDate)
|
||||
? averageInvestmentBetweenStartAndEndDate.gt(0)
|
||||
? totalNetPerformance.div(averageInvestmentBetweenStartAndEndDate)
|
||||
: new Big(0)
|
||||
: // This formula has the issue that buying more units with a price
|
||||
// lower than the average buying price results in a positive
|
||||
|
@ -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;
|
||||
}
|
||||
|
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,48 +150,48 @@
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@nx/angular:webpack-dev-server",
|
||||
"executor": "@nx/angular:dev-server",
|
||||
"options": {
|
||||
"proxyConfig": "apps/client/proxy.conf.json",
|
||||
"browserTarget": "client:build"
|
||||
"buildTarget": "client:build"
|
||||
},
|
||||
"configurations": {
|
||||
"development-de": {
|
||||
"browserTarget": "client:build:development-de"
|
||||
"buildTarget": "client:build:development-de"
|
||||
},
|
||||
"development-en": {
|
||||
"browserTarget": "client:build:development-en"
|
||||
"buildTarget": "client:build:development-en"
|
||||
},
|
||||
"development-es": {
|
||||
"browserTarget": "client:build:development-es"
|
||||
"buildTarget": "client:build:development-es"
|
||||
},
|
||||
"development-fr": {
|
||||
"browserTarget": "client:build:development-fr"
|
||||
"buildTarget": "client:build:development-fr"
|
||||
},
|
||||
"development-it": {
|
||||
"browserTarget": "client:build:development-it"
|
||||
"buildTarget": "client:build:development-it"
|
||||
},
|
||||
"development-nl": {
|
||||
"browserTarget": "client:build:development-nl"
|
||||
"buildTarget": "client:build:development-nl"
|
||||
},
|
||||
"development-pl": {
|
||||
"browserTarget": "client:build:development-pl"
|
||||
"buildTarget": "client:build:development-pl"
|
||||
},
|
||||
"development-pt": {
|
||||
"browserTarget": "client:build:development-pt"
|
||||
"buildTarget": "client:build:development-pt"
|
||||
},
|
||||
"development-tr": {
|
||||
"browserTarget": "client:build:development-tr"
|
||||
"buildTarget": "client:build:development-tr"
|
||||
},
|
||||
"production": {
|
||||
"browserTarget": "client:build:production"
|
||||
"buildTarget": "client:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "ng-extract-i18n-merge:ng-extract-i18n-merge",
|
||||
"options": {
|
||||
"browserTarget": "client:build",
|
||||
"buildTarget": "client:build",
|
||||
"includeContext": true,
|
||||
"outputPath": "src/locales",
|
||||
"targetFiles": [
|
||||
|
@ -7,6 +7,8 @@ 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';
|
||||
@ -16,6 +18,7 @@ import {
|
||||
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';
|
||||
@ -24,7 +27,6 @@ import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { AccountDetailDialogParams } from './interfaces/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
@Component({
|
||||
host: { class: 'd-flex flex-column h-100' },
|
||||
@ -38,6 +40,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
public activities: OrderWithAccount[];
|
||||
public balance: number;
|
||||
public currency: string;
|
||||
public dataSource: MatTableDataSource<OrderWithAccount>;
|
||||
public equity: number;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToDeleteAccountBalance: boolean;
|
||||
@ -46,6 +49,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
public isLoadingChart: boolean;
|
||||
public name: string;
|
||||
public platformName: string;
|
||||
public sortColumn = 'date';
|
||||
public sortDirection: SortDirection = 'desc';
|
||||
public totalItems: number;
|
||||
public transactionCount: number;
|
||||
public user: User;
|
||||
public valueInBaseCurrency: number;
|
||||
@ -77,8 +83,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.isLoadingActivities = true;
|
||||
|
||||
this.dataService
|
||||
.fetchAccount(this.data.accountId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -110,19 +114,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
@ -131,6 +122,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
});
|
||||
|
||||
this.fetchAccountBalances();
|
||||
this.fetchActivities();
|
||||
this.fetchPortfolioPerformance();
|
||||
}
|
||||
|
||||
@ -151,12 +143,20 @@ export class AccountDetailDialog 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.activities.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
)
|
||||
.fetchExport(activityIds)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((data) => {
|
||||
downloadAsFile({
|
||||
@ -172,6 +172,13 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
public onSortChanged({ active, direction }: Sort) {
|
||||
this.sortColumn = active;
|
||||
this.sortDirection = direction;
|
||||
|
||||
this.fetchActivities();
|
||||
}
|
||||
|
||||
private fetchAccountBalances() {
|
||||
this.dataService
|
||||
.fetchAccountBalances(this.data.accountId)
|
||||
@ -183,6 +190,41 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
|
@ -71,7 +71,25 @@
|
||||
>
|
||||
<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
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
[activities]="activities"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[deviceType]="data.deviceType"
|
||||
|
@ -7,6 +7,7 @@ import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-foote
|
||||
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';
|
||||
@ -19,6 +20,7 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
|
||||
CommonModule,
|
||||
GfAccountBalancesModule,
|
||||
GfActivitiesTableModule,
|
||||
GfActivitiesTableLazyModule,
|
||||
GfDialogFooterModule,
|
||||
GfDialogHeaderModule,
|
||||
GfInvestmentChartModule,
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -1,6 +1,7 @@
|
||||
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';
|
||||
@ -16,7 +17,7 @@ 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';
|
||||
import { DataSource, Order as OrderModel, Prisma } from '@prisma/client';
|
||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
@ -43,7 +44,7 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
|
||||
public pageSize = DEFAULT_PAGE_SIZE;
|
||||
public routeQueryParams: Subscription;
|
||||
public sortColumn = 'date';
|
||||
public sortDirection: Prisma.SortOrder = 'desc';
|
||||
public sortDirection: SortDirection = 'desc';
|
||||
public totalItems: number;
|
||||
public user: User;
|
||||
|
||||
@ -71,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 });
|
||||
@ -261,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 }
|
||||
|
@ -13,6 +13,8 @@
|
||||
[pageIndex]="pageIndex"
|
||||
[pageSize]="pageSize"
|
||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
|
||||
[sortColumn]="sortColumn"
|
||||
[sortDirection]="sortDirection"
|
||||
[totalItems]="totalItems"
|
||||
(activityDeleted)="onDeleteActivity($event)"
|
||||
(activityToClone)="onCloneActivity($event)"
|
||||
@ -23,6 +25,7 @@
|
||||
(import)="onImport()"
|
||||
(importDividends)="onImportDividends()"
|
||||
(pageChanged)="onChangePage($event)"
|
||||
(sortChanged)="onSortChanged($event)"
|
||||
></gf-activities-table-lazy>
|
||||
<gf-activities-table
|
||||
*ngIf="user?.settings?.isExperimentalFeatures !== true"
|
||||
|
@ -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();
|
||||
|
||||
@ -260,6 +268,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 +286,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';
|
||||
@ -38,7 +39,7 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||
import { AccountWithValue, DateRange, GroupBy } from '@ghostfolio/common/types';
|
||||
import { DataSource, Order as OrderModel, Prisma } from '@prisma/client';
|
||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { cloneDeep, groupBy, isNumber } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
@ -158,7 +159,7 @@ export class DataService {
|
||||
filters?: Filter[];
|
||||
skip?: number;
|
||||
sortColumn?: string;
|
||||
sortDirection?: Prisma.SortOrder;
|
||||
sortDirection?: SortDirection;
|
||||
take?: number;
|
||||
}): Observable<Activities> {
|
||||
let params = this.buildFiltersAsQueryParams({ filters });
|
||||
|
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.
BIN
apps/client/src/assets/fonts/InterDisplay-Bold.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-Bold.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-BoldItalic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-BoldItalic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-ExtraBold.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-ExtraBold.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-ExtraBoldItalic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-ExtraBoldItalic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-ExtraLight.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-ExtraLight.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-ExtraLightItalic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-ExtraLightItalic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-Italic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-Italic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-Light.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-Light.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-LightItalic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-LightItalic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-Medium.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-Medium.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-MediumItalic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-MediumItalic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-Regular.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-Regular.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-SemiBold.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-SemiBold.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-SemiBoldItalic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-SemiBoldItalic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-Thin.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-Thin.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterDisplay-ThinItalic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterDisplay-ThinItalic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterVariable-Italic.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterVariable-Italic.woff2
Normal file
Binary file not shown.
BIN
apps/client/src/assets/fonts/InterVariable.woff2
Normal file
BIN
apps/client/src/assets/fonts/InterVariable.woff2
Normal file
Binary file not shown.
@ -1,226 +1,273 @@
|
||||
/* Variable fonts usage:
|
||||
:root { font-family: "Inter", sans-serif; }
|
||||
@supports (font-variation-settings: normal) {
|
||||
:root { font-family: "InterVariable", sans-serif; font-optical-sizing: auto; }
|
||||
} */
|
||||
@font-face {
|
||||
font-family: InterVariable;
|
||||
font-style: normal;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('InterVariable.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: InterVariable;
|
||||
font-style: italic;
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
src: url('InterVariable-Italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* static fonts */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-Thin.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-Thin.woff?v=3.19') format('woff');
|
||||
src: url('Inter-Thin.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-ThinItalic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-ThinItalic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-ThinItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-ExtraLight.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-ExtraLight.woff?v=3.19') format('woff');
|
||||
src: url('Inter-ExtraLight.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-ExtraLightItalic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-ExtraLightItalic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-ExtraLightItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-Light.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-Light.woff?v=3.19') format('woff');
|
||||
src: url('Inter-Light.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-LightItalic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-LightItalic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-LightItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-Regular.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-Regular.woff?v=3.19') format('woff');
|
||||
src: url('Inter-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-Italic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-Italic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-Italic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-Medium.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-Medium.woff?v=3.19') format('woff');
|
||||
src: url('Inter-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-MediumItalic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-MediumItalic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-MediumItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-SemiBold.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-SemiBold.woff?v=3.19') format('woff');
|
||||
src: url('Inter-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-SemiBoldItalic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-SemiBoldItalic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-SemiBoldItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-Bold.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-Bold.woff?v=3.19') format('woff');
|
||||
src: url('Inter-Bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-BoldItalic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-BoldItalic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-BoldItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-ExtraBold.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-ExtraBold.woff?v=3.19') format('woff');
|
||||
src: url('Inter-ExtraBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-ExtraBoldItalic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-ExtraBoldItalic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-ExtraBoldItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-Black.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-Black.woff?v=3.19') format('woff');
|
||||
src: url('Inter-Black.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src:
|
||||
url('Inter-BlackItalic.woff2?v=3.19') format('woff2'),
|
||||
url('Inter-BlackItalic.woff?v=3.19') format('woff');
|
||||
src: url('Inter-BlackItalic.woff2') format('woff2');
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------
|
||||
Variable font.
|
||||
Usage:
|
||||
|
||||
html { font-family: 'Inter', sans-serif; }
|
||||
@supports (font-variation-settings: normal) {
|
||||
html { font-family: 'Inter var', sans-serif; }
|
||||
}
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter var';
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-named-instance: 'Regular';
|
||||
src: url('Inter-roman.var.woff2?v=3.19') format('woff2');
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-Thin.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter var';
|
||||
font-weight: 100 900;
|
||||
font-display: swap;
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-named-instance: 'Italic';
|
||||
src: url('Inter-italic.var.woff2?v=3.19') format('woff2');
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
[EXPERIMENTAL] Multi-axis, single variable font.
|
||||
|
||||
Slant axis is not yet widely supported (as of February 2019) and thus this
|
||||
multi-axis single variable font is opt-in rather than the default.
|
||||
|
||||
When using this, you will probably need to set font-variation-settings
|
||||
explicitly, e.g.
|
||||
|
||||
* { font-variation-settings: "slnt" 0deg }
|
||||
.italic { font-variation-settings: "slnt" 10deg }
|
||||
|
||||
*/
|
||||
@font-face {
|
||||
font-family: 'Inter var experimental';
|
||||
font-weight: 100 900;
|
||||
font-weight: 100;
|
||||
font-display: swap;
|
||||
font-style: oblique 0deg 10deg;
|
||||
src: url('Inter.var.woff2?v=3.19') format('woff2');
|
||||
src: url('InterDisplay-ThinItalic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-ExtraLight.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-weight: 200;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-ExtraLightItalic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-Light.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-LightItalic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-Regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-Italic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-Medium.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-MediumItalic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-SemiBoldItalic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-Bold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-BoldItalic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-ExtraBold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-weight: 800;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-ExtraBoldItalic.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: normal;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-Black.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'InterDisplay';
|
||||
font-style: italic;
|
||||
font-weight: 900;
|
||||
font-display: swap;
|
||||
src: url('InterDisplay-BlackItalic.woff2') format('woff2');
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -111,5 +111,7 @@ $gf-theme-dark: mat.define-dark-theme(
|
||||
--gf-theme-secondary-500: #3686cf;
|
||||
--gf-theme-secondary-500-rgb: 78, 208, 94;
|
||||
|
||||
--mdc-typography-button-letter-spacing: normal;
|
||||
--mdc-filled-button-label-text-tracking: normal;
|
||||
--mdc-outlined-button-label-text-tracking: normal;
|
||||
--mdc-text-button-label-text-tracking: normal;
|
||||
}
|
||||
|
@ -133,6 +133,14 @@ export function getAssetProfileIdentifier({ dataSource, symbol }: UniqueAsset) {
|
||||
return `${dataSource}-${symbol}`;
|
||||
}
|
||||
|
||||
export function getAverage(aArray: Big[]) {
|
||||
if (aArray?.length > 0) {
|
||||
return aArray.reduce((a, b) => a.plus(b), new Big(0)).div(aArray.length);
|
||||
}
|
||||
|
||||
return new Big(0);
|
||||
}
|
||||
|
||||
export function getBackgroundColor(aColorScheme: ColorScheme) {
|
||||
return getCssVariable(
|
||||
aColorScheme === 'DARK' ||
|
||||
@ -381,10 +389,10 @@ export function resolveFearAndGreedIndex(aValue: number) {
|
||||
export function resolveMarketCondition(
|
||||
aMarketCondition: Benchmark['marketCondition']
|
||||
) {
|
||||
if (aMarketCondition === 'BEAR_MARKET') {
|
||||
if (aMarketCondition === 'ALL_TIME_HIGH') {
|
||||
return { emoji: '🎉' };
|
||||
} else if (aMarketCondition === 'BEAR_MARKET') {
|
||||
return { emoji: '🐻' };
|
||||
} else if (aMarketCondition === 'BULL_MARKET') {
|
||||
return { emoji: '🐮' };
|
||||
} else {
|
||||
return { emoji: '⚪' };
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { BenchmarkTrend } from '@ghostfolio/common/types/';
|
||||
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
|
||||
|
||||
export interface Benchmark {
|
||||
marketCondition: 'BEAR_MARKET' | 'BULL_MARKET' | 'NEUTRAL_MARKET';
|
||||
marketCondition: 'ALL_TIME_HIGH' | 'BEAR_MARKET' | 'NEUTRAL_MARKET';
|
||||
name: EnhancedSymbolProfile['name'];
|
||||
performances: {
|
||||
allTimeHigh: {
|
||||
|
@ -65,8 +65,16 @@
|
||||
</div>
|
||||
|
||||
<div class="activities">
|
||||
<table class="gf-table w-100" mat-table [dataSource]="dataSource">
|
||||
<ng-container matColumnDef="select">
|
||||
<table
|
||||
class="gf-table w-100"
|
||||
mat-table
|
||||
matSort
|
||||
[dataSource]="dataSource"
|
||||
[matSortActive]="sortColumn"
|
||||
[matSortDirection]="sortDirection"
|
||||
[matSortDisabled]="sortDisabled"
|
||||
>
|
||||
<ng-container matColumnDef="select" sticky>
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<mat-checkbox
|
||||
color="primary"
|
||||
@ -118,12 +126,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="nameWithSymbol">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="SymbolProfile.symbol"
|
||||
>
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<ng-container i18n>Name</ng-container>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
|
||||
@ -243,7 +246,6 @@
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
@ -263,12 +265,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="currency">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="d-none d-lg-table-cell px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="SymbolProfile.currency"
|
||||
>
|
||||
<th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
|
||||
<ng-container i18n>Currency</ng-container>
|
||||
</th>
|
||||
<td
|
||||
@ -285,7 +282,6 @@
|
||||
*matHeaderCellDef
|
||||
class="d-lg-none d-xl-none justify-content-end px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header
|
||||
>
|
||||
<ng-container i18n>Value</ng-container>
|
||||
</th>
|
||||
@ -301,12 +297,7 @@
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="account">
|
||||
<th
|
||||
*matHeaderCellDef
|
||||
class="px-1"
|
||||
mat-header-cell
|
||||
mat-sort-header="Account.name"
|
||||
>
|
||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||
<span class="d-none d-lg-block" i18n>Account</span>
|
||||
</th>
|
||||
<td *matCellDef="let element" class="px-1" mat-cell>
|
||||
@ -468,15 +459,6 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<mat-paginator
|
||||
[length]="totalItems"
|
||||
[ngClass]="{
|
||||
'd-none': (isLoading && totalItems === 0) || totalItems <= pageSize
|
||||
}"
|
||||
[pageSize]="pageSize"
|
||||
(page)="onChangePage($event)"
|
||||
></mat-paginator>
|
||||
|
||||
<ngx-skeleton-loader
|
||||
*ngIf="isLoading"
|
||||
animation="pulse"
|
||||
@ -487,6 +469,17 @@
|
||||
}"
|
||||
></ngx-skeleton-loader>
|
||||
|
||||
<mat-paginator
|
||||
[length]="totalItems"
|
||||
[ngClass]="{
|
||||
'd-none': (isLoading && !totalItems) || totalItems <= pageSize
|
||||
}"
|
||||
[pageIndex]="pageIndex"
|
||||
[pageSize]="pageSize"
|
||||
[showFirstLastButtons]="true"
|
||||
(page)="onChangePage($event)"
|
||||
></mat-paginator>
|
||||
|
||||
<div
|
||||
*ngIf="
|
||||
dataSource?.data.length === 0 && hasPermissionToCreateActivity && !isLoading
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
@ -11,6 +12,7 @@ import {
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort, Sort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
@ -29,7 +31,7 @@ import { Subject, Subscription, takeUntil } from 'rxjs';
|
||||
templateUrl: './activities-table-lazy.component.html'
|
||||
})
|
||||
export class ActivitiesTableLazyComponent
|
||||
implements OnChanges, OnDestroy, OnInit
|
||||
implements AfterViewInit, OnChanges, OnDestroy, OnInit
|
||||
{
|
||||
@Input() baseCurrency: string;
|
||||
@Input() dataSource: MatTableDataSource<Activity>;
|
||||
@ -44,6 +46,9 @@ export class ActivitiesTableLazyComponent
|
||||
@Input() showCheckbox = false;
|
||||
@Input() showFooter = true;
|
||||
@Input() showNameColumn = true;
|
||||
@Input() sortColumn: string;
|
||||
@Input() sortDirection: SortDirection;
|
||||
@Input() sortDisabled = false;
|
||||
@Input() totalItems = Number.MAX_SAFE_INTEGER;
|
||||
|
||||
@Output() activityDeleted = new EventEmitter<string>();
|
||||
@ -56,8 +61,10 @@ export class ActivitiesTableLazyComponent
|
||||
@Output() importDividends = new EventEmitter<UniqueAsset>();
|
||||
@Output() pageChanged = new EventEmitter<PageEvent>();
|
||||
@Output() selectedActivities = new EventEmitter<Activity[]>();
|
||||
@Output() sortChanged = new EventEmitter<Sort>();
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public defaultDateFormat: string;
|
||||
public displayedColumns = [];
|
||||
@ -86,6 +93,12 @@ export class ActivitiesTableLazyComponent
|
||||
}
|
||||
}
|
||||
|
||||
public ngAfterViewInit() {
|
||||
this.sort.sortChange.subscribe((value: Sort) => {
|
||||
this.sortChanged.emit(value);
|
||||
});
|
||||
}
|
||||
|
||||
public areAllRowsSelected() {
|
||||
const numSelectedRows = this.selectedRows.selected.length;
|
||||
const numTotalRows = this.dataSource.data.length;
|
||||
|
@ -31,6 +31,7 @@ import { ActivitiesTableLazyComponent } from './activities-table-lazy.component'
|
||||
MatCheckboxModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
MatTooltipModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
|
@ -89,7 +89,7 @@
|
||||
'text-danger':
|
||||
element?.performances?.allTimeHigh?.performancePercent < 0,
|
||||
'text-success':
|
||||
element?.performances?.allTimeHigh?.performancePercent > 0
|
||||
element?.performances?.allTimeHigh?.performancePercent === 0
|
||||
}"
|
||||
[value]="element?.performances?.allTimeHigh?.performancePercent"
|
||||
/>
|
||||
|
@ -16,14 +16,20 @@
|
||||
<div
|
||||
*ngIf="isPercent"
|
||||
class="mb-0 value"
|
||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||
[ngClass]="{
|
||||
'font-weight-bold h2': size === 'large',
|
||||
h4: size === 'medium'
|
||||
}"
|
||||
>
|
||||
{{ formattedValue }}%
|
||||
</div>
|
||||
<div
|
||||
*ngIf="!isPercent"
|
||||
class="mb-0 value"
|
||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||
[ngClass]="{
|
||||
'font-weight-bold h2': size === 'large',
|
||||
h4: size === 'medium'
|
||||
}"
|
||||
>
|
||||
<ng-container *ngIf="value === null">
|
||||
<span class="text-monospace text-muted">***</span>
|
||||
@ -42,7 +48,10 @@
|
||||
<ng-container *ngIf="isString">
|
||||
<div
|
||||
class="mb-0 text-truncate value"
|
||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||
[ngClass]="{
|
||||
'font-weight-bold h2': size === 'large',
|
||||
h4: size === 'medium'
|
||||
}"
|
||||
>
|
||||
{{ formattedValue }}
|
||||
</div>
|
||||
|
@ -4,6 +4,7 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
|
||||
.h2 {
|
||||
font-variant-numeric: initial;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
82
package.json
82
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.29.0",
|
||||
"version": "2.31.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -53,17 +53,17 @@
|
||||
"workspace-generator": "nx workspace-generator"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "17.0.4",
|
||||
"@angular/cdk": "17.0.1",
|
||||
"@angular/common": "17.0.4",
|
||||
"@angular/compiler": "17.0.4",
|
||||
"@angular/core": "17.0.4",
|
||||
"@angular/forms": "17.0.4",
|
||||
"@angular/material": "17.0.1",
|
||||
"@angular/platform-browser": "17.0.4",
|
||||
"@angular/platform-browser-dynamic": "17.0.4",
|
||||
"@angular/router": "17.0.4",
|
||||
"@angular/service-worker": "17.0.4",
|
||||
"@angular/animations": "17.0.7",
|
||||
"@angular/cdk": "17.0.4",
|
||||
"@angular/common": "17.0.7",
|
||||
"@angular/compiler": "17.0.7",
|
||||
"@angular/core": "17.0.7",
|
||||
"@angular/forms": "17.0.7",
|
||||
"@angular/material": "17.0.4",
|
||||
"@angular/platform-browser": "17.0.7",
|
||||
"@angular/platform-browser-dynamic": "17.0.7",
|
||||
"@angular/router": "17.0.7",
|
||||
"@angular/service-worker": "17.0.7",
|
||||
"@codewithdan/observable-store": "2.2.15",
|
||||
"@dfinity/agent": "0.15.7",
|
||||
"@dfinity/auth-client": "0.15.7",
|
||||
@ -81,7 +81,7 @@
|
||||
"@nestjs/platform-express": "10.1.3",
|
||||
"@nestjs/schedule": "3.0.2",
|
||||
"@nestjs/serve-static": "4.0.0",
|
||||
"@prisma/client": "5.6.0",
|
||||
"@prisma/client": "5.7.0",
|
||||
"@simplewebauthn/browser": "8.3.1",
|
||||
"@simplewebauthn/server": "8.3.2",
|
||||
"@stripe/stripe-js": "1.47.0",
|
||||
@ -122,7 +122,7 @@
|
||||
"passport": "0.6.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.0",
|
||||
"prisma": "5.6.0",
|
||||
"prisma": "5.7.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.5.6",
|
||||
"stripe": "11.12.0",
|
||||
@ -133,34 +133,34 @@
|
||||
"zone.js": "0.14.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "17.0.3",
|
||||
"@angular-devkit/core": "17.0.3",
|
||||
"@angular-devkit/schematics": "17.0.3",
|
||||
"@angular-eslint/eslint-plugin": "17.1.0",
|
||||
"@angular-eslint/eslint-plugin-template": "17.1.0",
|
||||
"@angular-eslint/template-parser": "17.1.0",
|
||||
"@angular/cli": "17.0.3",
|
||||
"@angular/compiler-cli": "17.0.4",
|
||||
"@angular/language-service": "17.0.4",
|
||||
"@angular/localize": "17.0.4",
|
||||
"@angular/pwa": "17.0.3",
|
||||
"@angular-devkit/build-angular": "17.0.7",
|
||||
"@angular-devkit/core": "17.0.7",
|
||||
"@angular-devkit/schematics": "17.0.7",
|
||||
"@angular-eslint/eslint-plugin": "17.1.1",
|
||||
"@angular-eslint/eslint-plugin-template": "17.1.1",
|
||||
"@angular-eslint/template-parser": "17.1.1",
|
||||
"@angular/cli": "17.0.7",
|
||||
"@angular/compiler-cli": "17.0.7",
|
||||
"@angular/language-service": "17.0.7",
|
||||
"@angular/localize": "17.0.7",
|
||||
"@angular/pwa": "17.0.7",
|
||||
"@nestjs/schematics": "10.0.1",
|
||||
"@nestjs/testing": "10.1.3",
|
||||
"@nx/angular": "17.0.2",
|
||||
"@nx/cypress": "17.0.2",
|
||||
"@nx/eslint-plugin": "17.0.2",
|
||||
"@nx/jest": "17.0.2",
|
||||
"@nx/js": "17.0.2",
|
||||
"@nx/nest": "17.0.2",
|
||||
"@nx/node": "17.0.2",
|
||||
"@nx/storybook": "17.0.2",
|
||||
"@nx/web": "17.0.2",
|
||||
"@nx/workspace": "17.0.2",
|
||||
"@nx/angular": "17.2.5",
|
||||
"@nx/cypress": "17.2.5",
|
||||
"@nx/eslint-plugin": "17.2.5",
|
||||
"@nx/jest": "17.2.5",
|
||||
"@nx/js": "17.2.5",
|
||||
"@nx/nest": "17.2.5",
|
||||
"@nx/node": "17.2.5",
|
||||
"@nx/storybook": "17.2.5",
|
||||
"@nx/web": "17.2.5",
|
||||
"@nx/workspace": "17.2.5",
|
||||
"@schematics/angular": "17.0.0",
|
||||
"@simplewebauthn/typescript-types": "8.0.0",
|
||||
"@storybook/addon-essentials": "7.5.3",
|
||||
"@storybook/angular": "7.5.3",
|
||||
"@storybook/core-server": "7.5.3",
|
||||
"@storybook/addon-essentials": "7.6.5",
|
||||
"@storybook/angular": "7.6.5",
|
||||
"@storybook/core-server": "7.6.5",
|
||||
"@types/big.js": "6.1.6",
|
||||
"@types/body-parser": "1.19.2",
|
||||
"@types/cache-manager": "3.4.2",
|
||||
@ -168,7 +168,7 @@
|
||||
"@types/google-spreadsheet": "3.1.5",
|
||||
"@types/jest": "29.4.4",
|
||||
"@types/lodash": "4.14.195",
|
||||
"@types/node": "20.4.2",
|
||||
"@types/node": "18.16.9",
|
||||
"@types/papaparse": "5.3.7",
|
||||
"@types/passport-google-oauth20": "2.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.51.0",
|
||||
@ -185,8 +185,8 @@
|
||||
"import-sort-style-module": "6.0.0",
|
||||
"jest": "29.4.3",
|
||||
"jest-environment-jsdom": "29.4.3",
|
||||
"jest-preset-angular": "13.1.3",
|
||||
"nx": "17.0.2",
|
||||
"jest-preset-angular": "13.1.4",
|
||||
"nx": "17.2.5",
|
||||
"prettier": "3.1.0",
|
||||
"prettier-plugin-organize-attributes": "1.0.0",
|
||||
"react": "18.2.0",
|
||||
|
Reference in New Issue
Block a user