Compare commits

..

9 Commits

Author SHA1 Message Date
fefee11301 Release 2.55.0 (#3034) 2024-02-22 20:26:39 +01:00
40836b745b Feature/improve validation for currency in endpoints (#3030)
* Improve validation for currency

* Update changelog
2024-02-22 20:25:22 +01:00
07eabac059 Feature/add missing database indexes part 2 (#3033)
* Add missing database indexes (for orderBy and where clauses)

* Update changelog
2024-02-22 20:21:50 +01:00
48b412cfb8 Feature/harmonize setting of default locale (#3032)
* Harmonize setting of default locale

* Update changelog
2024-02-22 20:10:27 +01:00
b62488628c Prettify markup (#3029) 2024-02-21 09:58:15 +01:00
982c71c728 Feature/set angular parser in prettierrc (#3028)
* Set parser to angular

* Update changelog
2024-02-20 19:54:03 +01:00
5aa16a3779 Release 2.54.0 (#3027) 2024-02-19 19:47:37 +01:00
93de25e5b6 Feature/add missing database indexes (#3026)
* Add missing database indexes

* Update changelog
2024-02-19 19:45:52 +01:00
9acdb41aa2 Refactor params to object (#2987) 2024-02-19 19:32:10 +01:00
82 changed files with 1323 additions and 888 deletions

View File

@ -12,6 +12,12 @@
"importOrder": ["^@ghostfolio/(.*)$", "<THIRD_PARTY_MODULES>", "^[./]"],
"importOrderSeparation": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
},
{
"files": "*.ts",
"options": {

View File

@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.55.0 - 2024-02-22
### Added
- Added indexes for `alias`, `granteeUserId` and `userId` to the access database table
- Added indexes for `currency`, `name` and `userId` to the account database table
- Added an index for `accountId`, `date` and `updatedAt` to the account balance database table
- Added an index for `userId` to the auth device database table
- Added an index for `marketPrice` and `state` to the market data database table
- Added indexes for `date`, `isDraft` and `userId` to the order database table
- Added an index for `name` to the platform database table
- Added indexes for `assetClass`, `currency`, `dataSource`, `isin`, `name` and `symbol` to the symbol profile database table
- Added an index for `userId` to the subscription database table
- Added an index for `name` to the tag database table
- Added indexes for `accessToken`, `createdAt`, `provider`, `role` and `thirdPartyId` to the user database table
### Changed
- Improved the validation for `currency` in various endpoints
- Harmonized the setting of a default locale in various components
- Set the parser to `angular` in the `prettier` options
## 2.54.0 - 2024-02-19
### Added
- Added an index for `id` to the account database table
- Added indexes for `dataSource` and `date` to the market data database table
- Added an index for `accountId` to the order database table
## 2.53.1 - 2024-02-18
### Added

View File

@ -21,10 +21,8 @@ export class AccountService {
public async account({
id_userId
}: Prisma.AccountWhereUniqueInput): Promise<Account | null> {
const { id, userId } = id_userId;
const [account] = await this.accounts({
where: { id, userId }
where: id_userId
});
return account;

View File

@ -1,6 +1,7 @@
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsBoolean,
IsISO4217CurrencyCode,
IsNumber,
IsOptional,
IsString,
@ -19,7 +20,7 @@ export class CreateAccountDto {
)
comment?: string;
@IsString()
@IsISO4217CurrencyCode()
currency: string;
@IsOptional()

View File

@ -1,6 +1,7 @@
import { Transform, TransformFnParams } from 'class-transformer';
import {
IsBoolean,
IsISO4217CurrencyCode,
IsNumber,
IsOptional,
IsString,
@ -19,7 +20,7 @@ export class UpdateAccountDto {
)
comment?: string;
@IsString()
@IsISO4217CurrencyCode()
currency: string;
@IsString()

View File

@ -2,6 +2,7 @@ import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
import {
IsArray,
IsEnum,
IsISO4217CurrencyCode,
IsObject,
IsOptional,
IsString
@ -24,7 +25,7 @@ export class UpdateAssetProfileDto {
@IsOptional()
countries?: Prisma.InputJsonArray;
@IsString()
@IsISO4217CurrencyCode()
@IsOptional()
currency?: string;

View File

@ -10,6 +10,7 @@ import {
IsArray,
IsBoolean,
IsEnum,
IsISO4217CurrencyCode,
IsISO8601,
IsNumber,
IsOptional,
@ -38,7 +39,7 @@ export class CreateOrderDto {
)
comment?: string;
@IsString()
@IsISO4217CurrencyCode()
currency: string;
@IsOptional()

View File

@ -9,6 +9,7 @@ import { Transform, TransformFnParams } from 'class-transformer';
import {
IsArray,
IsEnum,
IsISO4217CurrencyCode,
IsISO8601,
IsNumber,
IsOptional,
@ -37,7 +38,7 @@ export class UpdateOrderDto {
)
comment?: string;
@IsString()
@IsISO4217CurrencyCode()
currency: string;
@IsString()

View File

@ -7,6 +7,7 @@ import type {
import {
IsArray,
IsBoolean,
IsISO4217CurrencyCode,
IsISO8601,
IsIn,
IsNumber,
@ -19,8 +20,8 @@ export class UpdateUserSettingDto {
@IsOptional()
annualInterestRate?: number;
@IsISO4217CurrencyCode()
@IsOptional()
@IsString()
baseCurrency?: string;
@IsString()

View File

@ -438,7 +438,7 @@ export class UserService {
settings
},
where: {
userId: userId
userId
}
});

View File

@ -37,12 +37,14 @@ export class AlphaVantageService implements DataProviderInterface {
return !!this.configurationService.get('API_KEY_ALPHA_VANTAGE');
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
return {
dataSource: this.getName(),
symbol: aSymbol
symbol,
dataSource: this.getName()
};
}

View File

@ -52,15 +52,17 @@ export class CoinGeckoService implements DataProviderInterface {
return true;
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
const response: Partial<SymbolProfile> = {
symbol,
assetClass: AssetClass.CASH,
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
currency: DEFAULT_CURRENCY,
dataSource: this.getName(),
symbol: aSymbol
dataSource: this.getName()
};
try {
@ -70,7 +72,7 @@ export class CoinGeckoService implements DataProviderInterface {
abortController.abort();
}, this.configurationService.get('REQUEST_TIMEOUT'));
const { name } = await got(`${this.apiUrl}/coins/${aSymbol}`, {
const { name } = await got(`${this.apiUrl}/coins/${symbol}`, {
headers: this.headers,
// @ts-ignore
signal: abortController.signal
@ -81,7 +83,7 @@ export class CoinGeckoService implements DataProviderInterface {
let message = error;
if (error?.code === 'ABORT_ERR') {
message = `RequestError: The operation to get the asset profile for ${aSymbol} was aborted because the request to the data provider took more than ${this.configurationService.get(
message = `RequestError: The operation to get the asset profile for ${symbol} was aborted because the request to the data provider took more than ${this.configurationService.get(
'REQUEST_TIMEOUT'
)}ms`;
}

View File

@ -92,7 +92,9 @@ export class DataProviderService {
for (const symbol of symbols) {
const promise = Promise.resolve(
this.getDataProvider(DataSource[dataSource]).getAssetProfile(symbol)
this.getDataProvider(DataSource[dataSource]).getAssetProfile({
symbol
})
);
promises.push(

View File

@ -46,19 +46,21 @@ export class EodHistoricalDataService implements DataProviderInterface {
return true;
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
const [searchResult] = await this.getSearchResult(aSymbol);
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
const [searchResult] = await this.getSearchResult(symbol);
return {
symbol,
assetClass: searchResult?.assetClass,
assetSubClass: searchResult?.assetSubClass,
currency: this.convertCurrency(searchResult?.currency),
dataSource: this.getName(),
isin: searchResult?.isin,
name: searchResult?.name,
symbol: aSymbol
name: searchResult?.name
};
}

View File

@ -37,12 +37,14 @@ export class FinancialModelingPrepService implements DataProviderInterface {
return true;
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
return {
dataSource: this.getName(),
symbol: aSymbol
symbol,
dataSource: this.getName()
};
}

View File

@ -33,12 +33,14 @@ export class GoogleSheetsService implements DataProviderInterface {
return true;
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
return {
dataSource: this.getName(),
symbol: aSymbol
symbol,
dataSource: this.getName()
};
}

View File

@ -11,7 +11,11 @@ import { DataSource, SymbolProfile } from '@prisma/client';
export interface DataProviderInterface {
canHandle(symbol: string): boolean;
getAssetProfile(aSymbol: string): Promise<Partial<SymbolProfile>>;
getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>>;
getDataProviderInfo(): DataProviderInfo;

View File

@ -43,16 +43,18 @@ export class ManualService implements DataProviderInterface {
return true;
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
const assetProfile: Partial<SymbolProfile> = {
dataSource: this.getName(),
symbol: aSymbol
symbol,
dataSource: this.getName()
};
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([
{ dataSource: this.getName(), symbol: aSymbol }
{ symbol, dataSource: this.getName() }
]);
if (symbolProfile) {

View File

@ -30,12 +30,14 @@ export class RapidApiService implements DataProviderInterface {
return !!this.configurationService.get('API_KEY_RAPID_API');
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
return {
dataSource: this.getName(),
symbol: aSymbol
symbol,
dataSource: this.getName()
};
}

View File

@ -33,11 +33,13 @@ export class YahooFinanceService implements DataProviderInterface {
return true;
}
public async getAssetProfile(
aSymbol: string
): Promise<Partial<SymbolProfile>> {
const { assetClass, assetSubClass, currency, name, symbol } =
await this.yahooFinanceDataEnhancerService.getAssetProfile(aSymbol);
public async getAssetProfile({
symbol
}: {
symbol: string;
}): Promise<Partial<SymbolProfile>> {
const { assetClass, assetSubClass, currency, name } =
await this.yahooFinanceDataEnhancerService.getAssetProfile(symbol);
return {
assetClass,

View File

@ -25,7 +25,9 @@
class="h-100"
[currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems"
[isInPercent]="data.hasImpersonationId || user.settings.isRestrictedView"
[isInPercent]="
data.hasImpersonationId || user.settings.isRestrictedView
"
[isLoading]="isLoadingChart"
[locale]="user?.settings?.locale"
/>
@ -92,7 +94,9 @@
[dataSource]="dataSource"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToExportActivities]="
!data.hasImpersonationId && !user.settings.isRestrictedView
"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
@ -113,7 +117,11 @@
[accountBalances]="accountBalances"
[accountId]="data.accountId"
[locale]="user?.settings?.locale"
[showActions]="!data.hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
[showActions]="
!data.hasImpersonationId &&
hasPermissionToDeleteAccountBalance &&
!user.settings.isRestrictedView
"
(accountBalanceDeleted)="onDeleteAccountBalance($event)"
/>
</mat-tab>

View File

@ -1,3 +1,5 @@
import { getLocale } from '@ghostfolio/common/helper';
import {
ChangeDetectionStrategy,
Component,
@ -27,7 +29,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToOpenDetails = true;
@Input() locale: string;
@Input() locale = getLocale();
@Input() showActions: boolean;
@Input() showBalance = true;
@Input() showFooter = true;

View File

@ -163,7 +163,12 @@
<mat-menu #assetProfileActionsMenu="matMenu" xPosition="before">
<button
mat-menu-item
(click)="onOpenAssetProfileDialog({ dataSource: element.dataSource, symbol: element.symbol })"
(click)="
onOpenAssetProfileDialog({
dataSource: element.dataSource,
symbol: element.symbol
})
"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
@ -173,7 +178,12 @@
<button
mat-menu-item
[disabled]="element.activitiesCount !== 0"
(click)="onDeleteProfileData({dataSource: element.dataSource, symbol: element.symbol})"
(click)="
onDeleteProfileData({
dataSource: element.dataSource,
symbol: element.symbol
})
"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
@ -189,16 +199,19 @@
*matRowDef="let row; columns: displayedColumns"
class="cursor-pointer"
mat-row
(click)="onOpenAssetProfileDialog({ dataSource: row.dataSource, symbol: row.symbol })"
(click)="
onOpenAssetProfileDialog({
dataSource: row.dataSource,
symbol: row.symbol
})
"
></tr>
</table>
<mat-paginator
[length]="totalItems"
[ngClass]="{
'd-none':
(isLoading && totalItems === 0) ||
totalItems <= pageSize
'd-none': (isLoading && totalItems === 0) || totalItems <= pageSize
}"
[pageSize]="pageSize"
[showFirstLastButtons]="true"

View File

@ -25,7 +25,9 @@
mat-menu-item
type="button"
[disabled]="assetProfileForm.dirty"
(click)="onGatherSymbol({dataSource: data.dataSource, symbol: data.symbol})"
(click)="
onGatherSymbol({ dataSource: data.dataSource, symbol: data.symbol })
"
>
<ng-container i18n>Gather Historical Data</ng-container>
</button>
@ -33,7 +35,12 @@
mat-menu-item
type="button"
[disabled]="assetProfileForm.dirty"
(click)="onGatherProfileDataBySymbol({dataSource: data.dataSource, symbol: data.symbol})"
(click)="
onGatherProfileDataBySymbol({
dataSource: data.dataSource,
symbol: data.symbol
})
"
>
<ng-container i18n>Gather Profile Data</ng-container>
</button>
@ -73,7 +80,12 @@
color="accent"
mat-flat-button
type="button"
[disabled]="!assetProfileForm.controls['historicalData']?.controls['csvString'].touched || assetProfileForm.controls['historicalData']?.controls['csvString']?.value === ''"
[disabled]="
!assetProfileForm.controls['historicalData']?.controls['csvString']
.touched ||
assetProfileForm.controls['historicalData']?.controls['csvString']
?.value === ''
"
(click)="onImportHistoricalData()"
>
<ng-container i18n>Import</ng-container>
@ -129,49 +141,54 @@
>
</div>
<ng-container
*ngIf="assetProfile?.countries?.length > 0 || assetProfile?.sectors?.length > 0"
*ngIf="
assetProfile?.countries?.length > 0 ||
assetProfile?.sectors?.length > 0
"
>
@if (assetProfile?.countries?.length === 1 &&
assetProfile?.sectors?.length === 1 ) {
<div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.sectors[0].name"
>Sector</gf-value
>
</div>
<div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.countries[0].name"
>Country</gf-value
>
</div>
@if (
assetProfile?.countries?.length === 1 &&
assetProfile?.sectors?.length === 1
) {
<div *ngIf="assetProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.sectors[0].name"
>Sector</gf-value
>
</div>
<div *ngIf="assetProfile?.countries?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="assetProfile?.countries[0].name"
>Country</gf-value
>
</div>
} @else {
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="sectors"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="countries"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="sectors"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[maxItems]="10"
[positions]="countries"
/>
</div>
}
</ng-container>
</div>
@ -222,7 +239,17 @@
color="primary"
i18n
[checked]="isBenchmark"
(change)="isBenchmark ? onUnsetBenchmark({dataSource: data.dataSource, symbol: data.symbol}) : onSetBenchmark({dataSource: data.dataSource, symbol: data.symbol})"
(change)="
isBenchmark
? onUnsetBenchmark({
dataSource: data.dataSource,
symbol: data.symbol
})
: onSetBenchmark({
dataSource: data.dataSource,
symbol: data.symbol
})
"
>Benchmark</mat-checkbox
>
</div>
@ -253,7 +280,9 @@
color="accent"
mat-flat-button
type="button"
[disabled]="assetProfileForm.controls['scraperConfiguration'].value === '{}'"
[disabled]="
assetProfileForm.controls['scraperConfiguration'].value === '{}'
"
(click)="onTestMarketData()"
>
<ng-container i18n>Test</ng-container>

View File

@ -28,7 +28,7 @@
[value]="transactionCount"
/>
<div *ngIf="transactionCount && userCount">
{{ transactionCount / userCount | number : '1.2-2' }}
{{ transactionCount / userCount | number: '1.2-2' }}
<span i18n>per User</span>
</div>
</div>
@ -69,10 +69,10 @@
<a
mat-menu-item
[queryParams]="{
assetProfileDialog: true,
dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol
}"
assetProfileDialog: true,
dataSource: exchangeRate.dataSource,
symbol: exchangeRate.symbol
}"
[routerLink]="['/admin', 'market-data']"
>
<span class="align-items-center d-flex">
@ -112,7 +112,9 @@
<mat-slide-toggle
color="primary"
hideIcon="true"
[checked]="info.globalPermissions.includes(permissions.createUserAccount)"
[checked]="
info.globalPermissions.includes(permissions.createUserAccount)
"
(change)="onEnableUserSignupModeChange($event)"
/>
</div>
@ -143,7 +145,7 @@
<div class="w-50" i18n>System Message</div>
<div class="w-50">
<div *ngIf="systemMessage" class="align-items-center d-flex">
<div class="text-truncate">{{ systemMessage | json }}</div>
<div class="text-truncate">{{ systemMessage | json }}</div>
<button
class="h-100 mx-1 no-min-width px-2"
mat-button

View File

@ -12,7 +12,7 @@
#
</th>
<td
*matCellDef="let element; let i=index"
*matCellDef="let element; let i = index"
class="mat-mdc-cell px-1 py-2 text-right"
mat-cell
>
@ -35,17 +35,23 @@
mat-cell
>
<div class="d-flex align-items-center">
<span class="d-none d-sm-inline-block text-monospace"
>{{ element.id }}</span
>
<span class="d-inline-block d-sm-none text-monospace"
>{{ (element.id | slice:0:5) + '...' }}</span
>
<span class="d-none d-sm-inline-block text-monospace">{{
element.id
}}</span>
<span class="d-inline-block d-sm-none text-monospace">{{
(element.id | slice: 0 : 5) + '...'
}}</span>
<gf-premium-indicator
*ngIf="element?.subscription?.type === 'Premium'"
class="ml-1"
[enableLink]="false"
[title]="'Expires ' + formatDistanceToNow(element.subscription.expiresAt) + ' (' + (element.subscription.expiresAt | date: defaultDateFormat) + ')'"
[title]="
'Expires ' +
formatDistanceToNow(element.subscription.expiresAt) +
' (' +
(element.subscription.expiresAt | date: defaultDateFormat) +
')'
"
/>
</div>
</td>
@ -67,9 +73,9 @@
class="mat-mdc-cell px-1 py-2"
mat-cell
>
<span class="h5" [title]="element.country"
>{{ getEmojiFlag(element.country) }}</span
>
<span class="h5" [title]="element.country">{{
getEmojiFlag(element.country)
}}</span>
</td>
</ng-container>

View File

@ -7,6 +7,7 @@ import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import {
getBackgroundColor,
getDateFormatString,
getLocale,
getTextColor,
parseDate
} from '@ghostfolio/common/helper';
@ -51,7 +52,7 @@ export class BenchmarkComparatorComponent implements OnChanges, OnDestroy {
@Input() colorScheme: ColorScheme;
@Input() daysInMarket: number;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() locale = getLocale();
@Input() performanceDataItems: LineChartItem[];
@Input() user: User;

View File

@ -1,100 +1,100 @@
<div
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative"
>
@if(hasPermissionToCreateOrder && historicalDataItems?.length === 0) {
<div class="justify-content-center row w-100">
<div class="col introduction">
<h4 i18n>Welcome to Ghostfolio</h4>
<p i18n>Ready to take control of your personal finances?</p>
<ol class="font-weight-bold">
<li
class="mb-2"
[ngClass]="{ 'text-muted': user?.accounts?.length > 1 }"
>
<a class="d-block" [routerLink]="['/accounts']"
><span i18n>Setup your accounts</span><br />
<span class="font-weight-normal" i18n
>Get a comprehensive financial overview by adding your bank and
brokerage accounts.</span
></a
@if (hasPermissionToCreateOrder && historicalDataItems?.length === 0) {
<div class="justify-content-center row w-100">
<div class="col introduction">
<h4 i18n>Welcome to Ghostfolio</h4>
<p i18n>Ready to take control of your personal finances?</p>
<ol class="font-weight-bold">
<li
class="mb-2"
[ngClass]="{ 'text-muted': user?.accounts?.length > 1 }"
>
</li>
<li class="mb-2">
<a class="d-block" [routerLink]="['/portfolio', 'activities']">
<span i18n>Capture your activities</span><br />
<span class="font-weight-normal" i18n
>Record your investment activities to keep your portfolio up to
date.</span
></a
>
</li>
<li class="mb-2">
<a class="d-block" [routerLink]="['/portfolio']">
<span i18n>Monitor and analyze your portfolio</span><br />
<span class="font-weight-normal" i18n
>Track your progress in real-time with comprehensive analysis and
insights.</span
<a class="d-block" [routerLink]="['/accounts']"
><span i18n>Setup your accounts</span><br />
<span class="font-weight-normal" i18n
>Get a comprehensive financial overview by adding your bank and
brokerage accounts.</span
></a
>
</li>
<li class="mb-2">
<a class="d-block" [routerLink]="['/portfolio', 'activities']">
<span i18n>Capture your activities</span><br />
<span class="font-weight-normal" i18n
>Record your investment activities to keep your portfolio up to
date.</span
></a
>
</li>
<li class="mb-2">
<a class="d-block" [routerLink]="['/portfolio']">
<span i18n>Monitor and analyze your portfolio</span><br />
<span class="font-weight-normal" i18n
>Track your progress in real-time with comprehensive analysis
and insights.</span
>
</a>
</li>
</ol>
<div class="d-flex justify-content-center">
<a
*ngIf="user?.accounts?.length === 1"
color="primary"
mat-flat-button
[routerLink]="['/accounts']"
>
<ng-container i18n>Setup accounts</ng-container>
</a>
</li>
</ol>
<div class="d-flex justify-content-center">
<a
*ngIf="user?.accounts?.length === 1"
color="primary"
mat-flat-button
[routerLink]="['/accounts']"
>
<ng-container i18n>Setup accounts</ng-container>
</a>
<a
*ngIf="user?.accounts?.length > 1"
color="primary"
mat-flat-button
[routerLink]="['/portfolio', 'activities']"
>
<ng-container i18n>Add activity</ng-container>
</a>
<a
*ngIf="user?.accounts?.length > 1"
color="primary"
mat-flat-button
[routerLink]="['/portfolio', 'activities']"
>
<ng-container i18n>Add activity</ng-container>
</a>
</div>
</div>
</div>
</div>
} @else {
<div class="row w-100">
<div class="col p-0">
<div class="chart-container mx-auto position-relative">
<gf-line-chart
class="position-absolute"
symbol="Performance"
unit="%"
[colorScheme]="user?.settings?.colorScheme"
[hidden]="historicalDataItems?.length === 0"
[historicalDataItems]="historicalDataItems"
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
<div class="row w-100">
<div class="col p-0">
<div class="chart-container mx-auto position-relative">
<gf-line-chart
class="position-absolute"
symbol="Performance"
unit="%"
[colorScheme]="user?.settings?.colorScheme"
[hidden]="historicalDataItems?.length === 0"
[historicalDataItems]="historicalDataItems"
[isAnimated]="user?.settings?.dateRange === '1d' ? false : true"
[locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
/>
</div>
</div>
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[deviceType]="deviceType"
[errors]="errors"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
[showGradient]="true"
[showLoader]="false"
[showXAxis]="false"
[showYAxis]="false"
[performance]="performance"
[showDetails]="showDetails"
[unit]="unit"
/>
</div>
</div>
</div>
<div class="overview-container row mt-1">
<div class="col">
<gf-portfolio-performance
class="pb-4"
[deviceType]="deviceType"
[errors]="errors"
[isAllTimeHigh]="isAllTimeHigh"
[isAllTimeLow]="isAllTimeLow"
[isLoading]="isLoadingPerformance"
[locale]="user?.settings?.locale"
[performance]="performance"
[showDetails]="showDetails"
[unit]="unit"
/>
</div>
</div>
}
</div>

View File

@ -6,7 +6,9 @@
<mat-card-content>
<gf-portfolio-summary
[baseCurrency]="user?.settings?.baseCurrency"
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[isLoading]="isLoading"
[language]="user?.settings?.language"
[locale]="user?.settings?.locale"

View File

@ -9,6 +9,7 @@ import {
DATE_FORMAT,
getBackgroundColor,
getDateFormatString,
getLocale,
getTextColor,
parseDate
} from '@ghostfolio/common/helper';
@ -65,7 +66,7 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
@Input() historicalDataItems: LineChartItem[] = [];
@Input() isInPercent = false;
@Input() isLoading = false;
@Input() locale: string;
@Input() locale = getLocale();
@Input() range: DateRange = 'max';
@Input() savingsRate = 0;

View File

@ -1,4 +1,5 @@
import {
getLocale,
getNumberFormatDecimal,
getNumberFormatGroup
} from '@ghostfolio/common/helper';
@ -31,7 +32,7 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() isAllTimeHigh: boolean;
@Input() isAllTimeLow: boolean;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() locale = getLocale();
@Input() performance: PortfolioPerformance;
@Input() showDetails: boolean;
@Input() unit: string;

View File

@ -1,4 +1,4 @@
import { getDateFnsLocale } from '@ghostfolio/common/helper';
import { getDateFnsLocale, getLocale } from '@ghostfolio/common/helper';
import { PortfolioSummary } from '@ghostfolio/common/interfaces';
import {
@ -23,7 +23,7 @@ export class PortfolioSummaryComponent implements OnChanges, OnInit {
@Input() hasPermissionToUpdateUserSettings: boolean;
@Input() isLoading: boolean;
@Input() language: string;
@Input() locale: string;
@Input() locale = getLocale();
@Input() summary: PortfolioSummary;
@Output() emergencyFundChanged = new EventEmitter<number>();

View File

@ -87,7 +87,11 @@
size="medium"
[isCurrency]="true"
[locale]="data.locale"
[ngClass]="{ 'text-danger': minPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
[ngClass]="{
'text-danger':
minPrice?.toFixed(2) === marketPrice?.toFixed(2) &&
maxPrice?.toFixed(2) !== minPrice?.toFixed(2)
}"
[unit]="SymbolProfile?.currency"
[value]="minPrice"
>Minimum Price</gf-value
@ -99,7 +103,11 @@
size="medium"
[isCurrency]="true"
[locale]="data.locale"
[ngClass]="{ 'text-success': maxPrice?.toFixed(2) === marketPrice?.toFixed(2) && maxPrice?.toFixed(2) !== minPrice?.toFixed(2) }"
[ngClass]="{
'text-success':
maxPrice?.toFixed(2) === marketPrice?.toFixed(2) &&
maxPrice?.toFixed(2) !== minPrice?.toFixed(2)
}"
[unit]="SymbolProfile?.currency"
[value]="maxPrice"
>Maximum Price</gf-value
@ -184,53 +192,61 @@
>
</div>
<ng-container
*ngIf="SymbolProfile?.countries?.length > 0 || SymbolProfile?.sectors?.length > 0"
*ngIf="
SymbolProfile?.countries?.length > 0 ||
SymbolProfile?.sectors?.length > 0
"
>
@if(SymbolProfile?.countries?.length === 1 &&
SymbolProfile?.sectors?.length === 1) {
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.sectors[0].name"
>Sector</gf-value
@if (
SymbolProfile?.countries?.length === 1 &&
SymbolProfile?.sectors?.length === 1
) {
<div *ngIf="SymbolProfile?.sectors?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.sectors[0].name"
>Sector</gf-value
>
</div>
<div
*ngIf="SymbolProfile?.countries?.length === 1"
class="col-6 mb-3"
>
</div>
<div *ngIf="SymbolProfile?.countries?.length === 1" class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.countries[0].name"
>Country</gf-value
>
</div>
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="SymbolProfile.countries[0].name"
>Country</gf-value
>
</div>
} @else {
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[locale]="data.locale"
[maxItems]="10"
[positions]="sectors"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[locale]="data.locale"
[maxItems]="10"
[positions]="countries"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Sectors</div>
<gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[locale]="data.locale"
[maxItems]="10"
[positions]="sectors"
/>
</div>
<div class="col-md-6 mb-3">
<div class="h5" i18n>Countries</div>
<gf-portfolio-proportion-chart
[baseCurrency]="data.baseCurrency"
[colorScheme]="data.colorScheme"
[isInPercent]="true"
[keys]="['name']"
[locale]="data.locale"
[maxItems]="10"
[positions]="countries"
/>
</div>
}
</ng-container>
<div *ngIf="dataProviderInfo" class="col-md-12 mb-3 text-center">
@ -257,7 +273,9 @@
[dataSource]="dataSource"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToExportActivities]="
!data.hasImpersonationId && !user.settings.isRestrictedView
"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data.locale"
@ -294,15 +312,17 @@
<div class="col">
<div class="h5" i18n>Tags</div>
<mat-chip-listbox>
<mat-chip-option *ngFor="let tag of tags" disabled
>{{ tag.name }}</mat-chip-option
>
<mat-chip-option *ngFor="let tag of tags" disabled>{{
tag.name
}}</mat-chip-option>
</mat-chip-listbox>
</div>
</div>
<div
*ngIf="activities?.length > 0 && data.hasPermissionToReportDataGlitch === true"
*ngIf="
activities?.length > 0 && data.hasPermissionToReportDataGlitch === true
"
class="row"
>
<div class="col">

View File

@ -1,4 +1,5 @@
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getLocale } from '@ghostfolio/common/helper';
import { Position } from '@ghostfolio/common/interfaces';
import {
@ -20,7 +21,7 @@ export class PositionComponent implements OnDestroy, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() isLoading: boolean;
@Input() locale: string;
@Input() locale = getLocale();
@Input() position: Position;
@Input() range: string;

View File

@ -1,3 +1,4 @@
import { getLocale } from '@ghostfolio/common/helper';
import { Position } from '@ghostfolio/common/interfaces';
import {
@ -18,7 +19,7 @@ export class PositionsComponent implements OnChanges, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToCreateOrder: boolean;
@Input() locale: string;
@Input() locale = getLocale();
@Input() positions: Position[];
@Input() range: string;

View File

@ -28,30 +28,32 @@
</div>
@if (accessForm.controls['type'].value === 'PRIVATE') {
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Permission</mat-label>
<mat-select formControlName="permissions">
<mat-option i18n value="READ_RESTRICTED">Restricted view</mat-option>
@if(data?.user?.settings?.isExperimentalFeatures) {
<mat-option i18n value="READ">View</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label>
Ghostfolio <ng-container i18n>User ID</ng-container>
</mat-label>
<input
formControlName="userId"
matInput
type="text"
(keydown.enter)="$event.stopPropagation()"
/>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Permission</mat-label>
<mat-select formControlName="permissions">
<mat-option i18n value="READ_RESTRICTED"
>Restricted view</mat-option
>
@if (data?.user?.settings?.isExperimentalFeatures) {
<mat-option i18n value="READ">View</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label>
Ghostfolio <ng-container i18n>User ID</ng-container>
</mat-label>
<input
formControlName="userId"
matInput
type="text"
(keydown.enter)="$event.stopPropagation()"
/>
</mat-form-field>
</div>
}
</div>
<div class="justify-content-end" mat-dialog-actions>

View File

@ -3,7 +3,7 @@
<div class="col">
<div class="align-items-center d-flex flex-column">
<gf-membership-card
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[name]="user?.subscription?.type"
/>
<div
@ -11,14 +11,19 @@
class="d-flex flex-column mt-5"
>
<ng-container
*ngIf="hasPermissionForSubscription && hasPermissionToUpdateUserSettings"
*ngIf="
hasPermissionForSubscription && hasPermissionToUpdateUserSettings
"
>
<button color="primary" mat-flat-button (click)="onCheckout()">
<ng-container *ngIf="user.subscription.offer === 'default'" i18n
>Upgrade Plan</ng-container
>
<ng-container
*ngIf="user.subscription.offer === 'renewal' || user.subscription.offer === 'renewal-early-bird'"
*ngIf="
user.subscription.offer === 'renewal' ||
user.subscription.offer === 'renewal-early-bird'
"
i18n
>Renew Plan</ng-container
>
@ -27,7 +32,8 @@
<ng-container *ngIf="coupon"
><del class="text-muted"
>{{ baseCurrency }}&nbsp;{{ price }}</del
>&nbsp;{{ baseCurrency }}&nbsp;{{ price - coupon
>&nbsp;{{ baseCurrency }}&nbsp;{{
price - coupon
}}</ng-container
>
<ng-container *ngIf="!coupon"

View File

@ -32,7 +32,9 @@
name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.baseCurrency"
(selectionChange)="onChangeUserSetting('baseCurrency', $event.value)"
(selectionChange)="
onChangeUserSetting('baseCurrency', $event.value)
"
>
<mat-option
*ngFor="let currency of currencies"
@ -53,7 +55,9 @@
>
If a translation is missing, kindly support us in extending it
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{language}}.xlf"
href="https://github.com/ghostfolio/ghostfolio/blob/main/apps/client/src/locales/messages.{{
language
}}.xlf"
target="_blank"
>here</a
>.
@ -65,7 +69,9 @@
name="language"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="language"
(selectionChange)="onChangeUserSetting('language', $event.value)"
(selectionChange)="
onChangeUserSetting('language', $event.value)
"
>
<mat-option [value]="null" />
<mat-option value="de">Deutsch</mat-option>
@ -115,12 +121,14 @@
name="locale"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.locale"
(selectionChange)="onChangeUserSetting('locale', $event.value)"
(selectionChange)="
onChangeUserSetting('locale', $event.value)
"
>
<mat-option [value]="null" />
<mat-option *ngFor="let locale of locales" [value]="locale"
>{{ locale }}</mat-option
>
<mat-option *ngFor="let locale of locales" [value]="locale">{{
locale
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -137,7 +145,9 @@
[disabled]="!hasPermissionToUpdateUserSettings"
[placeholder]="appearancePlaceholder"
[value]="user?.settings?.colorScheme"
(selectionChange)="onChangeUserSetting('colorScheme', $event.value)"
(selectionChange)="
onChangeUserSetting('colorScheme', $event.value)
"
>
<mat-option i18n [value]="null">Auto</mat-option>
<mat-option i18n value="LIGHT">Light</mat-option>

View File

@ -1,4 +1,4 @@
import { getNumberFormatGroup } from '@ghostfolio/common/helper';
import { getLocale, getNumberFormatGroup } from '@ghostfolio/common/helper';
import {
ChangeDetectionStrategy,
@ -21,7 +21,7 @@ export class WorldMapChartComponent implements OnChanges, OnDestroy, OnInit {
@Input() countries: { [code: string]: { name?: string; value: number } };
@Input() format: string;
@Input() isInPercent = false;
@Input() locale: string;
@Input() locale = getLocale();
public isLoading = true;
public svgMapElement;

View File

@ -21,7 +21,7 @@
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>

View File

@ -11,26 +11,28 @@
</h1>
<div class="row">
@for (ossFriend of ossFriends; track ossFriend) {
<div class="col-xs-12 col-md-4 mb-3">
<a target="_blank" [href]="ossFriend.href">
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-header>
<mat-card-title class="h4">{{ ossFriend.name }}</mat-card-title>
</mat-card-header>
<mat-card-content class="flex-grow-1">
<p>{{ ossFriend.description }}</p>
</mat-card-content>
<mat-card-actions class="justify-content-end">
<a mat-button target="_blank" [href]="ossFriend.href">
<span
><ng-container i18n>Visit</ng-container> {{ ossFriend.name
}}</span
><ion-icon class="ml-1" name="arrow-forward-outline" />
</a>
</mat-card-actions>
</mat-card>
</a>
</div>
<div class="col-xs-12 col-md-4 mb-3">
<a target="_blank" [href]="ossFriend.href">
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-header>
<mat-card-title class="h4">{{
ossFriend.name
}}</mat-card-title>
</mat-card-header>
<mat-card-content class="flex-grow-1">
<p>{{ ossFriend.description }}</p>
</mat-card-content>
<mat-card-actions class="justify-content-end">
<a mat-button target="_blank" [href]="ossFriend.href">
<span
><ng-container i18n>Visit</ng-container>
{{ ossFriend.name }}</span
><ion-icon class="ml-1" name="arrow-forward-outline" />
</a>
</mat-card-actions>
</mat-card>
</a>
</div>
}
</div>
</div>

View File

@ -147,15 +147,15 @@
>
</div>
@if (hasPermissionForSubscription) {
<div class="col-md-6 col-xs-12 my-2">
<a
class="py-4 w-100"
color="primary"
mat-flat-button
[routerLink]="['/blog']"
>Blog</a
>
</div>
<div class="col-md-6 col-xs-12 my-2">
<a
class="py-4 w-100"
color="primary"
mat-flat-button
[routerLink]="['/blog']"
>Blog</a
>
</div>
}
</div>
</div>

View File

@ -8,7 +8,11 @@
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToUpdateAccount && !user.settings.isRestrictedView"
[showActions]="
!hasImpersonationId &&
hasPermissionToUpdateAccount &&
!user.settings.isRestrictedView
"
[totalBalanceInBaseCurrency]="totalBalanceInBaseCurrency"
[totalValueInBaseCurrency]="totalValueInBaseCurrency"
[transactionCount]="transactionCount"
@ -21,7 +25,11 @@
</div>
<div
*ngIf="!hasImpersonationId && hasPermissionToCreateAccount && !user.settings.isRestrictedView"
*ngIf="
!hasImpersonationId &&
hasPermissionToCreateAccount &&
!user.settings.isRestrictedView
"
class="fab-container"
>
<a

View File

@ -5,9 +5,9 @@
(ngSubmit)="onSubmit()"
>
@if (data.account.id) {
<h1 i18n mat-dialog-title>Update account</h1>
<h1 i18n mat-dialog-title>Update account</h1>
} @else {
<h1 i18n mat-dialog-title>Add account</h1>
<h1 i18n mat-dialog-title>Add account</h1>
}
<div class="flex-grow-1 py-3" mat-dialog-content>
<div>
@ -38,9 +38,9 @@
type="number"
(keydown.enter)="$event.stopPropagation()"
/>
<span class="ml-2" matTextSuffix
>{{ accountForm.controls['currency']?.value?.value }}</span
>
<span class="ml-2" matTextSuffix>{{
accountForm.controls['currency']?.value?.value
}}</span>
</mat-form-field>
</div>
<div [ngClass]="{ 'd-none': platforms?.length < 1 }">
@ -55,18 +55,20 @@
(keydown.enter)="$event.stopPropagation()"
/>
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
@for (platformEntry of filteredPlatforms | async; track platformEntry)
{
<mat-option [value]="platformEntry">
<span class="d-flex">
<gf-symbol-icon
class="mr-1"
[tooltip]="platformEntry.name"
[url]="platformEntry.url"
/>
<span>{{ platformEntry.name }}</span>
</span>
</mat-option>
@for (
platformEntry of filteredPlatforms | async;
track platformEntry
) {
<mat-option [value]="platformEntry">
<span class="d-flex">
<gf-symbol-icon
class="mr-1"
[tooltip]="platformEntry.name"
[url]="platformEntry.url"
/>
<span>{{ platformEntry.name }}</span>
</span>
</mat-option>
}
</mat-autocomplete>
</mat-form-field>
@ -89,12 +91,12 @@
>
</div>
@if (data.account.id) {
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label>
<input formControlName="accountId" matInput />
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label>
<input formControlName="accountId" matInput />
</mat-form-field>
</div>
}
</div>
<div class="justify-content-end" mat-dialog-actions>

View File

@ -21,7 +21,7 @@
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>

View File

@ -9,30 +9,30 @@
>
</h1>
@if (hasPermissionForSubscription) {
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden w-100"
href="../en/blog/2023/11/black-week-2023"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">Black Week 2023</div>
<div class="d-flex text-muted">2023-11-19</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden w-100"
href="../en/blog/2023/11/black-week-2023"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">Black Week 2023</div>
<div class="d-flex text-muted">2023-11-19</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
</mat-card-content>
</mat-card>
}
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
@ -294,30 +294,30 @@
</mat-card-content>
</mat-card>
@if (hasPermissionForSubscription) {
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden w-100"
href="../en/blog/2022/11/black-friday-2022"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">Black Friday 2022</div>
<div class="d-flex text-muted">2022-11-13</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden w-100"
href="../en/blog/2022/11/black-friday-2022"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate">Black Friday 2022</div>
<div class="d-flex text-muted">2022-11-13</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
</mat-card-content>
</mat-card>
}
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>

View File

@ -21,7 +21,7 @@
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>

View File

@ -140,7 +140,7 @@
<h4 class="align-items-center d-flex">
<span i18n>Portfolio Calculations</span>
@if (hasPermissionForSubscription) {
<gf-premium-indicator class="ml-1" />
<gf-premium-indicator class="ml-1" />
}
</h4>
<p class="m-0">
@ -159,7 +159,7 @@
<h4 class="align-items-center d-flex">
<span i18n>Portfolio Allocations</span>
@if (hasPermissionForSubscription) {
<gf-premium-indicator class="ml-1" />
<gf-premium-indicator class="ml-1" />
}
</h4>
<p class="m-0">
@ -197,24 +197,24 @@
</mat-card>
</div>
@if (hasPermissionForSubscription) {
<div class="col-xs-12 col-md-4 mb-3">
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span i18n>Market Mood</span>
<gf-premium-indicator class="ml-1" />
</h4>
<p class="m-0">
Check the current market mood (<a
[routerLink]="routerLinkResources"
>Fear & Greed Index</a
>) within the app.
</p>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="col-xs-12 col-md-4 mb-3">
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span i18n>Market Mood</span>
<gf-premium-indicator class="ml-1" />
</h4>
<p class="m-0">
Check the current market mood (<a
[routerLink]="routerLinkResources"
>Fear & Greed Index</a
>) within the app.
</p>
</div>
</mat-card-content>
</mat-card>
</div>
}
<div class="col-xs-12 col-md-4 mb-3">
<mat-card appearance="outlined" class="d-flex flex-column h-100">
@ -223,7 +223,7 @@
<h4 class="align-items-center d-flex">
<span i18n>Static Analysis</span>
@if (hasPermissionForSubscription) {
<gf-premium-indicator class="ml-1" />
<gf-premium-indicator class="ml-1" />
}
</h4>
<p class="m-0">
@ -290,12 +290,16 @@
</div>
</div>
@if (!user) {
<div class="row">
<div class="col mt-3 text-center">
<a color="primary" i18n mat-flat-button [routerLink]="routerLinkRegister"
>Get Started</a
>
<div class="row">
<div class="col mt-3 text-center">
<a
color="primary"
i18n
mat-flat-button
[routerLink]="routerLinkRegister"
>Get Started</a
>
</div>
</div>
</div>
}
</div>

View File

@ -21,7 +21,7 @@
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>

View File

@ -339,7 +339,8 @@
[href]="testimonial.url"
>{{ testimonial.author }}</a
>
<span *ngIf="!testimonial.url">{{ testimonial.author }}</span>,
<span *ngIf="!testimonial.url">{{ testimonial.author }}</span
>,
{{ testimonial.country }}
</div>
</div>

View File

@ -11,7 +11,11 @@
[locale]="user?.settings?.locale"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
[showActions]="
!hasImpersonationId &&
hasPermissionToDeleteActivity &&
!user.settings.isRestrictedView
"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[totalItems]="totalItems"
@ -29,18 +33,21 @@
</div>
</div>
@if (!hasImpersonationId && hasPermissionToCreateActivity &&
!user.settings.isRestrictedView) {
<div class="fab-container">
<a
class="align-items-center d-flex justify-content-center"
color="primary"
mat-fab
[queryParams]="{ createDialog: true }"
[routerLink]="[]"
>
<ion-icon name="add-outline" size="large" />
</a>
</div>
@if (
!hasImpersonationId &&
hasPermissionToCreateActivity &&
!user.settings.isRestrictedView
) {
<div class="fab-container">
<a
class="align-items-center d-flex justify-content-center"
color="primary"
mat-fab
[queryParams]="{ createDialog: true }"
[routerLink]="[]"
>
<ion-icon name="add-outline" size="large" />
</a>
</div>
}
</div>

View File

@ -11,48 +11,61 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Type</mat-label>
<mat-select formControlName="type">
<mat-select-trigger
>{{ typesTranslationMap[activityForm.controls['type'].value]
}}</mat-select-trigger
>
<mat-select-trigger>{{
typesTranslationMap[activityForm.controls['type'].value]
}}</mat-select-trigger>
<mat-option value="BUY">
<span><b>{{ typesTranslationMap['BUY'] }}</b></span>
<span
><b>{{ typesTranslationMap['BUY'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
>
</mat-option>
<mat-option value="FEE">
<span><b>{{ typesTranslationMap['FEE'] }}</b></span>
<span
><b>{{ typesTranslationMap['FEE'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>One-time fee, annual account fees</small
>
</mat-option>
<mat-option value="DIVIDEND">
<span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span>
<span
><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Distribution of corporate earnings</small
>
</mat-option>
<mat-option value="INTEREST">
<span><b>{{ typesTranslationMap['INTEREST'] }}</b></span>
<span
><b>{{ typesTranslationMap['INTEREST'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Revenue for lending out money</small
>
</mat-option>
<mat-option value="LIABILITY">
<span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span>
<span
><b>{{ typesTranslationMap['LIABILITY'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Mortgages, personal loans, credit cards</small
>
</mat-option>
<mat-option value="SELL">
<span><b>{{ typesTranslationMap['SELL'] }}</b></span>
<span
><b>{{ typesTranslationMap['SELL'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
>
</mat-option>
<mat-option value="ITEM">
<span><b>{{ typesTranslationMap['ITEM'] }}</b></span>
<span
><b>{{ typesTranslationMap['ITEM'] }}</b></span
>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Luxury items, real estate, private companies</small
>
@ -60,16 +73,20 @@
</mat-select>
</mat-form-field>
</div>
<div [ngClass]="{'mb-3': data.activity.id}">
<div [ngClass]="{ 'mb-3': data.activity.id }">
<mat-form-field
appearance="outline"
class="w-100"
[ngClass]="{'mb-1 without-hint': !data.activity.id}"
[ngClass]="{ 'mb-1 without-hint': !data.activity.id }"
>
<mat-label i18n>Account</mat-label>
<mat-select formControlName="accountId">
<mat-option
*ngIf="!activityForm.controls['accountId'].hasValidator(Validators.required)"
*ngIf="
!activityForm.controls['accountId'].hasValidator(
Validators.required
)
"
[value]="null"
/>
<mat-option
@ -88,14 +105,18 @@
</mat-select>
</mat-form-field>
</div>
<div class="mb-3" [ngClass]="{'d-none': data.activity.id}">
<div class="mb-3" [ngClass]="{ 'd-none': data.activity.id }">
<mat-checkbox color="primary" formControlName="updateAccountBalance" i18n
>Update Cash Balance</mat-checkbox
>
</div>
<div
class="mb-3"
[ngClass]="{ 'd-none': !activityForm.controls['searchSymbol'].hasValidator(Validators.required) }"
[ngClass]="{
'd-none': !activityForm.controls['searchSymbol'].hasValidator(
Validators.required
)
}"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name, symbol or ISIN</mat-label>
@ -107,7 +128,11 @@
</div>
<div
class="mb-3"
[ngClass]="{ 'd-none': !activityForm.controls['name'].hasValidator(Validators.required) }"
[ngClass]="{
'd-none': !activityForm.controls['name'].hasValidator(
Validators.required
)
}"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label>
@ -118,9 +143,9 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label>
<mat-select formControlName="currency">
<mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option
>
<mat-option *ngFor="let currency of currencies" [value]="currency">{{
currency
}}</mat-option>
</mat-select>
</mat-form-field>
</div>
@ -146,7 +171,13 @@
</div>
<div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' || activityForm.controls['type']?.value === 'INTEREST' || activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
[ngClass]="{
'd-none':
activityForm.controls['type']?.value === 'FEE' ||
activityForm.controls['type']?.value === 'INTEREST' ||
activityForm.controls['type']?.value === 'ITEM' ||
activityForm.controls['type']?.value === 'LIABILITY'
}"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Quantity</mat-label>
@ -155,7 +186,7 @@
</div>
<div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' }"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'FEE' }"
>
<div class="align-items-start d-flex">
<mat-form-field appearance="outline" class="w-100">
@ -192,17 +223,25 @@
</mat-select>
</div>
<mat-error
*ngIf="activityForm.controls['unitPriceInCustomCurrency'].hasError('invalid')"
*ngIf="
activityForm.controls['unitPriceInCustomCurrency'].hasError(
'invalid'
)
"
><ng-container i18n
>Oops! Could not get the historical exchange rate
from</ng-container
>
{{ activityForm.controls['date']?.value | date: defaultDateFormat
{{
activityForm.controls['date']?.value | date: defaultDateFormat
}}</mat-error
>
</mat-form-field>
<button
*ngIf="currentMarketPrice && (data.activity.type === 'BUY' || data.activity.type === 'SELL')"
*ngIf="
currentMarketPrice &&
(data.activity.type === 'BUY' || data.activity.type === 'SELL')
"
class="ml-2 mt-1 no-min-width"
mat-button
title="Apply current market price"
@ -228,14 +267,19 @@
</ng-container>
</mat-label>
<input formControlName="unitPrice" matInput type="number" />
<span class="ml-2" matTextSuffix
>{{ activityForm.controls['currency'].value }}</span
>
<span class="ml-2" matTextSuffix>{{
activityForm.controls['currency'].value
}}</span>
</mat-form-field>
</div>
<div
class="mb-3"
[ngClass]="{ 'd-none': activityForm.controls['type']?.value === 'INTEREST' || activityForm.controls['type']?.value === 'ITEM' || activityForm.controls['type']?.value === 'LIABILITY' }"
[ngClass]="{
'd-none':
activityForm.controls['type']?.value === 'INTEREST' ||
activityForm.controls['type']?.value === 'ITEM' ||
activityForm.controls['type']?.value === 'LIABILITY'
}"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label>
@ -252,11 +296,14 @@
</mat-select>
</div>
<mat-error
*ngIf="activityForm.controls['feeInCustomCurrency'].hasError('invalid')"
*ngIf="
activityForm.controls['feeInCustomCurrency'].hasError('invalid')
"
><ng-container i18n
>Oops! Could not get the historical exchange rate from</ng-container
>
{{ activityForm.controls['date']?.value | date: defaultDateFormat
{{
activityForm.controls['date']?.value | date: defaultDateFormat
}}</mat-error
>
</mat-form-field>
@ -265,9 +312,9 @@
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Fee</mat-label>
<input formControlName="fee" matInput type="number" />
<span class="ml-2" matTextSuffix
>{{ activityForm.controls['currency'].value }}</span
>
<span class="ml-2" matTextSuffix>{{
activityForm.controls['currency'].value
}}</span>
</mat-form-field>
</div>
<div class="mb-3">
@ -340,7 +387,7 @@
(optionSelected)="onAddTag($event)"
>
<mat-option
*ngFor="let tag of filteredTagsObservable | async"
*ngFor="let tag of filteredTagsObservable | async"
[value]="tag.id"
>
{{ tag.name }}
@ -354,7 +401,10 @@
class="flex-grow-1"
[isCurrency]="true"
[locale]="data.user?.settings?.locale"
[unit]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency"
[unit]="
activityForm.controls['currency']?.value ??
data.user?.settings?.baseCurrency
"
[value]="total"
/>
<div>

View File

@ -25,81 +25,86 @@
</ng-template>
<div class="pt-3">
@if (mode === 'DIVIDEND') {
<form
[formGroup]="uniqueAssetForm"
(ngSubmit)="onLoadDividends(stepper)"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Holding</mat-label>
<mat-select formControlName="uniqueAsset">
<mat-select-trigger
>{{ uniqueAssetForm.controls['uniqueAsset']?.value?.name
}}</mat-select-trigger
>
<mat-option
*ngFor="let holding of holdings"
class="line-height-1"
[value]="{ dataSource: holding.dataSource, name: holding.name, symbol: holding.symbol }"
>
<span><b>{{ holding.name }}</b></span>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} · {{ holding.currency
}}</small
<form
[formGroup]="uniqueAssetForm"
(ngSubmit)="onLoadDividends(stepper)"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Holding</mat-label>
<mat-select formControlName="uniqueAsset">
<mat-select-trigger>{{
uniqueAssetForm.controls['uniqueAsset']?.value?.name
}}</mat-select-trigger>
<mat-option
*ngFor="let holding of holdings"
class="line-height-1"
[value]="{
dataSource: holding.dataSource,
name: holding.name,
symbol: holding.symbol
}"
>
</mat-option>
</mat-select>
<mat-spinner
*ngIf="isLoading"
class="position-absolute"
[diameter]="20"
/>
</mat-form-field>
<span
><b>{{ holding.name }}</b></span
>
<br />
<small class="text-muted"
>{{ holding.symbol | gfSymbol }} ·
{{ holding.currency }}</small
>
</mat-option>
</mat-select>
<mat-spinner
*ngIf="isLoading"
class="position-absolute"
[diameter]="20"
/>
</mat-form-field>
<div class="d-flex flex-column justify-content-center">
<button
color="primary"
mat-flat-button
type="submit"
[disabled]="!uniqueAssetForm.valid"
>
<span i18n>Load Dividends</span>
</button>
</div>
</form>
} @else {
<div class="d-flex flex-column justify-content-center">
<button
color="primary"
mat-flat-button
type="submit"
[disabled]="!uniqueAssetForm.valid"
class="drop-area p-4 text-center text-muted"
gfFileDrop
(click)="onSelectFile(stepper)"
(filesDropped)="onFilesDropped({ stepper, files: $event })"
>
<span i18n>Load Dividends</span>
<div
class="align-items-center d-flex flex-column justify-content-center"
>
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
<span i18n>Choose or drop a file here</span>
</div>
</button>
<p class="mb-0 mt-3 text-center">
<small>
<span class="mr-1" i18n
>The following file formats are supported:</span
>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
target="_blank"
>CSV</a
>
<span class="mx-1" i18n>or</span>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
target="_blank"
>JSON</a
>
</small>
</p>
</div>
</form>
} @else {
<div class="d-flex flex-column justify-content-center">
<button
class="drop-area p-4 text-center text-muted"
gfFileDrop
(click)="onSelectFile(stepper)"
(filesDropped)="onFilesDropped({stepper, files: $event})"
>
<div
class="align-items-center d-flex flex-column justify-content-center"
>
<ion-icon class="cloud-icon" name="cloud-upload-outline" />
<span i18n>Choose or drop a file here</span>
</div>
</button>
<p class="mb-0 mt-3 text-center">
<small>
<span class="mr-1" i18n
>The following file formats are supported:</span
>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.csv"
target="_blank"
>CSV</a
>
<span class="mx-1" i18n>or</span>
<a
href="https://github.com/ghostfolio/ghostfolio/blob/main/test/import/ok.json"
target="_blank"
>JSON</a
>
</small>
</p>
</div>
}
</div>
</mat-step>
@ -114,76 +119,76 @@
>
</ng-template>
<div class="pt-3">
@if(errorMessages?.length === 0) {
<gf-activities-table
*ngIf="importStep === 1"
[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"
[showSymbolColumn]="false"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[sortDisabled]="true"
[totalItems]="totalItems"
(selectedActivities)="updateSelection($event)"
/>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="!selectedActivities?.length"
(click)="onImportActivities()"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
@if (errorMessages?.length === 0) {
<gf-activities-table
*ngIf="importStep === 1"
[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"
[showSymbolColumn]="false"
[sortColumn]="sortColumn"
[sortDirection]="sortDirection"
[sortDisabled]="true"
[totalItems]="totalItems"
(selectedActivities)="updateSelection($event)"
/>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="!selectedActivities?.length"
(click)="onImportActivities()"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
} @else {
<mat-accordion displayMode="flat">
<mat-expansion-panel
*ngFor="let message of errorMessages; let i = index"
[disabled]="!details[i]"
>
<mat-expansion-panel-header class="pl-1">
<mat-panel-title>
<div class="d-flex">
<div class="align-items-center d-flex mr-2">
<ion-icon name="warning-outline" />
<mat-accordion displayMode="flat">
<mat-expansion-panel
*ngFor="let message of errorMessages; let i = index"
[disabled]="!details[i]"
>
<mat-expansion-panel-header class="pl-1">
<mat-panel-title>
<div class="d-flex">
<div class="align-items-center d-flex mr-2">
<ion-icon name="warning-outline" />
</div>
<div>{{ message }}</div>
</div>
<div>{{ message }}</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<pre
*ngIf="details[i]"
class="m-0"
><code>{{ details[i] | json }}</code></pre>
</mat-expansion-panel>
</mat-accordion>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="true"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<pre
*ngIf="details[i]"
class="m-0"
><code>{{ details[i] | json }}</code></pre>
</mat-expansion-panel>
</mat-accordion>
<div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container>
</button>
<button
class="ml-1"
color="primary"
mat-flat-button
[disabled]="true"
>
<ng-container i18n>Import</ng-container>
</button>
</div>
}
</div>
</mat-step>

View File

@ -15,13 +15,20 @@
class="justify-content-end l-2"
size="medium"
[isPercent]="true"
[value]="isLoading ? undefined : portfolioDetails?.filteredValueInPercentage"
[value]="
isLoading
? undefined
: portfolioDetails?.filteredValueInPercentage
"
/>
</mat-card-header>
<mat-card-content>
<mat-progress-bar
mode="determinate"
[title]="(portfolioDetails?.filteredValueInPercentage * 100).toFixed(2) + '%'"
[title]="
(portfolioDetails?.filteredValueInPercentage * 100).toFixed(2) +
'%'
"
[value]="portfolioDetails?.filteredValueInPercentage * 100"
/>
</mat-card-content>
@ -204,7 +211,9 @@
<gf-world-map-chart
[countries]="countries"
[format]="worldMapChartFormat"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[isInPercent]="
hasImpersonationId || user.settings.isRestrictedView
"
[locale]="user?.settings?.locale"
/>
</div>

View File

@ -19,105 +19,134 @@
</div>
@if (user?.settings?.isExperimentalFeatures) {
<div class="mb-5 row">
<div class="col">
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="d-flex py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Absolute Asset Performance
<div class="mb-5 row">
<div class="col">
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="d-flex py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Absolute Asset Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformance
"
/>
</div>
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformance"
/>
<div class="d-flex mb-3 ml-3 py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Asset Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformancePercent
"
/>
</div>
</div>
</div>
<div class="d-flex mb-3 ml-3 py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Asset Performance
<div class="d-flex py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Absolute Currency Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformanceWithCurrencyEffect ===
null
? null
: performance?.currentNetPerformanceWithCurrencyEffect -
performance?.currentNetPerformance
"
/>
</div>
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercent"
/>
<div class="d-flex ml-3 py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Currency Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformancePercentWithCurrencyEffect -
performance?.currentNetPerformancePercent
"
/>
</div>
</div>
</div>
<div class="d-flex py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Absolute Currency Performance
<div><hr /></div>
<div class="d-flex py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Absolute Net Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformanceWithCurrencyEffect
"
/>
</div>
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="isLoadingInvestmentChart ? undefined : (performance?.currentNetPerformanceWithCurrencyEffect === null ? null : performance?.currentNetPerformanceWithCurrencyEffect - performance?.currentNetPerformance)"
/>
<div class="d-flex ml-3 py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Net Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="
isLoadingInvestmentChart
? undefined
: performance?.currentNetPerformancePercentWithCurrencyEffect
"
/>
</div>
</div>
</div>
<div class="d-flex ml-3 py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Currency Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect - performance?.currentNetPerformancePercent"
/>
</div>
</div>
<div><hr /></div>
<div class="d-flex py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Absolute Net Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[isCurrency]="true"
[locale]="user?.settings?.locale"
[unit]="user?.settings?.baseCurrency"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformanceWithCurrencyEffect"
/>
</div>
</div>
<div class="d-flex ml-3 py-1">
<div class="flex-grow-1 mr-2 text-truncate" i18n>
Net Performance
</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="user?.settings?.locale"
[value]="isLoadingInvestmentChart ? undefined : performance?.currentNetPerformancePercentWithCurrencyEffect"
/>
</div>
</div>
</mat-card-content>
</mat-card>
</mat-card-content>
</mat-card>
</div>
</div>
</div>
}
<div class="mb-5 row">

View File

@ -16,11 +16,14 @@
[currency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[fireWealth]="fireWealth?.toNumber()"
[hasPermissionToUpdateUserSettings]="!hasImpersonationId && hasPermissionToUpdateUserSettings"
[hasPermissionToUpdateUserSettings]="
!hasImpersonationId && hasPermissionToUpdateUserSettings
"
[locale]="user?.settings?.locale"
[ngStyle]="{
opacity: user?.subscription?.type === 'Basic' ? '0.67' : 'initial',
'pointer-events': user?.subscription?.type === 'Basic' ? 'none' : 'initial'
'pointer-events':
user?.subscription?.type === 'Basic' ? 'none' : 'initial'
}"
[projectedTotalAmount]="user?.settings?.projectedTotalAmount"
[retirementDate]="user?.settings?.retirementDate"

View File

@ -14,15 +14,15 @@
[locale]="user?.settings?.locale"
/>
@if (hasPermissionToCreateOrder && holdings?.length > 0) {
<div class="text-center">
<a
class="mt-3"
i18n
mat-stroked-button
[routerLink]="['/portfolio', 'activities']"
>Manage Activities</a
>
</div>
<div class="text-center">
<a
class="mt-3"
i18n
mat-stroked-button
[routerLink]="['/portfolio', 'activities']"
>Manage Activities</a
>
</div>
}
</div>
</div>

View File

@ -21,7 +21,7 @@
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>

View File

@ -107,7 +107,7 @@
<mat-card
appearance="outlined"
class="h-100"
[ngClass]="{ 'active': user?.subscription?.type === 'Basic' }"
[ngClass]="{ active: user?.subscription?.type === 'Basic' }"
>
<mat-card-content class="d-flex flex-column h-100">
<div class="flex-grow-1">
@ -164,7 +164,7 @@
<mat-card
appearance="outlined"
class="h-100"
[ngClass]="{ 'active': user?.subscription?.type === 'Premium' }"
[ngClass]="{ active: user?.subscription?.type === 'Premium' }"
>
<mat-card-content class="d-flex flex-column h-100">
<div class="flex-grow-1">
@ -243,19 +243,22 @@
<ng-container *ngIf="coupon"
><del class="text-muted"
>{{ baseCurrency }}&nbsp;{{ price }}</del
>&nbsp;{{ baseCurrency }}&nbsp;<strong
>{{ price - coupon }}</strong
>
>&nbsp;{{ baseCurrency }}&nbsp;<strong>{{
price - coupon
}}</strong>
</ng-container>
<ng-container *ngIf="!coupon"
>{{ baseCurrency }}&nbsp;<strong
>{{ price }}</strong
></ng-container
>{{ baseCurrency }}&nbsp;<strong>{{
price
}}</strong></ng-container
>&nbsp;<span i18n>per year</span></span
>
</p>
<div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription?.type === 'Basic'"
*ngIf="
hasPermissionToUpdateUserSettings &&
user?.subscription?.type === 'Basic'
"
class="mt-3 text-center"
>
<button color="primary" mat-flat-button (click)="onCheckout()">
@ -265,7 +268,10 @@
>Upgrade Plan</ng-container
>
<ng-container
*ngIf="user.subscription.offer === 'renewal' || user.subscription.offer === 'renewal-early-bird'"
*ngIf="
user.subscription.offer === 'renewal' ||
user.subscription.offer === 'renewal-early-bird'
"
i18n
>Renew Plan</ng-container
>

View File

@ -1,8 +1,8 @@
<h1 mat-dialog-title>
<span i18n>Create Account</span
><span *ngIf="data.role === 'ADMIN'" class="badge badge-light ml-2"
>{{ data.role }}</span
>
><span *ngIf="data.role === 'ADMIN'" class="badge badge-light ml-2">{{
data.role
}}</span>
</h1>
<div class="py-3" mat-dialog-content>
<div>

View File

@ -19,32 +19,38 @@
</p>
</div>
@for (product of products; track product) {
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden w-100"
title="Compare Ghostfolio to {{ product.name }} - {{ product.slogan }}"
[routerLink]="[pathResources, 'personal-finance-tools', pathAlternativeTo + (product.alias ?? product.key)]"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate" i18n>
Open Source Alternative to {{ product.name }}
<mat-card appearance="outlined" class="mb-3">
<mat-card-content>
<div class="container p-0">
<div class="flex-nowrap no-gutters row">
<a
class="d-flex overflow-hidden w-100"
title="Compare Ghostfolio to {{ product.name }} - {{
product.slogan
}}"
[routerLink]="[
pathResources,
'personal-finance-tools',
pathAlternativeTo + (product.alias ?? product.key)
]"
>
<div class="flex-grow-1 overflow-hidden">
<div class="h6 m-0 text-truncate" i18n>
Open Source Alternative to {{ product.name }}
</div>
</div>
</div>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
<div class="align-items-center d-flex">
<ion-icon
class="chevron text-muted"
name="chevron-forward-outline"
size="small"
/>
</div>
</a>
</div>
</div>
</div>
</mat-card-content>
</mat-card>
</mat-card-content>
</mat-card>
}
</div>
</div>

View File

@ -11,8 +11,9 @@
</div>
<section class="mb-4">
<p i18n>
Are you looking for an open source alternative to {{ product2.name
}}? <a [routerLink]="routerLinkAbout">Ghostfolio</a> is a powerful
Are you looking for an open source alternative to
{{ product2.name }}?
<a [routerLink]="routerLinkAbout">Ghostfolio</a> is a powerful
portfolio management tool that provides individuals with a
comprehensive platform to track, analyze, and optimize their
investments. Whether you are an experienced investor or just
@ -35,18 +36,22 @@
its capabilities, security, and user experience.
</p>
<p i18n>
Lets dive deeper into the detailed Ghostfolio vs {{ product2.name
}} comparison table below to gain a thorough understanding of how
Ghostfolio positions itself relative to {{ product2.name }}. We will
explore various aspects such as features, data privacy, pricing, and
more, allowing you to make a well-informed choice for your personal
requirements.
Lets dive deeper into the detailed Ghostfolio vs
{{ product2.name }} comparison table below to gain a thorough
understanding of how Ghostfolio positions itself relative to
{{ product2.name }}. We will explore various aspects such as
features, data privacy, pricing, and more, allowing you to make a
well-informed choice for your personal requirements.
</p>
</section>
<section class="mb-4">
<table class="gf-table w-100">
<caption class="text-center" i18n>
Ghostfolio vs {{ product2.name }} comparison table
Ghostfolio vs
{{
product2.name
}}
comparison table
</caption>
<thead>
<tr class="mat-mdc-header-row">
@ -187,8 +192,9 @@
</td>
<td class="mat-mdc-cell px-1 py-2">
<ng-container *ngIf="product2.pricingPerYear"
><span i18n>Starting from</span> {{ product2.pricingPerYear
}} / <span i18n>year</span></ng-container
><span i18n>Starting from</span>
{{ product2.pricingPerYear }} /
<span i18n>year</span></ng-container
>
</td>
</tr>
@ -202,13 +208,13 @@
</section>
<section class="mb-4">
<p i18n>
Please note that the information provided in the Ghostfolio vs {{
product2.name }} comparison table is based on our independent
research and analysis. This website is not affiliated with {{
product2.name }} or any other product mentioned in the comparison.
As the landscape of personal finance tools evolves, it is essential
to verify any specific details or changes directly from the
respective product page. Data needs a refresh? Help us maintain
Please note that the information provided in the Ghostfolio vs
{{ product2.name }} comparison table is based on our independent
research and analysis. This website is not affiliated with
{{ product2.name }} or any other product mentioned in the
comparison. As the landscape of personal finance tools evolves, it
is essential to verify any specific details or changes directly from
the respective product page. Data needs a refresh? Help us maintain
accurate data on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</p>

View File

@ -171,21 +171,21 @@
</div>
</div>
@if (hasPermissionForSubscription) {
<div class="mb-4 media">
<div class="media-body">
<h3 class="h5 mt-0">Personal Finance Tools</h3>
<div class="mb-1">
Personal finance tools are software applications that help
individuals manage their money, track expenses, set budgets,
monitor investments, and make informed financial decisions.
</div>
<div>
<a [routerLink]="routerLinkResourcesPersonalFinanceTools"
>Personal Finance Tools →</a
>
<div class="mb-4 media">
<div class="media-body">
<h3 class="h5 mt-0">Personal Finance Tools</h3>
<div class="mb-1">
Personal finance tools are software applications that help
individuals manage their money, track expenses, set budgets,
monitor investments, and make informed financial decisions.
</div>
<div>
<a [routerLink]="routerLinkResourcesPersonalFinanceTools"
>Personal Finance Tools →</a
>
</div>
</div>
</div>
</div>
}
<div class="mb-4 media">
<div class="media-body">

View File

@ -21,7 +21,7 @@
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>

View File

@ -8,29 +8,29 @@
</div>
@if (!hasError) {
<div class="col d-flex justify-content-center">
<mat-spinner [diameter]="20" />
</div>
<div class="col d-flex justify-content-center">
<mat-spinner [diameter]="20" />
</div>
} @else {
<div
class="align-items-center col d-flex flex-column justify-content-center"
>
<h1 class="d-flex h5 justify-content-center mb-0 text-center">
<ng-container i18n>Oops, authentication has failed.</ng-container>
</h1>
<button
class="mb-3 mt-4"
color="primary"
mat-flat-button
(click)="signIn()"
<div
class="align-items-center col d-flex flex-column justify-content-center"
>
<ng-container i18n>Try again</ng-container>
</button>
<div class="text-muted"><ng-container i18n>or</ng-container></div>
<button class="mt-1" mat-flat-button (click)="deregisterDevice()">
<ng-container i18n>Go back to Home Page</ng-container>
</button>
</div>
<h1 class="d-flex h5 justify-content-center mb-0 text-center">
<ng-container i18n>Oops, authentication has failed.</ng-container>
</h1>
<button
class="mb-3 mt-4"
color="primary"
mat-flat-button
(click)="signIn()"
>
<ng-container i18n>Try again</ng-container>
</button>
<div class="text-muted"><ng-container i18n>or</ng-container></div>
<button class="mt-1" mat-flat-button (click)="deregisterDevice()">
<ng-container i18n>Go back to Home Page</ng-container>
</button>
</div>
}
</div>
</div>

View File

@ -21,7 +21,7 @@
>
<ion-icon
[name]="tab.iconName"
[size]="deviceType === 'mobile' ? 'large': 'small'"
[size]="deviceType === 'mobile' ? 'large' : 'small'"
/>
<div class="d-none d-sm-block ml-2">{{ tab.label }}</div>
</a>

View File

@ -6,6 +6,7 @@ import {
DATE_FORMAT_MONTHLY,
DATE_FORMAT_YEARLY,
getBackgroundColor,
getLocale,
getTextColor
} from './helper';
import { ColorScheme, GroupBy } from './types';
@ -30,7 +31,7 @@ export function getTooltipOptions({
colorScheme,
currency = '',
groupBy,
locale = '',
locale = getLocale(),
unit = ''
}: {
colorScheme?: ColorScheme;

View File

@ -217,9 +217,7 @@ export function getEmojiFlag(aCountryCode: string) {
}
export function getLocale() {
return navigator.languages?.length
? navigator.languages[0]
: navigator.language ?? locale;
return navigator.language ?? locale;
}
export function getNumberFormatDecimal(aLocale?: string) {
@ -230,7 +228,7 @@ export function getNumberFormatDecimal(aLocale?: string) {
}).value;
}
export function getNumberFormatGroup(aLocale?: string) {
export function getNumberFormatGroup(aLocale = getLocale()) {
const formatObject = new Intl.NumberFormat(aLocale).formatToParts(9999.99);
return formatObject.find((object) => {

View File

@ -1,3 +1,4 @@
import { getLocale } from '@ghostfolio/common/helper';
import { AccountBalancesResponse } from '@ghostfolio/common/interfaces';
import {
@ -25,7 +26,7 @@ import { Subject } from 'rxjs';
export class AccountBalancesComponent implements OnChanges, OnDestroy, OnInit {
@Input() accountBalances: AccountBalancesResponse['balances'];
@Input() accountId: string;
@Input() locale: string;
@Input() locale = getLocale();
@Input() showActions = true;
@Output() accountBalanceDeleted = new EventEmitter<string>();

View File

@ -1,6 +1,6 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { getDateFormatString, getLocale } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
@ -40,7 +40,7 @@ export class ActivitiesTableComponent
@Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToExportActivities: boolean;
@Input() hasPermissionToOpenDetails = true;
@Input() locale: string;
@Input() locale = getLocale();
@Input() pageIndex: number;
@Input() pageSize = DEFAULT_PAGE_SIZE;
@Input() showActions = true;

View File

@ -4,12 +4,13 @@
[queryParams]="queryParams"
[routerLink]="routerLink"
(click)="onClick()"
><span><b>{{ item?.name }}</b></span>
><span
><b>{{ item?.name }}</b></span
>
<br />
<small class="text-muted"
>{{ item?.symbol | gfSymbol }} · {{ item?.currency }}<ng-container
*ngIf="item?.assetSubClassString"
>
>{{ item?.symbol | gfSymbol }} · {{ item?.currency
}}<ng-container *ngIf="item?.assetSubClassString">
· {{ item?.assetSubClassString }}</ng-container
></small
></a

View File

@ -39,7 +39,7 @@
</button>
</div>
<div
*ngIf="isLoading || searchFormControl.value"
*ngIf="isLoading || searchFormControl.value"
class="overflow-auto py-3 result-container"
>
<div>
@ -56,9 +56,9 @@
animation="pulse"
class="mx-2"
[theme]="{
height: '1.5rem',
width: '100%'
}"
height: '1.5rem',
width: '100%'
}"
/>
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
</ng-container>
@ -77,9 +77,9 @@
animation="pulse"
class="mx-2"
[theme]="{
height: '1.5rem',
width: '100%'
}"
height: '1.5rem',
width: '100%'
}"
/>
<div *ngIf="!isLoading" class="px-2 py-1" i18n>No entries...</div>
</ng-container>
@ -87,7 +87,7 @@
</div>
</div>
<form [formGroup]="filterForm">
<ng-container *ngIf="!(isLoading || searchFormControl.value)">
<ng-container *ngIf="!(isLoading || searchFormControl.value)">
<div class="date-range-selector-container p-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Date Range</mat-label>
@ -96,7 +96,7 @@
(selectionChange)="onChangeDateRange($event.value)"
>
@for (range of dateRangeOptions; track range) {
<mat-option [value]="range.value">{{ range.label }}</mat-option>
<mat-option [value]="range.value">{{ range.label }}</mat-option>
}
</mat-select>
</mat-form-field>
@ -108,16 +108,16 @@
<mat-select formControlName="account">
<mat-option [value]="null" />
@for (account of accounts; track account.id) {
<mat-option [value]="account.id">
<div class="d-flex">
<gf-symbol-icon
*ngIf="account.Platform?.url"
class="mr-1"
[tooltip]="account.Platform?.name"
[url]="account.Platform?.url"
/><span>{{ account.name }}</span>
</div>
</mat-option>
<mat-option [value]="account.id">
<div class="d-flex">
<gf-symbol-icon
*ngIf="account.Platform?.url"
class="mr-1"
[tooltip]="account.Platform?.name"
[url]="account.Platform?.url"
/><span>{{ account.name }}</span>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
@ -128,7 +128,7 @@
<mat-select formControlName="tag">
<mat-option [value]="null" />
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
}
</mat-select>
</mat-form-field>
@ -139,9 +139,9 @@
<mat-select formControlName="assetClass">
<mat-option [value]="null" />
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id"
>{{ assetClass.label }}</mat-option
>
<mat-option [value]="assetClass.id">{{
assetClass.label
}}</mat-option>
}
</mat-select>
</mat-form-field>

View File

@ -1,4 +1,4 @@
import { resolveMarketCondition } from '@ghostfolio/common/helper';
import { getLocale, resolveMarketCondition } from '@ghostfolio/common/helper';
import { Benchmark, User } from '@ghostfolio/common/interfaces';
import {
@ -16,7 +16,7 @@ import {
})
export class BenchmarkComponent implements OnChanges {
@Input() benchmarks: Benchmark[];
@Input() locale: string;
@Input() locale = getLocale();
@Input() user: User;
public displayedColumns = ['name', 'date', 'change', 'marketCondition'];

View File

@ -3,6 +3,7 @@ import {
transformTickToAbbreviation
} from '@ghostfolio/common/chart-helper';
import { primaryColorRgb } from '@ghostfolio/common/config';
import { getLocale } from '@ghostfolio/common/helper';
import { ColorScheme } from '@ghostfolio/common/types';
import {
@ -55,7 +56,7 @@ export class FireCalculatorComponent implements OnChanges, OnDestroy {
@Input() deviceType: string;
@Input() fireWealth: number;
@Input() hasPermissionToUpdateUserSettings: boolean;
@Input() locale: string;
@Input() locale = getLocale();
@Input() projectedTotalAmount = 0;
@Input() retirementDate: Date;
@Input() savingsRate = 0;

View File

@ -1,3 +1,4 @@
import { getLocale } from '@ghostfolio/common/helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
import {
@ -29,7 +30,7 @@ export class HoldingsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() hasPermissionToOpenDetails = true;
@Input() hasPermissionToShowValues = true;
@Input() holdings: PortfolioPosition[];
@Input() locale: string;
@Input() locale = getLocale();
@Input() pageSize = Number.MAX_SAFE_INTEGER;
@ViewChild(MatPaginator) paginator: MatPaginator;

View File

@ -3,14 +3,11 @@ import {
getTooltipPositionerMapTop,
getVerticalHoverLinePlugin
} from '@ghostfolio/common/chart-helper';
import {
locale,
primaryColorRgb,
secondaryColorRgb
} from '@ghostfolio/common/config';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import {
getBackgroundColor,
getDateFormatString,
getLocale,
getTextColor
} from '@ghostfolio/common/helper';
import { LineChartItem } from '@ghostfolio/common/interfaces';
@ -51,7 +48,7 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
@Input() currency: string;
@Input() historicalDataItems: LineChartItem[];
@Input() isAnimated = false;
@Input() locale: string;
@Input() locale = getLocale();
@Input() showGradient = false;
@Input() showLegend = false;
@Input() showLoader = true;
@ -106,10 +103,6 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
this.changeDetectorRef.markForCheck();
});
}
if (!this.locale) {
this.locale = locale;
}
}
public ngOnDestroy() {

View File

@ -1,6 +1,6 @@
import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getTextColor } from '@ghostfolio/common/helper';
import { getLocale, getTextColor } from '@ghostfolio/common/helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
import { ColorScheme } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
@ -41,7 +41,7 @@ export class PortfolioProportionChartComponent
@Input() cursor: string;
@Input() isInPercent = false;
@Input() keys: string[] = [];
@Input() locale = '';
@Input() locale = getLocale();
@Input() maxItems?: number;
@Input() showLabels = false;
@Input() positions: {

View File

@ -21,7 +21,7 @@ export class ValueComponent implements OnChanges {
@Input() isCurrency = false;
@Input() isDate = false;
@Input() isPercent = false;
@Input() locale: string | undefined;
@Input() locale = getLocale();
@Input() position = '';
@Input() precision: number | undefined;
@Input() size: 'large' | 'medium' | 'small' = 'small';
@ -129,11 +129,6 @@ export class ValueComponent implements OnChanges {
this.formattedValue = '';
this.isNumber = false;
this.isString = false;
if (!this.locale) {
this.locale = getLocale();
}
this.useAbsoluteValue = false;
}
}

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.53.1",
"version": "2.55.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -25,7 +25,7 @@
"database:push": "prisma db push",
"database:seed": "prisma db seed",
"database:setup": "yarn database:push && yarn database:seed",
"database:validate": "prisma validate",
"database:validate-schema": "prisma validate",
"dep-graph": "nx dep-graph",
"e2e": "ng e2e",
"extract-locales": "nx run client:extract-i18n --output-path ./apps/client/src/locales",

View File

@ -0,0 +1,11 @@
-- CreateIndex
CREATE INDEX "Account_id_idx" ON "Account"("id");
-- CreateIndex
CREATE INDEX "MarketData_dataSource_idx" ON "MarketData"("dataSource");
-- CreateIndex
CREATE INDEX "MarketData_date_idx" ON "MarketData"("date");
-- CreateIndex
CREATE INDEX "Order_accountId_idx" ON "Order"("accountId");

View File

@ -0,0 +1,86 @@
-- CreateIndex
CREATE INDEX "Access_alias_idx" ON "Access"("alias");
-- CreateIndex
CREATE INDEX "Access_granteeUserId_idx" ON "Access"("granteeUserId");
-- CreateIndex
CREATE INDEX "Access_userId_idx" ON "Access"("userId");
-- CreateIndex
CREATE INDEX "Account_currency_idx" ON "Account"("currency");
-- CreateIndex
CREATE INDEX "Account_name_idx" ON "Account"("name");
-- CreateIndex
CREATE INDEX "Account_userId_idx" ON "Account"("userId");
-- CreateIndex
CREATE INDEX "AccountBalance_accountId_idx" ON "AccountBalance"("accountId");
-- CreateIndex
CREATE INDEX "AccountBalance_date_idx" ON "AccountBalance"("date");
-- CreateIndex
CREATE INDEX "Analytics_updatedAt_idx" ON "Analytics"("updatedAt");
-- CreateIndex
CREATE INDEX "AuthDevice_userId_idx" ON "AuthDevice"("userId");
-- CreateIndex
CREATE INDEX "MarketData_marketPrice_idx" ON "MarketData"("marketPrice");
-- CreateIndex
CREATE INDEX "MarketData_state_idx" ON "MarketData"("state");
-- CreateIndex
CREATE INDEX "Order_date_idx" ON "Order"("date");
-- CreateIndex
CREATE INDEX "Order_isDraft_idx" ON "Order"("isDraft");
-- CreateIndex
CREATE INDEX "Order_userId_idx" ON "Order"("userId");
-- CreateIndex
CREATE INDEX "Platform_name_idx" ON "Platform"("name");
-- CreateIndex
CREATE INDEX "Subscription_userId_idx" ON "Subscription"("userId");
-- CreateIndex
CREATE INDEX "SymbolProfile_assetClass_idx" ON "SymbolProfile"("assetClass");
-- CreateIndex
CREATE INDEX "SymbolProfile_currency_idx" ON "SymbolProfile"("currency");
-- CreateIndex
CREATE INDEX "SymbolProfile_dataSource_idx" ON "SymbolProfile"("dataSource");
-- CreateIndex
CREATE INDEX "SymbolProfile_isin_idx" ON "SymbolProfile"("isin");
-- CreateIndex
CREATE INDEX "SymbolProfile_name_idx" ON "SymbolProfile"("name");
-- CreateIndex
CREATE INDEX "SymbolProfile_symbol_idx" ON "SymbolProfile"("symbol");
-- CreateIndex
CREATE INDEX "Tag_name_idx" ON "Tag"("name");
-- CreateIndex
CREATE INDEX "User_accessToken_idx" ON "User"("accessToken");
-- CreateIndex
CREATE INDEX "User_createdAt_idx" ON "User"("createdAt");
-- CreateIndex
CREATE INDEX "User_provider_idx" ON "User"("provider");
-- CreateIndex
CREATE INDEX "User_role_idx" ON "User"("role");
-- CreateIndex
CREATE INDEX "User_thirdPartyId_idx" ON "User"("thirdPartyId");

View File

@ -19,6 +19,10 @@ model Access {
userId String
GranteeUser User? @relation("accessGet", fields: [granteeUserId], references: [id])
User User @relation("accessGive", fields: [userId], references: [id])
@@index([alias])
@@index([granteeUserId])
@@index([userId])
}
model Account {
@ -39,6 +43,10 @@ model Account {
Order Order[]
@@id([id, userId])
@@index([currency])
@@index([id])
@@index([name])
@@index([userId])
}
model AccountBalance {
@ -50,6 +58,9 @@ model AccountBalance {
userId String
value Float
Account Account @relation(fields: [accountId, userId], onDelete: Cascade, references: [id, userId])
@@index([accountId])
@@index([date])
}
model Analytics {
@ -58,6 +69,8 @@ model Analytics {
updatedAt DateTime @updatedAt
userId String @id
User User @relation(fields: [userId], references: [id])
@@index([updatedAt])
}
model AuthDevice {
@ -69,6 +82,8 @@ model AuthDevice {
updatedAt DateTime @updatedAt
userId String
User User @relation(fields: [userId], references: [id])
@@index([userId])
}
model MarketData {
@ -81,6 +96,10 @@ model MarketData {
symbol String
@@unique([dataSource, date, symbol])
@@index([dataSource])
@@index([date])
@@index([marketPrice])
@@index([state])
@@index([symbol])
}
@ -103,6 +122,11 @@ model Order {
SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id])
User User @relation(fields: [userId], references: [id])
tags Tag[]
@@index([accountId])
@@index([date])
@@index([isDraft])
@@index([userId])
}
model Platform {
@ -110,6 +134,8 @@ model Platform {
name String?
url String @unique
Account Account[]
@@index([name])
}
model Property {
@ -148,6 +174,12 @@ model SymbolProfile {
SymbolProfileOverrides SymbolProfileOverrides?
@@unique([dataSource, symbol])
@@index([assetClass])
@@index([currency])
@@index([dataSource])
@@index([isin])
@@index([name])
@@index([symbol])
}
model SymbolProfileOverrides {
@ -170,12 +202,16 @@ model Subscription {
updatedAt DateTime @updatedAt
userId String
User User @relation(fields: [userId], references: [id])
@@index([userId])
}
model Tag {
id String @id @default(uuid())
name String @unique
orders Order[]
@@index([name])
}
model User {
@ -195,6 +231,12 @@ model User {
Order Order[]
Settings Settings?
Subscription Subscription[]
@@index([accessToken])
@@index([createdAt])
@@index([provider])
@@index([role])
@@index([thirdPartyId])
}
enum AccessPermission {