Merge branch 'main' of gitea.suda.codes:giteauser/ghostfolio-mirror
This commit is contained in:
commit
a05203c785
15
CHANGELOG.md
15
CHANGELOG.md
@ -9,10 +9,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Changed
|
||||
|
||||
- Optimized the portfolio calculations with smarter cloning of activities
|
||||
- Integrated the add currency functionality into the market data section of the admin control panel
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `prisma` from version `5.19.1` to `5.20.0`
|
||||
- Upgraded `webpack-bundle-analyzer` from version `4.10.1` to `4.10.2`
|
||||
|
||||
## 2.110.0 - 2024-09-24
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the usability of various action menus by introducing horizontal lines to separate the delete action
|
||||
- Improved the chart in the account detail dialog (experimental)
|
||||
- Aligned the holdings and regions of the public page with the allocations page
|
||||
- Considered the user’s language in the link of the access table to share the portfolio
|
||||
- Improved the language localization for German (`de`)
|
||||
|
||||
## 2.109.0 - 2024-09-17
|
||||
## 2.109.0 - 2024-09-21
|
||||
|
||||
### Added
|
||||
|
||||
|
@ -2,14 +2,18 @@ import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.
|
||||
import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
|
||||
import { resetHours } from '@ghostfolio/common/helper';
|
||||
import { AccountBalancesResponse, Filter } from '@ghostfolio/common/interfaces';
|
||||
import { UserWithSettings } from '@ghostfolio/common/types';
|
||||
import { DATE_FORMAT, getSum, resetHours } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AccountBalancesResponse,
|
||||
Filter,
|
||||
HistoricalDataItem
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { AccountBalance, Prisma } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { Big } from 'big.js';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
|
||||
import { CreateAccountBalanceDto } from './create-account-balance.dto';
|
||||
|
||||
@ -91,17 +95,55 @@ export class AccountBalanceService {
|
||||
return accountBalance;
|
||||
}
|
||||
|
||||
public async getAccountBalanceItems({
|
||||
filters,
|
||||
userCurrency,
|
||||
userId
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<HistoricalDataItem[]> {
|
||||
const { balances } = await this.getAccountBalances({
|
||||
filters,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts: false // TODO
|
||||
});
|
||||
const accumulatedBalancesByDate: { [date: string]: HistoricalDataItem } =
|
||||
{};
|
||||
const lastBalancesByAccount: { [accountId: string]: Big } = {};
|
||||
|
||||
for (const { accountId, date, valueInBaseCurrency } of balances) {
|
||||
const formattedDate = format(date, DATE_FORMAT);
|
||||
|
||||
lastBalancesByAccount[accountId] = new Big(valueInBaseCurrency);
|
||||
|
||||
const totalBalance = getSum(Object.values(lastBalancesByAccount));
|
||||
|
||||
// Add or update the accumulated balance for this date
|
||||
accumulatedBalancesByDate[formattedDate] = {
|
||||
date: formattedDate,
|
||||
value: totalBalance.toNumber()
|
||||
};
|
||||
}
|
||||
|
||||
return Object.values(accumulatedBalancesByDate);
|
||||
}
|
||||
|
||||
@LogPerformance
|
||||
public async getAccountBalances({
|
||||
filters,
|
||||
user,
|
||||
userCurrency,
|
||||
userId,
|
||||
withExcludedAccounts
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
user: UserWithSettings;
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
withExcludedAccounts?: boolean;
|
||||
}): Promise<AccountBalancesResponse> {
|
||||
const where: Prisma.AccountBalanceWhereInput = { userId: user.id };
|
||||
const where: Prisma.AccountBalanceWhereInput = { userId };
|
||||
|
||||
const accountFilter = filters?.find(({ type }) => {
|
||||
return type === 'ACCOUNT';
|
||||
@ -132,10 +174,11 @@ export class AccountBalanceService {
|
||||
balances: balances.map((balance) => {
|
||||
return {
|
||||
...balance,
|
||||
accountId: balance.Account.id,
|
||||
valueInBaseCurrency: this.exchangeRateDataService.toCurrency(
|
||||
balance.value,
|
||||
balance.Account.currency,
|
||||
user.Settings.settings.baseCurrency
|
||||
userCurrency
|
||||
)
|
||||
};
|
||||
})
|
||||
|
@ -137,7 +137,8 @@ export class AccountController {
|
||||
): Promise<AccountBalancesResponse> {
|
||||
return this.accountBalanceService.getAccountBalances({
|
||||
filters: [{ id, type: 'ACCOUNT' }],
|
||||
user: this.request.user
|
||||
userCurrency: this.request.user.Settings.settings.baseCurrency,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -63,7 +63,6 @@ export class PublicController {
|
||||
{ performance: performanceYtd }
|
||||
] = await Promise.all([
|
||||
this.portfolioService.getDetails({
|
||||
filters: [{ id: 'EQUITY', type: 'ASSET_CLASS' }],
|
||||
impersonationId: access.userId,
|
||||
userId: user.id,
|
||||
withMarkets: true
|
||||
@ -114,6 +113,7 @@ export class PublicController {
|
||||
publicPortfolioResponse.holdings[symbol] = {
|
||||
allocationInPercentage:
|
||||
portfolioPosition.valueInBaseCurrency / totalValue,
|
||||
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
|
||||
countries: hasDetails ? portfolioPosition.countries : [],
|
||||
currency: hasDetails ? portfolioPosition.currency : undefined,
|
||||
dataSource: portfolioPosition.dataSource,
|
||||
|
@ -104,6 +104,10 @@ export abstract class PortfolioCalculator {
|
||||
|
||||
let dateOfFirstActivity = new Date();
|
||||
|
||||
if (this.accountBalanceItems[0]) {
|
||||
dateOfFirstActivity = parseDate(this.accountBalanceItems[0].date);
|
||||
}
|
||||
|
||||
this.activities = activities
|
||||
.map(
|
||||
({
|
||||
@ -269,6 +273,10 @@ export abstract class PortfolioCalculator {
|
||||
)
|
||||
});
|
||||
|
||||
for (const accountBalanceItem of this.accountBalanceItems) {
|
||||
chartDateMap[accountBalanceItem.date] = true;
|
||||
}
|
||||
|
||||
const chartDates = sortBy(Object.keys(chartDateMap), (chartDate) => {
|
||||
return chartDate;
|
||||
});
|
||||
@ -447,9 +455,28 @@ export abstract class PortfolioCalculator {
|
||||
}
|
||||
}
|
||||
|
||||
let lastDate = chartDates[0];
|
||||
const accountBalanceItemsMap = this.accountBalanceItems.reduce(
|
||||
(map, { date, value }) => {
|
||||
map[date] = new Big(value);
|
||||
|
||||
return map;
|
||||
},
|
||||
{} as { [date: string]: Big }
|
||||
);
|
||||
|
||||
const accountBalanceMap: { [date: string]: Big } = {};
|
||||
|
||||
let lastKnownBalance = new Big(0);
|
||||
|
||||
for (const dateString of chartDates) {
|
||||
if (accountBalanceItemsMap[dateString] !== undefined) {
|
||||
// If there's an exact balance for this date, update lastKnownBalance
|
||||
lastKnownBalance = accountBalanceItemsMap[dateString];
|
||||
}
|
||||
|
||||
// Add the most recent balance to the accountBalanceMap
|
||||
accountBalanceMap[dateString] = lastKnownBalance;
|
||||
|
||||
for (const symbol of Object.keys(valuesBySymbol)) {
|
||||
const symbolValues = valuesBySymbol[symbol];
|
||||
|
||||
@ -492,18 +519,7 @@ export abstract class PortfolioCalculator {
|
||||
accumulatedValuesByDate[dateString]
|
||||
?.investmentValueWithCurrencyEffect ?? new Big(0)
|
||||
).add(investmentValueWithCurrencyEffect),
|
||||
totalAccountBalanceWithCurrencyEffect: this.accountBalanceItems.some(
|
||||
({ date }) => {
|
||||
return date === dateString;
|
||||
}
|
||||
)
|
||||
? new Big(
|
||||
this.accountBalanceItems.find(({ date }) => {
|
||||
return date === dateString;
|
||||
}).value
|
||||
)
|
||||
: (accumulatedValuesByDate[lastDate]
|
||||
?.totalAccountBalanceWithCurrencyEffect ?? new Big(0)),
|
||||
totalAccountBalanceWithCurrencyEffect: accountBalanceMap[dateString],
|
||||
totalCurrentValue: (
|
||||
accumulatedValuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
|
||||
).add(currentValue),
|
||||
@ -537,8 +553,6 @@ export abstract class PortfolioCalculator {
|
||||
).add(timeWeightedInvestmentValueWithCurrencyEffect)
|
||||
};
|
||||
}
|
||||
|
||||
lastDate = dateString;
|
||||
}
|
||||
|
||||
const historicalData: HistoricalDataItem[] = Object.entries(
|
||||
@ -733,12 +747,12 @@ export abstract class PortfolioCalculator {
|
||||
timeWeightedInvestmentValue === 0
|
||||
? 0
|
||||
: netPerformanceWithCurrencyEffectSinceStartDate /
|
||||
timeWeightedInvestmentValue,
|
||||
timeWeightedInvestmentValue
|
||||
// TODO: Add net worth with valuables
|
||||
// netWorth: totalCurrentValueWithCurrencyEffect
|
||||
// .plus(totalAccountBalanceWithCurrencyEffect)
|
||||
// .toNumber()
|
||||
netWorth: 0
|
||||
// netWorth: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -815,7 +829,7 @@ export abstract class PortfolioCalculator {
|
||||
endDate: Date;
|
||||
startDate: Date;
|
||||
step: number;
|
||||
}) {
|
||||
}): { [date: string]: true } {
|
||||
// Create a map of all relevant chart dates:
|
||||
// 1. Add transaction point dates
|
||||
let chartDateMap = this.transactionPoints.reduce((result, { date }) => {
|
||||
|
@ -180,10 +180,10 @@ export class TWRPortfolioCalculator extends PortfolioCalculator {
|
||||
let valueAtStartDateWithCurrencyEffect: Big;
|
||||
|
||||
// Clone orders to keep the original values in this.orders
|
||||
let orders: PortfolioOrderItem[] = cloneDeep(this.activities).filter(
|
||||
({ SymbolProfile }) => {
|
||||
let orders: PortfolioOrderItem[] = cloneDeep(
|
||||
this.activities.filter(({ SymbolProfile }) => {
|
||||
return SymbolProfile.symbol === symbol;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (orders.length <= 0) {
|
||||
|
@ -1058,35 +1058,12 @@ export class PortfolioService {
|
||||
const user = await this.userService.user({ id: userId });
|
||||
const userCurrency = this.getUserCurrency(user);
|
||||
|
||||
const accountBalances = await this.accountBalanceService.getAccountBalances(
|
||||
{ filters, user, withExcludedAccounts }
|
||||
);
|
||||
|
||||
let accountBalanceItems: HistoricalDataItem[] = Object.values(
|
||||
// Reduce the array to a map with unique dates as keys
|
||||
accountBalances.balances.reduce(
|
||||
(
|
||||
map: { [date: string]: HistoricalDataItem },
|
||||
{ date, valueInBaseCurrency }
|
||||
) => {
|
||||
const formattedDate = format(date, DATE_FORMAT);
|
||||
|
||||
if (map[formattedDate]) {
|
||||
// If the value exists, add the current value to the existing one
|
||||
map[formattedDate].value += valueInBaseCurrency;
|
||||
} else {
|
||||
// Otherwise, initialize the value for that date
|
||||
map[formattedDate] = {
|
||||
date: formattedDate,
|
||||
value: valueInBaseCurrency
|
||||
};
|
||||
}
|
||||
|
||||
return map;
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
const accountBalanceItems =
|
||||
await this.accountBalanceService.getAccountBalanceItems({
|
||||
filters,
|
||||
userId,
|
||||
userCurrency
|
||||
});
|
||||
|
||||
const { activities } =
|
||||
await this.orderService.getOrdersForPortfolioCalculator({
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module';
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
|
||||
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
|
||||
@ -17,6 +18,7 @@ import { PortfolioSnapshotProcessor } from './portfolio-snapshot.processor';
|
||||
@Module({
|
||||
exports: [BullModule, PortfolioSnapshotService],
|
||||
imports: [
|
||||
AccountBalanceModule,
|
||||
BullModule.registerQueue({
|
||||
name: PORTFOLIO_SNAPSHOT_QUEUE
|
||||
}),
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import {
|
||||
PerformanceCalculationType,
|
||||
@ -24,6 +25,7 @@ import { IPortfolioSnapshotQueueJob } from './interfaces/portfolio-snapshot-queu
|
||||
@Processor(PORTFOLIO_SNAPSHOT_QUEUE)
|
||||
export class PortfolioSnapshotProcessor {
|
||||
public constructor(
|
||||
private readonly accountBalanceService: AccountBalanceService,
|
||||
private readonly calculatorFactory: PortfolioCalculatorFactory,
|
||||
private readonly configurationService: ConfigurationService,
|
||||
private readonly orderService: OrderService,
|
||||
@ -56,7 +58,15 @@ export class PortfolioSnapshotProcessor {
|
||||
userId: job.data.userId
|
||||
});
|
||||
|
||||
const accountBalanceItems =
|
||||
await this.accountBalanceService.getAccountBalanceItems({
|
||||
filters: job.data.filters,
|
||||
userCurrency: job.data.userCurrency,
|
||||
userId: job.data.userId
|
||||
});
|
||||
|
||||
const portfolioCalculator = this.calculatorFactory.createCalculator({
|
||||
accountBalanceItems,
|
||||
activities,
|
||||
calculationType: PerformanceCalculationType.TWR,
|
||||
currency: job.data.userCurrency,
|
||||
|
@ -69,6 +69,7 @@
|
||||
<button mat-menu-item (click)="onCopyToClipboard(element.id)">
|
||||
<ng-container i18n>Copy link to clipboard</ng-container>
|
||||
</button>
|
||||
<hr class="my-0" />
|
||||
}
|
||||
<button mat-menu-item (click)="onDeleteAccess(element.id)">
|
||||
<ng-container i18n>Revoke</ng-container>
|
||||
|
@ -2,7 +2,7 @@ import { CreateAccountBalanceDto } from '@ghostfolio/api/app/account-balance/cre
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AccountBalancesResponse,
|
||||
HistoricalDataItem,
|
||||
@ -27,7 +27,7 @@ import { Router } from '@angular/router';
|
||||
import { Big } from 'big.js';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { isNumber } from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
import { forkJoin, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { AccountDetailDialogParams } from './interfaces/interfaces';
|
||||
@ -87,11 +87,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public ngOnInit() {
|
||||
this.fetchAccount();
|
||||
this.fetchAccountBalances();
|
||||
this.fetchActivities();
|
||||
this.fetchPortfolioHoldings();
|
||||
this.fetchPortfolioPerformance();
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
public onCloneActivity(aActivity: Activity) {
|
||||
@ -111,9 +107,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
.postAccountBalance(accountBalance)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.fetchAccount();
|
||||
this.fetchAccountBalances();
|
||||
this.fetchPortfolioPerformance();
|
||||
this.initialize();
|
||||
});
|
||||
}
|
||||
|
||||
@ -122,9 +116,7 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
.deleteAccountBalance(aId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.fetchAccount();
|
||||
this.fetchAccountBalances();
|
||||
this.fetchPortfolioPerformance();
|
||||
this.initialize();
|
||||
});
|
||||
}
|
||||
|
||||
@ -198,17 +190,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@ -229,6 +210,58 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private fetchChart() {
|
||||
this.isLoadingChart = true;
|
||||
|
||||
forkJoin({
|
||||
accountBalances: this.dataService
|
||||
.fetchAccountBalances(this.data.accountId)
|
||||
.pipe(takeUntil(this.unsubscribeSubject)),
|
||||
portfolioPerformance: this.dataService
|
||||
.fetchPortfolioPerformance({
|
||||
filters: [
|
||||
{
|
||||
id: this.data.accountId,
|
||||
type: 'ACCOUNT'
|
||||
}
|
||||
],
|
||||
range: 'max',
|
||||
withExcludedAccounts: true,
|
||||
withItems: true
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
}).subscribe({
|
||||
error: () => {
|
||||
this.isLoadingChart = false;
|
||||
},
|
||||
next: ({ accountBalances, portfolioPerformance }) => {
|
||||
this.accountBalances = accountBalances.balances;
|
||||
|
||||
if (portfolioPerformance.chart.length > 0) {
|
||||
this.historicalDataItems = portfolioPerformance.chart.map(
|
||||
({ date, netWorth, netWorthInPercentage }) => ({
|
||||
date,
|
||||
value: isNumber(netWorth) ? netWorth : netWorthInPercentage
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.historicalDataItems = this.accountBalances.map(
|
||||
({ date, valueInBaseCurrency }) => {
|
||||
return {
|
||||
date: format(date, DATE_FORMAT),
|
||||
value: valueInBaseCurrency
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
this.isLoadingChart = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private fetchPortfolioHoldings() {
|
||||
this.dataService
|
||||
.fetchPortfolioHoldings({
|
||||
@ -247,36 +280,11 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
private fetchPortfolioPerformance() {
|
||||
this.isLoadingChart = true;
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioPerformance({
|
||||
filters: [
|
||||
{
|
||||
id: this.data.accountId,
|
||||
type: 'ACCOUNT'
|
||||
}
|
||||
],
|
||||
range: 'max',
|
||||
withExcludedAccounts: true,
|
||||
withItems: true
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ chart }) => {
|
||||
this.historicalDataItems = chart.map(
|
||||
({ date, netWorth, netWorthInPercentage }) => {
|
||||
return {
|
||||
date,
|
||||
value: isNumber(netWorth) ? netWorth : netWorthInPercentage
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
this.isLoadingChart = false;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
private initialize() {
|
||||
this.fetchAccount();
|
||||
this.fetchActivities();
|
||||
this.fetchChart();
|
||||
this.fetchPortfolioHoldings();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -20,20 +20,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TODO
|
||||
<div class="chart-container mb-3">
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isInPercent]="
|
||||
data.hasImpersonationId || user.settings.isRestrictedView
|
||||
"
|
||||
[isLoading]="isLoadingChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
/>
|
||||
</div>
|
||||
-->
|
||||
@if (user?.settings?.isExperimentalFeatures) {
|
||||
<div class="chart-container mb-3">
|
||||
<gf-investment-chart
|
||||
class="h-100"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isInPercent]="
|
||||
data.hasImpersonationId || user.settings.isRestrictedView
|
||||
"
|
||||
[isLoading]="isLoadingChart"
|
||||
[locale]="user?.settings?.locale"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="mb-3 row">
|
||||
<div class="col-6 mb-3">
|
||||
|
@ -278,6 +278,7 @@
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.transactionCount > 0"
|
||||
|
@ -175,6 +175,7 @@
|
||||
<button mat-menu-item (click)="onExecuteJob(element.id)">
|
||||
<ng-container i18n>Execute Job</ng-container>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
<button mat-menu-item (click)="onDeleteJob(element.id)">
|
||||
<ng-container i18n>Delete Job</ng-container>
|
||||
</button>
|
||||
|
@ -197,6 +197,7 @@
|
||||
<button mat-menu-item (click)="onGatherProfileData()">
|
||||
<ng-container i18n>Gather Profile Data</ng-container>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="!selection.hasValue()"
|
||||
@ -230,6 +231,7 @@
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</a>
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="
|
||||
|
@ -2,7 +2,10 @@ import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/con
|
||||
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
|
||||
import { getCurrencyFromSymbol, isCurrency } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
getCurrencyFromSymbol,
|
||||
isDerivedCurrency
|
||||
} from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AssetProfileIdentifier,
|
||||
AdminMarketDataItem
|
||||
@ -74,7 +77,7 @@ export class AdminMarketDataService {
|
||||
return (
|
||||
activitiesCount === 0 &&
|
||||
!isBenchmark &&
|
||||
!isCurrency(getCurrencyFromSymbol(symbol)) &&
|
||||
!isDerivedCurrency(getCurrencyFromSymbol(symbol)) &&
|
||||
!symbol.startsWith(ghostfolioScraperApiSymbolPrefix)
|
||||
);
|
||||
}
|
||||
|
@ -44,6 +44,7 @@
|
||||
>
|
||||
<ng-container i18n>Gather Profile Data</ng-container>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
mat-menu-item
|
||||
type="button"
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
@ -15,6 +18,10 @@ import {
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { uniq } from 'lodash';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
import { CreateAssetProfileDialogMode } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
@ -25,17 +32,29 @@ import { MatDialogRef } from '@angular/material/dialog';
|
||||
})
|
||||
export class CreateAssetProfileDialog implements OnInit, OnDestroy {
|
||||
public createAssetProfileForm: FormGroup;
|
||||
public mode: 'auto' | 'manual';
|
||||
public mode: CreateAssetProfileDialogMode;
|
||||
|
||||
private customCurrencies: string[];
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
public readonly adminService: AdminService,
|
||||
private readonly changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly dataService: DataService,
|
||||
public readonly dialogRef: MatDialogRef<CreateAssetProfileDialog>,
|
||||
public readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
public ngOnInit() {
|
||||
this.initializeCustomCurrencies();
|
||||
|
||||
this.createAssetProfileForm = this.formBuilder.group(
|
||||
{
|
||||
addCurrency: new FormControl(null, [
|
||||
Validators.maxLength(3),
|
||||
Validators.minLength(3),
|
||||
Validators.required
|
||||
]),
|
||||
addSymbol: new FormControl(null, [Validators.required]),
|
||||
searchSymbol: new FormControl(null, [Validators.required])
|
||||
},
|
||||
@ -51,34 +70,75 @@ export class CreateAssetProfileDialog implements OnInit, OnDestroy {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
public onRadioChange(mode: 'auto' | 'manual') {
|
||||
public onRadioChange(mode: CreateAssetProfileDialogMode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
this.mode === 'auto'
|
||||
? this.dialogRef.close({
|
||||
dataSource:
|
||||
this.createAssetProfileForm.get('searchSymbol').value.dataSource,
|
||||
symbol: this.createAssetProfileForm.get('searchSymbol').value.symbol
|
||||
if (this.mode === 'auto') {
|
||||
this.dialogRef.close({
|
||||
dataSource:
|
||||
this.createAssetProfileForm.get('searchSymbol').value.dataSource,
|
||||
symbol: this.createAssetProfileForm.get('searchSymbol').value.symbol
|
||||
});
|
||||
} else if (this.mode === 'currency') {
|
||||
const currency = this.createAssetProfileForm
|
||||
.get('addCurrency')
|
||||
.value.toUpperCase();
|
||||
|
||||
const currencies = uniq([...this.customCurrencies, currency]);
|
||||
|
||||
this.dataService
|
||||
.putAdminSetting(PROPERTY_CURRENCIES, {
|
||||
value: JSON.stringify(currencies)
|
||||
})
|
||||
: this.dialogRef.close({
|
||||
dataSource: 'MANUAL',
|
||||
symbol: this.createAssetProfileForm.get('addSymbol').value
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dialogRef.close();
|
||||
});
|
||||
} else if (this.mode === 'manual') {
|
||||
this.dialogRef.close({
|
||||
dataSource: 'MANUAL',
|
||||
symbol: this.createAssetProfileForm.get('addSymbol').value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public ngOnDestroy() {}
|
||||
public get showCurrencyErrorMessage() {
|
||||
const addCurrencyFormControl =
|
||||
this.createAssetProfileForm.get('addCurrency');
|
||||
|
||||
if (
|
||||
addCurrencyFormControl.hasError('maxlength') ||
|
||||
addCurrencyFormControl.hasError('minlength')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private atLeastOneValid(control: AbstractControl): ValidationErrors {
|
||||
const addCurrencyControl = control.get('addCurrency');
|
||||
const addSymbolControl = control.get('addSymbol');
|
||||
const searchSymbolControl = control.get('searchSymbol');
|
||||
|
||||
if (addSymbolControl.valid && searchSymbolControl.valid) {
|
||||
if (
|
||||
addCurrencyControl.valid &&
|
||||
addSymbolControl.valid &&
|
||||
searchSymbolControl.valid
|
||||
) {
|
||||
return { atLeastOneValid: true };
|
||||
}
|
||||
|
||||
if (
|
||||
addCurrencyControl.valid ||
|
||||
!addCurrencyControl ||
|
||||
addSymbolControl.valid ||
|
||||
!addSymbolControl ||
|
||||
searchSymbolControl.valid ||
|
||||
@ -89,4 +149,15 @@ export class CreateAssetProfileDialog implements OnInit, OnDestroy {
|
||||
|
||||
return { atLeastOneValid: true };
|
||||
}
|
||||
|
||||
private initializeCustomCurrencies() {
|
||||
this.adminService
|
||||
.fetchAdminData()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ settings }) => {
|
||||
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,9 @@
|
||||
<mat-radio-button class="ml-3" name="manual" value="manual">
|
||||
</mat-radio-button>
|
||||
<label class="m-0" for="manual" i18n>Add Manually</label>
|
||||
<mat-radio-button class="ml-3" name="currency" value="currency">
|
||||
</mat-radio-button>
|
||||
<label class="m-0" for="currency" i18n>Add Currency</label>
|
||||
</mat-radio-group>
|
||||
</div>
|
||||
|
||||
@ -37,6 +40,16 @@
|
||||
<input formControlName="addSymbol" matInput />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
} @else if (mode === 'currency') {
|
||||
<div>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>Currency</mat-label>
|
||||
<input formControlName="addCurrency" matInput />
|
||||
@if (showCurrencyErrorMessage) {
|
||||
<mat-error i18n>Oops! Invalid currency.</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex justify-content-end" mat-dialog-actions>
|
||||
|
@ -2,3 +2,5 @@ export interface CreateAssetProfileDialogParams {
|
||||
deviceType: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
export type CreateAssetProfileDialogMode = 'auto' | 'currency' | 'manual';
|
||||
|
@ -126,7 +126,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
|
||||
if (currency) {
|
||||
if (currency.length === 3) {
|
||||
const currencies = uniq([...this.customCurrencies, currency]);
|
||||
const currencies = uniq([
|
||||
...this.customCurrencies,
|
||||
currency.toUpperCase()
|
||||
]);
|
||||
this.putAdminSetting({ key: PROPERTY_CURRENCIES, value: currencies });
|
||||
} else {
|
||||
this.notificationService.alert({
|
||||
|
@ -79,6 +79,7 @@
|
||||
</span>
|
||||
</a>
|
||||
@if (customCurrencies.includes(exchangeRate.label2)) {
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="onDeleteCurrency(exchangeRate.label2)"
|
||||
|
@ -92,6 +92,7 @@
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.accountCount > 0"
|
||||
|
@ -71,6 +71,7 @@
|
||||
<span i18n>Edit</span>
|
||||
</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="element.activityCount > 0"
|
||||
|
@ -218,6 +218,7 @@
|
||||
<span i18n>Impersonate User</span>
|
||||
</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
}
|
||||
<button
|
||||
mat-menu-item
|
||||
|
@ -4,7 +4,7 @@
|
||||
<article>
|
||||
<div class="mb-4 text-center">
|
||||
<h1 class="mb-1">Hacktoberfest 2024</h1>
|
||||
<div class="mb-3 text-muted"><small>2024-09-21</small></div>
|
||||
<div class="mb-3 text-muted"><small>2024-09-24</small></div>
|
||||
<img
|
||||
alt="Hacktoberfest 2024 with Ghostfolio Teaser"
|
||||
class="rounded w-100"
|
||||
@ -21,7 +21,7 @@
|
||||
>third time</a
|
||||
>
|
||||
and look forward to connecting with new, enthusiastic open-source
|
||||
contributors. Hacktoberfest is a a month-long celebration of all
|
||||
contributors. Hacktoberfest is a month-long celebration of all
|
||||
things open-source: projects, their maintainers, and the entire
|
||||
community of contributors. Every October, open source maintainers
|
||||
around the globe dedicate extra time to support new contributors
|
||||
|
@ -18,7 +18,7 @@
|
||||
>
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<div class="h6 m-0 text-truncate">Hacktoberfest 2024</div>
|
||||
<div class="d-flex text-muted">2024-09-21</div>
|
||||
<div class="d-flex text-muted">2024-09-24</div>
|
||||
</div>
|
||||
<div class="align-items-center d-flex">
|
||||
<ion-icon
|
||||
|
@ -52,8 +52,10 @@
|
||||
</p>
|
||||
<ol>
|
||||
<li>Go to the <i>Admin Control</i> panel</li>
|
||||
<li>Click on the <i>Add Currency</i> button</li>
|
||||
<li>Insert e.g. <code>EUR</code> in the prompt</li>
|
||||
<li>Go to the <i>Market Data</i> section</li>
|
||||
<li>Click on the <i>+</i> button</li>
|
||||
<li>Switch to <i>Add Currency</i></li>
|
||||
<li>Insert e.g. <code>EUR</code> for Euro</li>
|
||||
</ol>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
@ -9,6 +9,7 @@ import { Market } from '@ghostfolio/common/types';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { AssetClass } from '@prisma/client';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { isNumber } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
@ -145,69 +146,77 @@ export class PublicPageComponent implements OnInit {
|
||||
value: position.allocationInPercentage
|
||||
};
|
||||
|
||||
if (position.countries.length > 0) {
|
||||
this.markets.developedMarkets.value +=
|
||||
position.markets.developedMarkets * position.valueInBaseCurrency;
|
||||
this.markets.emergingMarkets.value +=
|
||||
position.markets.emergingMarkets * position.valueInBaseCurrency;
|
||||
this.markets.otherMarkets.value +=
|
||||
position.markets.otherMarkets * position.valueInBaseCurrency;
|
||||
if (position.assetClass !== AssetClass.LIQUIDITY) {
|
||||
// Prepare analysis data by continents, countries, holdings and sectors except for liquidity
|
||||
|
||||
for (const country of position.countries) {
|
||||
const { code, continent, name, weight } = country;
|
||||
if (position.countries.length > 0) {
|
||||
this.markets.developedMarkets.value +=
|
||||
position.markets.developedMarkets * position.valueInBaseCurrency;
|
||||
this.markets.emergingMarkets.value +=
|
||||
position.markets.emergingMarkets * position.valueInBaseCurrency;
|
||||
this.markets.otherMarkets.value +=
|
||||
position.markets.otherMarkets * position.valueInBaseCurrency;
|
||||
|
||||
if (this.continents[continent]?.value) {
|
||||
this.continents[continent].value +=
|
||||
weight * position.valueInBaseCurrency;
|
||||
} else {
|
||||
this.continents[continent] = {
|
||||
name: continent,
|
||||
value:
|
||||
weight *
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||
};
|
||||
for (const country of position.countries) {
|
||||
const { code, continent, name, weight } = country;
|
||||
|
||||
if (this.continents[continent]?.value) {
|
||||
this.continents[continent].value +=
|
||||
weight * position.valueInBaseCurrency;
|
||||
} else {
|
||||
this.continents[continent] = {
|
||||
name: continent,
|
||||
value:
|
||||
weight *
|
||||
this.publicPortfolioDetails.holdings[symbol]
|
||||
.valueInBaseCurrency
|
||||
};
|
||||
}
|
||||
|
||||
if (this.countries[code]?.value) {
|
||||
this.countries[code].value +=
|
||||
weight * position.valueInBaseCurrency;
|
||||
} else {
|
||||
this.countries[code] = {
|
||||
name,
|
||||
value:
|
||||
weight *
|
||||
this.publicPortfolioDetails.holdings[symbol]
|
||||
.valueInBaseCurrency
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.continents[UNKNOWN_KEY].value +=
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
||||
|
||||
if (this.countries[code]?.value) {
|
||||
this.countries[code].value += weight * position.valueInBaseCurrency;
|
||||
} else {
|
||||
this.countries[code] = {
|
||||
name,
|
||||
value:
|
||||
weight *
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||
};
|
||||
}
|
||||
this.countries[UNKNOWN_KEY].value +=
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
||||
|
||||
this.markets[UNKNOWN_KEY].value +=
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
||||
}
|
||||
} else {
|
||||
this.continents[UNKNOWN_KEY].value +=
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
||||
|
||||
this.countries[UNKNOWN_KEY].value +=
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
||||
if (position.sectors.length > 0) {
|
||||
for (const sector of position.sectors) {
|
||||
const { name, weight } = sector;
|
||||
|
||||
this.markets[UNKNOWN_KEY].value +=
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
||||
}
|
||||
|
||||
if (position.sectors.length > 0) {
|
||||
for (const sector of position.sectors) {
|
||||
const { name, weight } = sector;
|
||||
|
||||
if (this.sectors[name]?.value) {
|
||||
this.sectors[name].value += weight * position.valueInBaseCurrency;
|
||||
} else {
|
||||
this.sectors[name] = {
|
||||
name,
|
||||
value:
|
||||
weight *
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency
|
||||
};
|
||||
if (this.sectors[name]?.value) {
|
||||
this.sectors[name].value += weight * position.valueInBaseCurrency;
|
||||
} else {
|
||||
this.sectors[name] = {
|
||||
name,
|
||||
value:
|
||||
weight *
|
||||
this.publicPortfolioDetails.holdings[symbol]
|
||||
.valueInBaseCurrency
|
||||
};
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.sectors[UNKNOWN_KEY].value +=
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
||||
}
|
||||
} else {
|
||||
this.sectors[UNKNOWN_KEY].value +=
|
||||
this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency;
|
||||
}
|
||||
|
||||
this.symbols[prettifySymbol(symbol)] = {
|
||||
|
@ -2391,7 +2391,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="a3d148b40a389fda0665eb583c9e434ec5ee1ced" datatype="html">
|
||||
<source> Ghostfolio empowers you to keep track of your wealth. </source>
|
||||
<target state="translated">Ghostfolio verschafft Ihnen den Überblick über Ihr Vermögen.</target>
|
||||
<target state="translated">Ghostfolio verschafft dir den Überblick über dein Vermögen.</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/public/public-page.html</context>
|
||||
<context context-type="linenumber">215</context>
|
||||
@ -4919,7 +4919,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="c8ef12032b654cfd51b9ae1082fde84247945e03" datatype="html">
|
||||
<source> Protect your <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>assets<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/>. Refine your <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>personal investment strategy<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/>. </source>
|
||||
<target state="translated"> Schützen Sie Ihr <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>Vermögen<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/>. Optimieren Sie Ihre <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>persönliche Anlagestrategie<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/>. </target>
|
||||
<target state="translated"> Schütze dein <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>Vermögen<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/>. Optimiere deine <x id="START_TAG_STRONG" ctype="x-strong" equiv-text="<strong>"/>persönliche Anlagestrategie<x id="CLOSE_TAG_STRONG" ctype="x-strong" equiv-text="</strong>"/>. </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/landing/landing-page.html</context>
|
||||
<context context-type="linenumber">225</context>
|
||||
@ -5728,7 +5728,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="2fc47ae80c47144eb6250979fe927a010da3aee5" datatype="html">
|
||||
<source>Choose or drop a file here</source>
|
||||
<target state="translated">Wählen Sie eine Datei aus oder ziehen Sie sie hierhin</target>
|
||||
<target state="translated">Wähle eine Datei aus oder ziehe sie hierhin</target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html</context>
|
||||
<context context-type="linenumber">84</context>
|
||||
@ -5984,7 +5984,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="metaDescription" datatype="html">
|
||||
<source> Ghostfolio is a personal finance dashboard to keep track of your net worth including cash, stocks, ETFs and cryptocurrencies across multiple platforms. </source>
|
||||
<target state="translated"> Mit dem Finanz-Dashboard Ghostfolio können Sie Ihr Vermögen in Cash, Aktien, ETFs und Kryptowährungen über mehrere Finanzinstitute überwachen. </target>
|
||||
<target state="translated"> Mit dem Finanz-Dashboard Ghostfolio kannst du dein Vermögen in Cash, Aktien, ETFs und Kryptowährungen über mehrere Finanzinstitute überwachen. </target>
|
||||
<context-group purpose="location">
|
||||
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
|
||||
<context context-type="linenumber">4</context>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AccountBalance } from '@prisma/client';
|
||||
|
||||
export interface AccountBalancesResponse {
|
||||
balances: (Pick<AccountBalance, 'date' | 'id' | 'value'> & {
|
||||
balances: (Pick<AccountBalance, 'accountId' | 'date' | 'id' | 'value'> & {
|
||||
valueInBaseCurrency: number;
|
||||
})[];
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 {
|
||||
[symbol: string]: Pick<
|
||||
PortfolioPosition,
|
||||
| 'allocationInPercentage'
|
||||
| 'assetClass'
|
||||
| 'countries'
|
||||
| 'currency'
|
||||
| 'dataSource'
|
||||
|
@ -55,6 +55,7 @@
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
class="align-items-center d-flex"
|
||||
mat-menu-item
|
||||
@ -445,6 +446,7 @@
|
||||
<span i18n>Export Draft as ICS</span>
|
||||
</span>
|
||||
</button>
|
||||
<hr class="m-0" />
|
||||
<button
|
||||
mat-menu-item
|
||||
[disabled]="!hasPermissionToDeleteActivity"
|
||||
|
87
package-lock.json
generated
87
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.108.0",
|
||||
"version": "2.110.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "ghostfolio",
|
||||
"version": "2.108.0",
|
||||
"version": "2.110.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
@ -40,7 +40,7 @@
|
||||
"@nestjs/platform-express": "10.1.3",
|
||||
"@nestjs/schedule": "3.0.2",
|
||||
"@nestjs/serve-static": "4.0.0",
|
||||
"@prisma/client": "5.19.1",
|
||||
"@prisma/client": "5.20.0",
|
||||
"@simplewebauthn/browser": "9.0.1",
|
||||
"@simplewebauthn/server": "9.0.3",
|
||||
"@stripe/stripe-js": "3.5.0",
|
||||
@ -84,7 +84,7 @@
|
||||
"passport": "0.7.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.1",
|
||||
"prisma": "5.19.1",
|
||||
"prisma": "5.20.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.5.6",
|
||||
"stripe": "15.11.0",
|
||||
@ -158,7 +158,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"tslib": "2.6.0",
|
||||
"typescript": "5.5.3",
|
||||
"webpack-bundle-analyzer": "4.10.1"
|
||||
"webpack-bundle-analyzer": "4.10.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@ -9646,9 +9646,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.19.1.tgz",
|
||||
"integrity": "sha512-x30GFguInsgt+4z5I4WbkZP2CGpotJMUXy+Gl/aaUjHn2o1DnLYNTA+q9XdYmAQZM8fIIkvUiA2NpgosM3fneg==",
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.20.0.tgz",
|
||||
"integrity": "sha512-CLv55ZuMuUawMsxoqxGtLT3bEZoa2W8L3Qnp6rDIFWy+ZBrUcOFKdoeGPSnbBqxc3SkdxJrF+D1veN/WNynZYA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@ -9664,48 +9664,48 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.19.1.tgz",
|
||||
"integrity": "sha512-lAG6A6QnG2AskAukIEucYJZxxcSqKsMK74ZFVfCTOM/7UiyJQi48v6TQ47d6qKG3LbMslqOvnTX25dj/qvclGg==",
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.20.0.tgz",
|
||||
"integrity": "sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.19.1.tgz",
|
||||
"integrity": "sha512-kR/PoxZDrfUmbbXqqb8SlBBgCjvGaJYMCOe189PEYzq9rKqitQ2fvT/VJ8PDSe8tTNxhc2KzsCfCAL+Iwm/7Cg==",
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.20.0.tgz",
|
||||
"integrity": "sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.19.1",
|
||||
"@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
|
||||
"@prisma/fetch-engine": "5.19.1",
|
||||
"@prisma/get-platform": "5.19.1"
|
||||
"@prisma/debug": "5.20.0",
|
||||
"@prisma/engines-version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284",
|
||||
"@prisma/fetch-engine": "5.20.0",
|
||||
"@prisma/get-platform": "5.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3.tgz",
|
||||
"integrity": "sha512-xR6rt+z5LnNqTP5BBc+8+ySgf4WNMimOKXRn6xfNRDSpHvbOEmd7+qAOmzCrddEc4Cp8nFC0txU14dstjH7FXA==",
|
||||
"version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz",
|
||||
"integrity": "sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.19.1.tgz",
|
||||
"integrity": "sha512-pCq74rtlOVJfn4pLmdJj+eI4P7w2dugOnnTXpRilP/6n5b2aZiA4ulJlE0ddCbTPkfHmOL9BfaRgA8o+1rfdHw==",
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz",
|
||||
"integrity": "sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.19.1",
|
||||
"@prisma/engines-version": "5.19.1-2.69d742ee20b815d88e17e54db4a2a7a3b30324e3",
|
||||
"@prisma/get-platform": "5.19.1"
|
||||
"@prisma/debug": "5.20.0",
|
||||
"@prisma/engines-version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284",
|
||||
"@prisma/get-platform": "5.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.19.1.tgz",
|
||||
"integrity": "sha512-sCeoJ+7yt0UjnR+AXZL7vXlg5eNxaFOwC23h0KvW1YIXUoa7+W2ZcAUhoEQBmJTW4GrFqCuZ8YSP0mkDa4k3Zg==",
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.20.0.tgz",
|
||||
"integrity": "sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.19.1"
|
||||
"@prisma/debug": "5.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@redis/bloom": {
|
||||
@ -28820,13 +28820,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.19.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.19.1.tgz",
|
||||
"integrity": "sha512-c5K9MiDaa+VAAyh1OiYk76PXOme9s3E992D7kvvIOhCrNsBQfy2mP2QAQtX0WNj140IgG++12kwZpYB9iIydNQ==",
|
||||
"version": "5.20.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.20.0.tgz",
|
||||
"integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.19.1"
|
||||
"@prisma/engines": "5.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
@ -33666,10 +33666,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer": {
|
||||
"version": "4.10.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz",
|
||||
"integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==",
|
||||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz",
|
||||
"integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "0.5.7",
|
||||
"acorn": "^8.0.4",
|
||||
@ -33679,7 +33680,6 @@
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"gzip-size": "^6.0.0",
|
||||
"html-escaper": "^2.0.2",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"opener": "^1.5.2",
|
||||
"picocolors": "^1.0.0",
|
||||
"sirv": "^2.0.3",
|
||||
@ -33713,15 +33713,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer/node_modules/is-plain-object": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
|
||||
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-bundle-analyzer/node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ghostfolio",
|
||||
"version": "2.109.0",
|
||||
"version": "2.110.0",
|
||||
"homepage": "https://ghostfol.io",
|
||||
"license": "AGPL-3.0",
|
||||
"repository": "https://github.com/ghostfolio/ghostfolio",
|
||||
@ -84,7 +84,7 @@
|
||||
"@nestjs/platform-express": "10.1.3",
|
||||
"@nestjs/schedule": "3.0.2",
|
||||
"@nestjs/serve-static": "4.0.0",
|
||||
"@prisma/client": "5.19.1",
|
||||
"@prisma/client": "5.20.0",
|
||||
"@simplewebauthn/browser": "9.0.1",
|
||||
"@simplewebauthn/server": "9.0.3",
|
||||
"@stripe/stripe-js": "3.5.0",
|
||||
@ -128,7 +128,7 @@
|
||||
"passport": "0.7.0",
|
||||
"passport-google-oauth20": "2.0.0",
|
||||
"passport-jwt": "4.0.1",
|
||||
"prisma": "5.19.1",
|
||||
"prisma": "5.20.0",
|
||||
"reflect-metadata": "0.1.13",
|
||||
"rxjs": "7.5.6",
|
||||
"stripe": "15.11.0",
|
||||
@ -202,7 +202,7 @@
|
||||
"ts-node": "10.9.2",
|
||||
"tslib": "2.6.0",
|
||||
"typescript": "5.5.3",
|
||||
"webpack-bundle-analyzer": "4.10.1"
|
||||
"webpack-bundle-analyzer": "4.10.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
|
Loading…
x
Reference in New Issue
Block a user