This commit is contained in:
124
CHANGELOG.md
124
CHANGELOG.md
@@ -7,16 +7,140 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Set up the language localization for the static portfolio analysis rule: _Regional Market Cluster Risks_ (Asia-Pacific)
|
||||
- Set up the language localization for the static portfolio analysis rule: _Regional Market Cluster Risks_ (Emerging Markets)
|
||||
- Set up the language localization for the static portfolio analysis rule: _Regional Market Cluster Risks_ (Europe)
|
||||
- Set up the language localization for the static portfolio analysis rule: _Regional Market Cluster Risks_ (Japan)
|
||||
- Set up the language localization for the static portfolio analysis rule: _Regional Market Cluster Risks_ (North America)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization for Catalan (`ca`)
|
||||
- Improved the language localization for Dutch (`nl`)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Improved the language localization for Spanish (`es`)
|
||||
- Upgraded `countries-and-timezones` from version `3.7.2` to `3.8.0`
|
||||
|
||||
## 2.183.0 - 2025-07-20
|
||||
|
||||
### Added
|
||||
|
||||
- Set up the language localization for the static portfolio analysis rule: _Economic Market Cluster Risks_ (Developed Markets)
|
||||
- Set up the language localization for the static portfolio analysis rule: _Economic Market Cluster Risks_ (Emerging Markets)
|
||||
|
||||
### Changed
|
||||
|
||||
- Extended the export functionality by custom asset profiles
|
||||
- Improved the platform icon in the create or update platform dialog of the admin control
|
||||
- Localized the durations of the coupon system
|
||||
- Refactored the admin pages to standalone
|
||||
- Refactored the Frequently Asked Questions (FAQ) pages to standalone
|
||||
- Refactored the home pages to standalone
|
||||
- Refactored the resources pages to standalone
|
||||
- Refactored the access table component to standalone
|
||||
- Refactored the accounts table component to standalone
|
||||
- Improved the language localization for Catalan (`ca`)
|
||||
- Improved the language localization for Dutch (`nl`)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Improved the language localization for Italian (`it`)
|
||||
- Improved the language localization for Portuguese (`pt`)
|
||||
- Improved the language localization for Spanish (`es`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the horizontal ellipsis icon in the accounts table component
|
||||
- Fixed the quantity value in the update activity dialog
|
||||
- Fixed the static portfolio analysis rule for no accounts: _Account Cluster Risks_ (Current Investment)
|
||||
- Fixed the static portfolio analysis rule for no accounts: _Account Cluster Risks_ (Single Account)
|
||||
|
||||
## 2.182.0 - 2025-07-16
|
||||
|
||||
### Added
|
||||
|
||||
- Added a message to the assistant if no results have been found
|
||||
- Added the category title to the settings dialog to customize the rule thresholds of the _X-ray_ page (experimental)
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the label for asset profiles with `MANUAL` data source in the chart of the asset profile details dialog in the admin control panel
|
||||
- Improved the label for asset profiles with `MANUAL` data source in the chart of the holding detail dialog
|
||||
- Skipped errors for the custom asset profiles in the portfolio snapshot calculation
|
||||
- Removed the date range query parameter from the search for the holdings in the assistant
|
||||
- Improved the language localization for Chinese (`zh`)
|
||||
- Improved the language localization for Dutch (`nl`)
|
||||
- Improved the language localization for French (`fr`)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Improved the language localization for Portuguese (`pt`)
|
||||
- Improved the language localization for Spanish (`es`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue with the clone functionality related to a custom asset profile activity
|
||||
|
||||
## 2.181.0 - 2025-07-11
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the portfolio calculations for activities without historical market data
|
||||
- Improved the asset profile dialog’s asset sub class selector of the admin control panel to update the options dynamically based on the selected asset class
|
||||
- Improved the asset profile dialog’s data gathering checkbox of the admin control panel to reflect the global settings
|
||||
- Improved the language localization for Catalan (`ca`)
|
||||
- Improved the language localization for Chinese (`zh`)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Improved the language localization for Italian (`it`)
|
||||
- Improved the language localization for Portuguese (`pt`)
|
||||
- Improved the language localization for Spanish (`es`)
|
||||
- Improved the language localization for Turkish (`tr`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed an issue in the biometric authentication related to matching passkeys
|
||||
|
||||
## 2.180.0 - 2025-07-08
|
||||
|
||||
### Added
|
||||
|
||||
- Added alternative investment as an asset class
|
||||
- Added collectible as an asset sub class
|
||||
|
||||
### Changed
|
||||
|
||||
- Respected the filter by account for accounts when exporting activities on the portfolio activities page
|
||||
- Improved the label for asset profiles with `MANUAL` data source in the chart of the holdings tab on the home page
|
||||
- Renamed `AccessGive` to `accessesGive` in the `User` database schema
|
||||
- Improved the language localization for Catalan (`ca`)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Improved the language localization for Spanish (`es`)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the export functionality for accounts without activities
|
||||
|
||||
## 2.179.0 - 2025-07-07
|
||||
|
||||
### Added
|
||||
|
||||
- Added a _Manage Asset Profile_ button for administrators to the holding detail dialog
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the language localization in the users table of the admin control panel
|
||||
- Refactored the accounts pages to standalone
|
||||
- Refactored the portfolio pages to standalone
|
||||
- Refactored the user account pages to standalone
|
||||
- Renamed `Settings` to `settings` in the `User` database schema
|
||||
- Improved the language localization for Catalan (`ca`)
|
||||
- Improved the language localization for Dutch (`nl`)
|
||||
- Improved the language localization for Español (`es`)
|
||||
- Improved the language localization for German (`de`)
|
||||
- Upgraded `ionicons` from version `7.4.0` to `8.0.10`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the allocations by asset class for unknown asset classes on the allocations page
|
||||
|
||||
## 2.178.0 - 2025-07-05
|
||||
|
||||
### Changed
|
||||
|
@@ -133,17 +133,19 @@ export class AuthController {
|
||||
return this.webAuthService.verifyAttestation(body.credential);
|
||||
}
|
||||
|
||||
@Post('webauthn/generate-assertion-options')
|
||||
public async generateAssertionOptions(@Body() body: { deviceId: string }) {
|
||||
return this.webAuthService.generateAssertionOptions(body.deviceId);
|
||||
@Post('webauthn/generate-authentication-options')
|
||||
public async generateAuthenticationOptions(
|
||||
@Body() body: { deviceId: string }
|
||||
) {
|
||||
return this.webAuthService.generateAuthenticationOptions(body.deviceId);
|
||||
}
|
||||
|
||||
@Post('webauthn/verify-assertion')
|
||||
public async verifyAssertion(
|
||||
@Post('webauthn/verify-authentication')
|
||||
public async verifyAuthentication(
|
||||
@Body() body: { deviceId: string; credential: AssertionCredentialJSON }
|
||||
) {
|
||||
try {
|
||||
const authToken = await this.webAuthService.verifyAssertion(
|
||||
const authToken = await this.webAuthService.verifyAuthentication(
|
||||
body.deviceId,
|
||||
body.credential
|
||||
);
|
||||
|
@@ -25,6 +25,7 @@ import {
|
||||
VerifyRegistrationResponseOpts
|
||||
} from '@simplewebauthn/server';
|
||||
import { isoBase64URL, isoUint8Array } from '@simplewebauthn/server/helpers';
|
||||
import ms from 'ms';
|
||||
|
||||
import {
|
||||
AssertionCredentialJSON,
|
||||
@@ -41,42 +42,42 @@ export class WebAuthService {
|
||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||
) {}
|
||||
|
||||
get rpID() {
|
||||
return new URL(this.configurationService.get('ROOT_URL')).hostname;
|
||||
private get expectedOrigin() {
|
||||
return this.configurationService.get('ROOT_URL');
|
||||
}
|
||||
|
||||
get expectedOrigin() {
|
||||
return this.configurationService.get('ROOT_URL');
|
||||
private get rpID() {
|
||||
return new URL(this.configurationService.get('ROOT_URL')).hostname;
|
||||
}
|
||||
|
||||
public async generateRegistrationOptions() {
|
||||
const user = this.request.user;
|
||||
|
||||
const opts: GenerateRegistrationOptionsOpts = {
|
||||
rpName: 'Ghostfolio',
|
||||
rpID: this.rpID,
|
||||
userID: isoUint8Array.fromUTF8String(user.id),
|
||||
userName: '',
|
||||
timeout: 60000,
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: 'platform',
|
||||
requireResidentKey: false,
|
||||
userVerification: 'required'
|
||||
}
|
||||
residentKey: 'required',
|
||||
userVerification: 'preferred'
|
||||
},
|
||||
rpID: this.rpID,
|
||||
rpName: 'Ghostfolio',
|
||||
timeout: ms('60 seconds'),
|
||||
userID: isoUint8Array.fromUTF8String(user.id),
|
||||
userName: ''
|
||||
};
|
||||
|
||||
const options = await generateRegistrationOptions(opts);
|
||||
const registrationOptions = await generateRegistrationOptions(opts);
|
||||
|
||||
await this.userService.updateUser({
|
||||
data: {
|
||||
authChallenge: options.challenge
|
||||
authChallenge: registrationOptions.challenge
|
||||
},
|
||||
where: {
|
||||
id: user.id
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
return registrationOptions;
|
||||
}
|
||||
|
||||
public async verifyAttestation(
|
||||
@@ -84,13 +85,14 @@ export class WebAuthService {
|
||||
): Promise<AuthDeviceDto> {
|
||||
const user = this.request.user;
|
||||
const expectedChallenge = user.authChallenge;
|
||||
|
||||
let verification: VerifiedRegistrationResponse;
|
||||
|
||||
try {
|
||||
const opts: VerifyRegistrationResponseOpts = {
|
||||
expectedChallenge,
|
||||
expectedOrigin: this.expectedOrigin,
|
||||
expectedRPID: this.rpID,
|
||||
requireUserVerification: false,
|
||||
response: {
|
||||
clientExtensionResults: credential.clientExtensionResults,
|
||||
id: credential.id,
|
||||
@@ -99,6 +101,7 @@ export class WebAuthService {
|
||||
type: 'public-key'
|
||||
}
|
||||
};
|
||||
|
||||
verification = await verifyRegistrationResponse(opts);
|
||||
} catch (error) {
|
||||
Logger.error(error, 'WebAuthService');
|
||||
@@ -144,7 +147,7 @@ export class WebAuthService {
|
||||
throw new InternalServerErrorException('An unknown error occurred');
|
||||
}
|
||||
|
||||
public async generateAssertionOptions(deviceId: string) {
|
||||
public async generateAuthenticationOptions(deviceId: string) {
|
||||
const device = await this.deviceService.authDevice({ id: deviceId });
|
||||
|
||||
if (!device) {
|
||||
@@ -152,32 +155,27 @@ export class WebAuthService {
|
||||
}
|
||||
|
||||
const opts: GenerateAuthenticationOptionsOpts = {
|
||||
allowCredentials: [
|
||||
{
|
||||
id: isoBase64URL.fromBuffer(device.credentialId),
|
||||
transports: ['internal']
|
||||
}
|
||||
],
|
||||
allowCredentials: [],
|
||||
rpID: this.rpID,
|
||||
timeout: 60000,
|
||||
timeout: ms('60 seconds'),
|
||||
userVerification: 'preferred'
|
||||
};
|
||||
|
||||
const options = await generateAuthenticationOptions(opts);
|
||||
const authenticationOptions = await generateAuthenticationOptions(opts);
|
||||
|
||||
await this.userService.updateUser({
|
||||
data: {
|
||||
authChallenge: options.challenge
|
||||
authChallenge: authenticationOptions.challenge
|
||||
},
|
||||
where: {
|
||||
id: device.userId
|
||||
}
|
||||
});
|
||||
|
||||
return options;
|
||||
return authenticationOptions;
|
||||
}
|
||||
|
||||
public async verifyAssertion(
|
||||
public async verifyAuthentication(
|
||||
deviceId: string,
|
||||
credential: AssertionCredentialJSON
|
||||
) {
|
||||
@@ -190,6 +188,7 @@ export class WebAuthService {
|
||||
const user = await this.userService.user({ id: device.userId });
|
||||
|
||||
let verification: VerifiedAuthenticationResponse;
|
||||
|
||||
try {
|
||||
const opts: VerifyAuthenticationResponseOpts = {
|
||||
credential: {
|
||||
@@ -200,6 +199,7 @@ export class WebAuthService {
|
||||
expectedChallenge: `${user.authChallenge}`,
|
||||
expectedOrigin: this.expectedOrigin,
|
||||
expectedRPID: this.rpID,
|
||||
requireUserVerification: false,
|
||||
response: {
|
||||
clientExtensionResults: credential.clientExtensionResults,
|
||||
id: credential.id,
|
||||
@@ -208,13 +208,14 @@ export class WebAuthService {
|
||||
type: 'public-key'
|
||||
}
|
||||
};
|
||||
|
||||
verification = await verifyAuthenticationResponse(opts);
|
||||
} catch (error) {
|
||||
Logger.error(error, 'WebAuthService');
|
||||
throw new InternalServerErrorException({ error: error.message });
|
||||
}
|
||||
|
||||
const { verified, authenticationInfo } = verification;
|
||||
const { authenticationInfo, verified } = verification;
|
||||
|
||||
if (verified) {
|
||||
device.counter = authenticationInfo.newCounter;
|
||||
|
@@ -2,6 +2,7 @@ import { AccountModule } from '@ghostfolio/api/app/account/account.module';
|
||||
import { OrderModule } from '@ghostfolio/api/app/order/order.module';
|
||||
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
|
||||
import { ApiModule } from '@ghostfolio/api/services/api/api.module';
|
||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module';
|
||||
import { TagModule } from '@ghostfolio/api/services/tag/tag.module';
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
@@ -14,6 +15,7 @@ import { ExportService } from './export.service';
|
||||
imports: [
|
||||
AccountModule,
|
||||
ApiModule,
|
||||
MarketDataModule,
|
||||
OrderModule,
|
||||
TagModule,
|
||||
TransformDataSourceInRequestModule
|
||||
|
@@ -1,16 +1,19 @@
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { environment } from '@ghostfolio/api/environments/environment';
|
||||
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
|
||||
import { TagService } from '@ghostfolio/api/services/tag/tag.service';
|
||||
import { Filter, Export } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Platform } from '@prisma/client';
|
||||
import { Platform, Prisma } from '@prisma/client';
|
||||
import { groupBy, uniqBy } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class ExportService {
|
||||
public constructor(
|
||||
private readonly accountService: AccountService,
|
||||
private readonly marketDataService: MarketDataService,
|
||||
private readonly orderService: OrderService,
|
||||
private readonly tagService: TagService
|
||||
) {}
|
||||
@@ -26,6 +29,9 @@ export class ExportService {
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<Export> {
|
||||
const { ACCOUNT: filtersByAccount } = groupBy(filters, ({ type }) => {
|
||||
return type;
|
||||
});
|
||||
const platformsMap: { [platformId: string]: Platform } = {};
|
||||
|
||||
let { activities } = await this.orderService.getOrders({
|
||||
@@ -44,20 +50,30 @@ export class ExportService {
|
||||
});
|
||||
}
|
||||
|
||||
const where: Prisma.AccountWhereInput = { userId };
|
||||
|
||||
if (filtersByAccount?.length > 0) {
|
||||
where.id = {
|
||||
in: filtersByAccount.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
const accounts = (
|
||||
await this.accountService.accounts({
|
||||
where,
|
||||
include: {
|
||||
balances: true,
|
||||
platform: true
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc'
|
||||
},
|
||||
where: { userId }
|
||||
}
|
||||
})
|
||||
)
|
||||
.filter(({ id }) => {
|
||||
return activities.length > 0
|
||||
return activityIds?.length > 0
|
||||
? activities.some(({ accountId }) => {
|
||||
return accountId === id;
|
||||
})
|
||||
@@ -94,6 +110,36 @@ export class ExportService {
|
||||
}
|
||||
);
|
||||
|
||||
const customAssetProfiles = uniqBy(
|
||||
activities
|
||||
.map(({ SymbolProfile }) => {
|
||||
return SymbolProfile;
|
||||
})
|
||||
.filter(({ userId: assetProfileUserId }) => {
|
||||
return assetProfileUserId === userId;
|
||||
}),
|
||||
({ id }) => {
|
||||
return id;
|
||||
}
|
||||
);
|
||||
|
||||
const marketDataByAssetProfile = Object.fromEntries(
|
||||
await Promise.all(
|
||||
customAssetProfiles.map(async ({ dataSource, id, symbol }) => {
|
||||
const marketData = (
|
||||
await this.marketDataService.marketDataItems({
|
||||
where: { dataSource, symbol }
|
||||
})
|
||||
).map(({ date, marketPrice }) => ({
|
||||
date: date.toISOString(),
|
||||
marketPrice
|
||||
}));
|
||||
|
||||
return [id, marketData] as const;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const tags = (await this.tagService.getTagsForUser(userId))
|
||||
.filter(
|
||||
({ id, isUsed }) =>
|
||||
@@ -114,6 +160,55 @@ export class ExportService {
|
||||
return {
|
||||
meta: { date: new Date().toISOString(), version: environment.version },
|
||||
accounts,
|
||||
assetProfiles: customAssetProfiles.map(
|
||||
({
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
comment,
|
||||
countries,
|
||||
currency,
|
||||
cusip,
|
||||
dataSource,
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings,
|
||||
id,
|
||||
isActive,
|
||||
isin,
|
||||
name,
|
||||
scraperConfiguration,
|
||||
sectors,
|
||||
symbol,
|
||||
symbolMapping,
|
||||
url
|
||||
}) => {
|
||||
return {
|
||||
assetClass,
|
||||
assetSubClass,
|
||||
comment,
|
||||
countries: countries as unknown as Prisma.JsonArray,
|
||||
currency,
|
||||
cusip,
|
||||
dataSource,
|
||||
figi,
|
||||
figiComposite,
|
||||
figiShareClass,
|
||||
holdings: holdings as unknown as Prisma.JsonArray,
|
||||
id,
|
||||
isActive,
|
||||
isin,
|
||||
marketData: marketDataByAssetProfile[id],
|
||||
name,
|
||||
scraperConfiguration:
|
||||
scraperConfiguration as unknown as Prisma.JsonArray,
|
||||
sectors: sectors as unknown as Prisma.JsonArray,
|
||||
symbol,
|
||||
symbolMapping,
|
||||
url
|
||||
};
|
||||
}
|
||||
),
|
||||
platforms: Object.values(platformsMap),
|
||||
tags,
|
||||
activities: activities.map(
|
||||
@@ -141,11 +236,7 @@ export class ExportService {
|
||||
currency: currency ?? SymbolProfile.currency,
|
||||
dataSource: SymbolProfile.dataSource,
|
||||
date: date.toISOString(),
|
||||
symbol:
|
||||
['FEE', 'INTEREST', 'LIABILITY'].includes(type) ||
|
||||
(SymbolProfile.dataSource === 'MANUAL' && type === 'BUY')
|
||||
? SymbolProfile.name
|
||||
: SymbolProfile.symbol,
|
||||
symbol: SymbolProfile.symbol,
|
||||
tags: currentTags.map(({ id: tagId }) => {
|
||||
return tagId;
|
||||
})
|
||||
|
@@ -8,7 +8,8 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/sy
|
||||
import {
|
||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH,
|
||||
GATHER_ASSET_PROFILE_PROCESS_JOB_NAME,
|
||||
GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS
|
||||
GATHER_ASSET_PROFILE_PROCESS_JOB_OPTIONS,
|
||||
ghostfolioPrefix
|
||||
} from '@ghostfolio/common/config';
|
||||
import { getAssetProfileIdentifier } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
Type as ActivityType
|
||||
} from '@prisma/client';
|
||||
import { Big } from 'big.js';
|
||||
import { isUUID } from 'class-validator';
|
||||
import { endOfToday, isAfter } from 'date-fns';
|
||||
import { groupBy, uniqBy } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
@@ -125,19 +127,33 @@ export class OrderService {
|
||||
const assetClass = data.assetClass;
|
||||
const assetSubClass = data.assetSubClass;
|
||||
const dataSource: DataSource = 'MANUAL';
|
||||
const id = uuidv4();
|
||||
const name = data.SymbolProfile.connectOrCreate.create.symbol;
|
||||
|
||||
data.id = id;
|
||||
let name: string;
|
||||
let symbol: string;
|
||||
|
||||
if (
|
||||
data.SymbolProfile.connectOrCreate.create.symbol.startsWith(
|
||||
`${ghostfolioPrefix}_`
|
||||
) ||
|
||||
isUUID(data.SymbolProfile.connectOrCreate.create.symbol)
|
||||
) {
|
||||
// Connect custom asset profile (clone)
|
||||
symbol = data.SymbolProfile.connectOrCreate.create.symbol;
|
||||
} else {
|
||||
// Create custom asset profile
|
||||
name = data.SymbolProfile.connectOrCreate.create.symbol;
|
||||
symbol = uuidv4();
|
||||
}
|
||||
|
||||
data.SymbolProfile.connectOrCreate.create.assetClass = assetClass;
|
||||
data.SymbolProfile.connectOrCreate.create.assetSubClass = assetSubClass;
|
||||
data.SymbolProfile.connectOrCreate.create.dataSource = dataSource;
|
||||
data.SymbolProfile.connectOrCreate.create.name = name;
|
||||
data.SymbolProfile.connectOrCreate.create.symbol = id;
|
||||
data.SymbolProfile.connectOrCreate.create.symbol = symbol;
|
||||
data.SymbolProfile.connectOrCreate.create.userId = userId;
|
||||
data.SymbolProfile.connectOrCreate.where.dataSource_symbol = {
|
||||
dataSource,
|
||||
symbol: id
|
||||
symbol
|
||||
};
|
||||
}
|
||||
|
||||
|
@@ -446,7 +446,8 @@ export abstract class PortfolioCalculator {
|
||||
currentRateErrors.find(({ dataSource, symbol }) => {
|
||||
return dataSource === item.dataSource && symbol === item.symbol;
|
||||
})) &&
|
||||
item.investment.gt(0)
|
||||
item.investment.gt(0) &&
|
||||
item.skipErrors === false
|
||||
) {
|
||||
errors.push({ dataSource: item.dataSource, symbol: item.symbol });
|
||||
}
|
||||
@@ -896,9 +897,14 @@ export abstract class PortfolioCalculator {
|
||||
unitPrice
|
||||
} of this.activities) {
|
||||
let currentTransactionPointItem: TransactionPointSymbol;
|
||||
const oldAccumulatedSymbol = symbols[SymbolProfile.symbol];
|
||||
|
||||
const currency = SymbolProfile.currency;
|
||||
const dataSource = SymbolProfile.dataSource;
|
||||
const factor = getFactor(type);
|
||||
const skipErrors = !!SymbolProfile.userId; // Skip errors for custom asset profiles
|
||||
const symbol = SymbolProfile.symbol;
|
||||
|
||||
const oldAccumulatedSymbol = symbols[symbol];
|
||||
|
||||
if (oldAccumulatedSymbol) {
|
||||
let investment = oldAccumulatedSymbol.investment;
|
||||
@@ -918,32 +924,34 @@ export abstract class PortfolioCalculator {
|
||||
}
|
||||
|
||||
currentTransactionPointItem = {
|
||||
currency,
|
||||
dataSource,
|
||||
investment,
|
||||
skipErrors,
|
||||
symbol,
|
||||
averagePrice: newQuantity.gt(0)
|
||||
? investment.div(newQuantity)
|
||||
: new Big(0),
|
||||
currency: SymbolProfile.currency,
|
||||
dataSource: SymbolProfile.dataSource,
|
||||
dividend: new Big(0),
|
||||
fee: oldAccumulatedSymbol.fee.plus(fee),
|
||||
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
|
||||
quantity: newQuantity,
|
||||
symbol: SymbolProfile.symbol,
|
||||
tags: oldAccumulatedSymbol.tags.concat(tags),
|
||||
transactionCount: oldAccumulatedSymbol.transactionCount + 1
|
||||
};
|
||||
} else {
|
||||
currentTransactionPointItem = {
|
||||
currency,
|
||||
dataSource,
|
||||
fee,
|
||||
skipErrors,
|
||||
symbol,
|
||||
tags,
|
||||
averagePrice: unitPrice,
|
||||
currency: SymbolProfile.currency,
|
||||
dataSource: SymbolProfile.dataSource,
|
||||
dividend: new Big(0),
|
||||
firstBuyDate: date,
|
||||
investment: unitPrice.mul(quantity).mul(factor),
|
||||
quantity: quantity.mul(factor),
|
||||
symbol: SymbolProfile.symbol,
|
||||
transactionCount: 1
|
||||
};
|
||||
}
|
||||
|
@@ -114,14 +114,8 @@ describe('PortfolioCalculator', () => {
|
||||
|
||||
expect(portfolioSnapshot).toMatchObject({
|
||||
currentValueInBaseCurrency: new Big('500000'),
|
||||
// TODO: []
|
||||
errors: [
|
||||
{
|
||||
dataSource: 'MANUAL',
|
||||
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde'
|
||||
}
|
||||
],
|
||||
hasErrors: true, // TODO: false
|
||||
errors: [],
|
||||
hasErrors: false,
|
||||
positions: [
|
||||
{
|
||||
averagePrice: new Big('500000'),
|
||||
@@ -132,31 +126,35 @@ describe('PortfolioCalculator', () => {
|
||||
fee: new Big('0'),
|
||||
feeInBaseCurrency: new Big('0'),
|
||||
firstBuyDate: '2022-01-01',
|
||||
grossPerformance: null,
|
||||
grossPerformancePercentage: null,
|
||||
grossPerformancePercentageWithCurrencyEffect: null,
|
||||
grossPerformanceWithCurrencyEffect: null,
|
||||
investment: new Big('0'), // TODO: new Big('500000')
|
||||
investmentWithCurrencyEffect: new Big('0'), // TODO: new Big('500000')
|
||||
grossPerformance: new Big('0'),
|
||||
grossPerformancePercentage: new Big('0'),
|
||||
grossPerformancePercentageWithCurrencyEffect: new Big('0'),
|
||||
grossPerformanceWithCurrencyEffect: new Big('0'),
|
||||
investment: new Big('500000'),
|
||||
investmentWithCurrencyEffect: new Big('500000'),
|
||||
marketPrice: null,
|
||||
marketPriceInBaseCurrency: 500000,
|
||||
netPerformance: null,
|
||||
netPerformancePercentage: null,
|
||||
netPerformancePercentageWithCurrencyEffectMap: null,
|
||||
netPerformanceWithCurrencyEffectMap: null,
|
||||
netPerformance: new Big('0'),
|
||||
netPerformancePercentage: new Big('0'),
|
||||
netPerformancePercentageWithCurrencyEffectMap: {
|
||||
max: new Big('0')
|
||||
},
|
||||
netPerformanceWithCurrencyEffectMap: {
|
||||
max: new Big('0')
|
||||
},
|
||||
quantity: new Big('1'),
|
||||
symbol: 'dac95060-d4f2-4653-a253-2c45e6fb5cde',
|
||||
tags: [],
|
||||
timeWeightedInvestment: new Big('0'),
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big('0'),
|
||||
timeWeightedInvestment: new Big('500000'),
|
||||
timeWeightedInvestmentWithCurrencyEffect: new Big('500000'),
|
||||
transactionCount: 1,
|
||||
valueInBaseCurrency: new Big('500000')
|
||||
}
|
||||
],
|
||||
totalFeesWithCurrencyEffect: new Big('0'),
|
||||
totalInterestWithCurrencyEffect: new Big('0'),
|
||||
totalInvestment: new Big('0'), // TODO: new Big('500000')
|
||||
totalInvestmentWithCurrencyEffect: new Big('0'), // TODO: new Big('500000')
|
||||
totalInvestment: new Big('500000'),
|
||||
totalInvestmentWithCurrencyEffect: new Big('500000'),
|
||||
totalLiabilitiesWithCurrencyEffect: new Big('0')
|
||||
});
|
||||
|
||||
@@ -166,7 +164,7 @@ describe('PortfolioCalculator', () => {
|
||||
netPerformanceInPercentage: 0,
|
||||
netPerformanceInPercentageWithCurrencyEffect: 0,
|
||||
netPerformanceWithCurrencyEffect: 0,
|
||||
totalInvestmentValueWithCurrencyEffect: 0 // TODO: 500000
|
||||
totalInvestmentValueWithCurrencyEffect: 500000
|
||||
})
|
||||
);
|
||||
});
|
||||
|
@@ -231,7 +231,20 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
|
||||
const startDateString = format(start, DATE_FORMAT);
|
||||
|
||||
const unitPriceAtStartDate = marketSymbolMap[startDateString]?.[symbol];
|
||||
const unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol];
|
||||
let unitPriceAtEndDate = marketSymbolMap[endDateString]?.[symbol];
|
||||
|
||||
let latestActivity = orders.at(-1);
|
||||
|
||||
if (
|
||||
dataSource === 'MANUAL' &&
|
||||
['BUY', 'SELL'].includes(latestActivity?.type) &&
|
||||
latestActivity?.unitPrice &&
|
||||
!unitPriceAtEndDate
|
||||
) {
|
||||
// For BUY / SELL activities with a MANUAL data source where no historical market price is available,
|
||||
// the calculation should fall back to using the activity’s unit price.
|
||||
unitPriceAtEndDate = latestActivity.unitPrice;
|
||||
}
|
||||
|
||||
if (
|
||||
!unitPriceAtEndDate ||
|
||||
@@ -344,9 +357,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator {
|
||||
});
|
||||
}
|
||||
|
||||
const lastOrder = orders.at(-1);
|
||||
latestActivity = orders.at(-1);
|
||||
|
||||
lastUnitPrice = lastOrder.unitPriceFromMarketData ?? lastOrder.unitPrice;
|
||||
lastUnitPrice =
|
||||
latestActivity.unitPriceFromMarketData ?? latestActivity.unitPrice;
|
||||
}
|
||||
|
||||
// Sort orders so that the start and end placeholder order are at the correct
|
||||
|
@@ -6,7 +6,7 @@ export interface PortfolioOrder extends Pick<Activity, 'tags' | 'type'> {
|
||||
quantity: Big;
|
||||
SymbolProfile: Pick<
|
||||
Activity['SymbolProfile'],
|
||||
'currency' | 'dataSource' | 'name' | 'symbol'
|
||||
'currency' | 'dataSource' | 'name' | 'symbol' | 'userId'
|
||||
>;
|
||||
unitPrice: Big;
|
||||
}
|
||||
|
@@ -10,6 +10,7 @@ export interface TransactionPointSymbol {
|
||||
firstBuyDate: string;
|
||||
investment: Big;
|
||||
quantity: Big;
|
||||
skipErrors: boolean;
|
||||
symbol: string;
|
||||
tags?: Tag[];
|
||||
transactionCount: number;
|
||||
|
@@ -1223,13 +1223,17 @@ export class PortfolioService {
|
||||
[
|
||||
new EconomicMarketClusterRiskDevelopedMarkets(
|
||||
this.exchangeRateDataService,
|
||||
this.i18nService,
|
||||
marketsTotalInBaseCurrency,
|
||||
markets.developedMarkets.valueInBaseCurrency
|
||||
markets.developedMarkets.valueInBaseCurrency,
|
||||
userSettings.language
|
||||
),
|
||||
new EconomicMarketClusterRiskEmergingMarkets(
|
||||
this.exchangeRateDataService,
|
||||
this.i18nService,
|
||||
marketsTotalInBaseCurrency,
|
||||
markets.emergingMarkets.valueInBaseCurrency
|
||||
markets.emergingMarkets.valueInBaseCurrency,
|
||||
userSettings.language
|
||||
)
|
||||
],
|
||||
userSettings
|
||||
@@ -1268,26 +1272,36 @@ export class PortfolioService {
|
||||
[
|
||||
new RegionalMarketClusterRiskAsiaPacific(
|
||||
this.exchangeRateDataService,
|
||||
this.i18nService,
|
||||
userSettings.language,
|
||||
marketsAdvancedTotalInBaseCurrency,
|
||||
marketsAdvanced.asiaPacific.valueInBaseCurrency
|
||||
),
|
||||
new RegionalMarketClusterRiskEmergingMarkets(
|
||||
this.exchangeRateDataService,
|
||||
this.i18nService,
|
||||
userSettings.language,
|
||||
marketsAdvancedTotalInBaseCurrency,
|
||||
marketsAdvanced.emergingMarkets.valueInBaseCurrency
|
||||
),
|
||||
new RegionalMarketClusterRiskEurope(
|
||||
this.exchangeRateDataService,
|
||||
this.i18nService,
|
||||
userSettings.language,
|
||||
marketsAdvancedTotalInBaseCurrency,
|
||||
marketsAdvanced.europe.valueInBaseCurrency
|
||||
),
|
||||
new RegionalMarketClusterRiskJapan(
|
||||
this.exchangeRateDataService,
|
||||
this.i18nService,
|
||||
userSettings.language,
|
||||
marketsAdvancedTotalInBaseCurrency,
|
||||
marketsAdvanced.japan.valueInBaseCurrency
|
||||
),
|
||||
new RegionalMarketClusterRiskNorthAmerica(
|
||||
this.exchangeRateDataService,
|
||||
this.i18nService,
|
||||
userSettings.language,
|
||||
marketsAdvancedTotalInBaseCurrency,
|
||||
marketsAdvanced.northAmerica.valueInBaseCurrency
|
||||
)
|
||||
|
@@ -22,6 +22,7 @@ export class RulesService {
|
||||
return {
|
||||
evaluation,
|
||||
value,
|
||||
categoryName: rule.getCategoryName(),
|
||||
configuration: rule.getConfiguration(),
|
||||
isActive: true,
|
||||
key: rule.getKey(),
|
||||
@@ -29,6 +30,7 @@ export class RulesService {
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
categoryName: rule.getCategoryName(),
|
||||
isActive: false,
|
||||
key: rule.getKey(),
|
||||
name: rule.getName()
|
||||
|
@@ -300,12 +300,16 @@ export class UserService {
|
||||
).getSettings(user.settings.settings),
|
||||
EconomicMarketClusterRiskDevelopedMarkets:
|
||||
new EconomicMarketClusterRiskDevelopedMarkets(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
).getSettings(user.settings.settings),
|
||||
EconomicMarketClusterRiskEmergingMarkets:
|
||||
new EconomicMarketClusterRiskEmergingMarkets(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
@@ -325,28 +329,38 @@ export class UserService {
|
||||
).getSettings(user.settings.settings),
|
||||
RegionalMarketClusterRiskAsiaPacific:
|
||||
new RegionalMarketClusterRiskAsiaPacific(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
).getSettings(user.settings.settings),
|
||||
RegionalMarketClusterRiskEmergingMarkets:
|
||||
new RegionalMarketClusterRiskEmergingMarkets(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
).getSettings(user.settings.settings),
|
||||
RegionalMarketClusterRiskEurope: new RegionalMarketClusterRiskEurope(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
).getSettings(user.settings.settings),
|
||||
RegionalMarketClusterRiskJapan: new RegionalMarketClusterRiskJapan(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
).getSettings(user.settings.settings),
|
||||
RegionalMarketClusterRiskNorthAmerica:
|
||||
new RegionalMarketClusterRiskNorthAmerica(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined
|
||||
|
@@ -70,6 +70,8 @@ export abstract class Rule<T extends RuleSettings> implements RuleInterface<T> {
|
||||
|
||||
public abstract evaluate(aRuleSettings: T): EvaluationResult;
|
||||
|
||||
public abstract getCategoryName(): string;
|
||||
|
||||
public abstract getConfiguration(): Partial<
|
||||
PortfolioReportRule['configuration']
|
||||
>;
|
||||
|
@@ -37,6 +37,16 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
};
|
||||
}
|
||||
|
||||
if (Object.keys(accounts).length === 0) {
|
||||
return {
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.accountClusterRiskCurrentInvestment.false.invalid',
|
||||
languageCode: this.getLanguageCode()
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
let maxAccount: (typeof accounts)[0];
|
||||
let totalInvestment = 0;
|
||||
|
||||
@@ -85,6 +95,13 @@ export class AccountClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.accountClusterRisk.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
|
@@ -22,9 +22,17 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
||||
}
|
||||
|
||||
public evaluate() {
|
||||
const accounts: string[] = Object.keys(this.accounts);
|
||||
const accountIds: string[] = Object.keys(this.accounts);
|
||||
|
||||
if (accounts.length === 1) {
|
||||
if (accountIds.length === 0) {
|
||||
return {
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.accountClusterRiskSingleAccount.false.invalid',
|
||||
languageCode: this.getLanguageCode()
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
} else if (accountIds.length === 1) {
|
||||
return {
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.accountClusterRiskSingleAccount.false',
|
||||
@@ -39,13 +47,20 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
||||
id: 'rule.accountClusterRiskSingleAccount.true',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
accountsLength: accounts.length
|
||||
accountsLength: accountIds.length
|
||||
}
|
||||
}),
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.accountClusterRisk.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return undefined;
|
||||
}
|
||||
@@ -55,7 +70,6 @@ export class AccountClusterRiskSingleAccount extends Rule<RuleSettings> {
|
||||
id: 'rule.accountClusterRiskSingleAccount',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
return 'Single Account';
|
||||
}
|
||||
|
||||
public getSettings({ xRayRules }: UserSettings): RuleSettings {
|
||||
|
@@ -81,6 +81,13 @@ export class AssetClassClusterRiskEquity extends Rule<Settings> {
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.assetClassClusterRisk.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
|
@@ -81,6 +81,13 @@ export class AssetClassClusterRiskFixedIncome extends Rule<Settings> {
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.assetClassClusterRisk.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
|
@@ -79,6 +79,13 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule<Setti
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.currencyClusterRisk.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -72,6 +72,13 @@ export class CurrencyClusterRiskCurrentInvestment extends Rule<Settings> {
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.currencyClusterRisk.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
|
||||
@@ -9,10 +10,13 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
private i18nService: I18nService,
|
||||
currentValueInBaseCurrency: number,
|
||||
developedMarketsValueInBaseCurrency: number
|
||||
developedMarketsValueInBaseCurrency: number,
|
||||
languageCode: string
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
languageCode,
|
||||
key: EconomicMarketClusterRiskDevelopedMarkets.name
|
||||
});
|
||||
|
||||
@@ -29,30 +33,57 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
|
||||
|
||||
if (developedMarketsValueRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `The developed markets contribution of your current investment (${(developedMarketsValueRatio * 100).toPrecision(3)}%) exceeds ${(
|
||||
ruleSettings.thresholdMax * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRiskDevelopedMarkets.false.max',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
developedMarketsValueRatio: (
|
||||
developedMarketsValueRatio * 100
|
||||
).toPrecision(3),
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
} else if (developedMarketsValueRatio < ruleSettings.thresholdMin) {
|
||||
return {
|
||||
evaluation: `The developed markets contribution of your current investment (${(developedMarketsValueRatio * 100).toPrecision(3)}%) is below ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRiskDevelopedMarkets.false.min',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
developedMarketsValueRatio: (
|
||||
developedMarketsValueRatio * 100
|
||||
).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: `The developed markets contribution of your current investment (${(developedMarketsValueRatio * 100).toPrecision(3)}%) is within the range of ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(
|
||||
3
|
||||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRiskDevelopedMarkets.true',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
developedMarketsValueRatio: (
|
||||
developedMarketsValueRatio * 100
|
||||
).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRisk.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
@@ -67,7 +98,10 @@ export class EconomicMarketClusterRiskDevelopedMarkets extends Rule<Settings> {
|
||||
}
|
||||
|
||||
public getName() {
|
||||
return 'Developed Markets';
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRiskDevelopedMarkets',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface';
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
|
||||
@@ -9,10 +10,13 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
private i18nService: I18nService,
|
||||
currentValueInBaseCurrency: number,
|
||||
emergingMarketsValueInBaseCurrency: number
|
||||
emergingMarketsValueInBaseCurrency: number,
|
||||
languageCode: string
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
languageCode,
|
||||
key: EconomicMarketClusterRiskEmergingMarkets.name
|
||||
});
|
||||
|
||||
@@ -29,30 +33,57 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
|
||||
|
||||
if (emergingMarketsValueRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `The emerging markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) exceeds ${(
|
||||
ruleSettings.thresholdMax * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRiskEmergingMarkets.false.max',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
emergingMarketsValueRatio: (
|
||||
emergingMarketsValueRatio * 100
|
||||
).toPrecision(3),
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
} else if (emergingMarketsValueRatio < ruleSettings.thresholdMin) {
|
||||
return {
|
||||
evaluation: `The emerging markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) is below ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRiskEmergingMarkets.false.min',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
emergingMarketsValueRatio: (
|
||||
emergingMarketsValueRatio * 100
|
||||
).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: `The emerging markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) is within the range of ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(
|
||||
3
|
||||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRiskEmergingMarkets.true',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
emergingMarketsValueRatio: (
|
||||
emergingMarketsValueRatio * 100
|
||||
).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRisk.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
@@ -67,7 +98,10 @@ export class EconomicMarketClusterRiskEmergingMarkets extends Rule<Settings> {
|
||||
}
|
||||
|
||||
public getName() {
|
||||
return 'Emerging Markets';
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.economicMarketClusterRiskEmergingMarkets',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
|
||||
|
@@ -41,6 +41,13 @@ export class EmergencyFundSetup extends Rule<Settings> {
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.emergencyFund.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -56,6 +56,13 @@ export class FeeRatioInitialInvestment extends Rule<Settings> {
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.fees.category',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Settings } from './interfaces/rule-settings.interface';
|
||||
@@ -10,10 +11,13 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
private i18nService: I18nService,
|
||||
languageCode: string,
|
||||
currentValueInBaseCurrency: number,
|
||||
asiaPacificValueInBaseCurrency: number
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
languageCode,
|
||||
key: RegionalMarketClusterRiskAsiaPacific.name
|
||||
});
|
||||
|
||||
@@ -28,30 +32,48 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
|
||||
|
||||
if (asiaPacificMarketValueRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) exceeds ${(
|
||||
ruleSettings.thresholdMax * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskAsiaPacific.false.max',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
valueRatio: (asiaPacificMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
} else if (asiaPacificMarketValueRatio < ruleSettings.thresholdMin) {
|
||||
return {
|
||||
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) is below ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskAsiaPacific.false.min',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (asiaPacificMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: `The Asia-Pacific market contribution of your current investment (${(asiaPacificMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(
|
||||
3
|
||||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskAsiaPacific.true',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (asiaPacificMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return 'Regional Market Cluster Risk'; // TODO: Replace hardcoded text with i18n translation
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
@@ -66,7 +88,10 @@ export class RegionalMarketClusterRiskAsiaPacific extends Rule<Settings> {
|
||||
}
|
||||
|
||||
public getName() {
|
||||
return 'Asia-Pacific';
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskAsiaPacific',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Settings } from './interfaces/rule-settings.interface';
|
||||
@@ -10,10 +11,13 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
private i18nService: I18nService,
|
||||
languageCode: string,
|
||||
currentValueInBaseCurrency: number,
|
||||
emergingMarketsValueInBaseCurrency: number
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
languageCode,
|
||||
key: RegionalMarketClusterRiskEmergingMarkets.name
|
||||
});
|
||||
|
||||
@@ -30,30 +34,48 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
|
||||
|
||||
if (emergingMarketsValueRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `The Emerging Markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) exceeds ${(
|
||||
ruleSettings.thresholdMax * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskEmergingMarkets.false.max',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
valueRatio: (emergingMarketsValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
} else if (emergingMarketsValueRatio < ruleSettings.thresholdMin) {
|
||||
return {
|
||||
evaluation: `The Emerging Markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) is below ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskEmergingMarkets.false.min',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (emergingMarketsValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: `The Emerging Markets contribution of your current investment (${(emergingMarketsValueRatio * 100).toPrecision(3)}%) is within the range of ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(
|
||||
3
|
||||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskEmergingMarkets.true',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (emergingMarketsValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return 'Regional Market Cluster Risk'; // TODO: Replace hardcoded text with i18n translation
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
@@ -68,7 +90,10 @@ export class RegionalMarketClusterRiskEmergingMarkets extends Rule<Settings> {
|
||||
}
|
||||
|
||||
public getName() {
|
||||
return 'Emerging Markets';
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskEmergingMarkets',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Settings } from './interfaces/rule-settings.interface';
|
||||
@@ -10,10 +11,13 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
private i18nService: I18nService,
|
||||
languageCode: string,
|
||||
currentValueInBaseCurrency: number,
|
||||
europeValueInBaseCurrency: number
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
languageCode,
|
||||
key: RegionalMarketClusterRiskEurope.name
|
||||
});
|
||||
|
||||
@@ -28,30 +32,48 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
|
||||
|
||||
if (europeMarketValueRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `The Europe market contribution of your current investment (${(europeMarketValueRatio * 100).toPrecision(3)}%) exceeds ${(
|
||||
ruleSettings.thresholdMax * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskEurope.false.max',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
valueRatio: (europeMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
} else if (europeMarketValueRatio < ruleSettings.thresholdMin) {
|
||||
return {
|
||||
evaluation: `The Europe market contribution of your current investment (${(europeMarketValueRatio * 100).toPrecision(3)}%) is below ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskEurope.false.min',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (europeMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: `The Europe market contribution of your current investment (${(europeMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(
|
||||
3
|
||||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskEurope.true',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (europeMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return 'Regional Market Cluster Risk'; // TODO: Replace hardcoded text with i18n translation
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
@@ -66,7 +88,10 @@ export class RegionalMarketClusterRiskEurope extends Rule<Settings> {
|
||||
}
|
||||
|
||||
public getName() {
|
||||
return 'Europe';
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskEurope',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Settings } from './interfaces/rule-settings.interface';
|
||||
@@ -10,10 +11,13 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
private i18nService: I18nService,
|
||||
languageCode: string,
|
||||
currentValueInBaseCurrency: number,
|
||||
japanValueInBaseCurrency: number
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
languageCode,
|
||||
key: RegionalMarketClusterRiskJapan.name
|
||||
});
|
||||
|
||||
@@ -28,30 +32,48 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
|
||||
|
||||
if (japanMarketValueRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) exceeds ${(
|
||||
ruleSettings.thresholdMax * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskJapan.false.max',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
valueRatio: (japanMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
} else if (japanMarketValueRatio < ruleSettings.thresholdMin) {
|
||||
return {
|
||||
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) is below ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskJapan.false.min',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (japanMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: `The Japan market contribution of your current investment (${(japanMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(
|
||||
3
|
||||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskJapan.true',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (japanMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return 'Regional Market Cluster Risk'; // TODO: Replace hardcoded text with i18n translation
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
@@ -66,7 +88,10 @@ export class RegionalMarketClusterRiskJapan extends Rule<Settings> {
|
||||
}
|
||||
|
||||
public getName() {
|
||||
return 'Japan';
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskJapan',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { Rule } from '@ghostfolio/api/models/rule';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
|
||||
import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service';
|
||||
import { UserSettings } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Settings } from './interfaces/rule-settings.interface';
|
||||
@@ -10,10 +11,13 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
|
||||
|
||||
public constructor(
|
||||
protected exchangeRateDataService: ExchangeRateDataService,
|
||||
private i18nService: I18nService,
|
||||
languageCode: string,
|
||||
currentValueInBaseCurrency: number,
|
||||
northAmericaValueInBaseCurrency: number
|
||||
) {
|
||||
super(exchangeRateDataService, {
|
||||
languageCode,
|
||||
key: RegionalMarketClusterRiskNorthAmerica.name
|
||||
});
|
||||
|
||||
@@ -28,30 +32,48 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
|
||||
|
||||
if (northAmericaMarketValueRatio > ruleSettings.thresholdMax) {
|
||||
return {
|
||||
evaluation: `The North America market contribution of your current investment (${(northAmericaMarketValueRatio * 100).toPrecision(3)}%) exceeds ${(
|
||||
ruleSettings.thresholdMax * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskNorthAmerica.false.max',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
valueRatio: (northAmericaMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
} else if (northAmericaMarketValueRatio < ruleSettings.thresholdMin) {
|
||||
return {
|
||||
evaluation: `The North America market contribution of your current investment (${(northAmericaMarketValueRatio * 100).toPrecision(3)}%) is below ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskNorthAmerica.false.min',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (northAmericaMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: false
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
evaluation: `The North America market contribution of your current investment (${(northAmericaMarketValueRatio * 100).toPrecision(3)}%) is within the range of ${(
|
||||
ruleSettings.thresholdMin * 100
|
||||
).toPrecision(
|
||||
3
|
||||
)}% and ${(ruleSettings.thresholdMax * 100).toPrecision(3)}%`,
|
||||
evaluation: this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskNorthAmerica.true',
|
||||
languageCode: this.getLanguageCode(),
|
||||
placeholders: {
|
||||
thresholdMax: (ruleSettings.thresholdMax * 100).toPrecision(3),
|
||||
thresholdMin: (ruleSettings.thresholdMin * 100).toPrecision(3),
|
||||
valueRatio: (northAmericaMarketValueRatio * 100).toPrecision(3)
|
||||
}
|
||||
}),
|
||||
value: true
|
||||
};
|
||||
}
|
||||
|
||||
public getCategoryName() {
|
||||
return 'Regional Market Cluster Risk'; // TODO: Replace hardcoded text with i18n translation
|
||||
}
|
||||
|
||||
public getConfiguration() {
|
||||
return {
|
||||
threshold: {
|
||||
@@ -66,7 +88,10 @@ export class RegionalMarketClusterRiskNorthAmerica extends Rule<Settings> {
|
||||
}
|
||||
|
||||
public getName() {
|
||||
return 'North America';
|
||||
return this.i18nService.getTranslation({
|
||||
id: 'rule.regionalMarketClusterRiskNorthAmerica',
|
||||
languageCode: this.getLanguageCode()
|
||||
});
|
||||
}
|
||||
|
||||
public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings {
|
||||
|
@@ -16,21 +16,19 @@ const routes: Routes = [
|
||||
{
|
||||
path: internalRoutes.account.path,
|
||||
loadChildren: () =>
|
||||
import('./pages/user-account/user-account-page.module').then(
|
||||
(m) => m.UserAccountPageModule
|
||||
import('./pages/user-account/user-account-page.routes').then(
|
||||
(m) => m.routes
|
||||
)
|
||||
},
|
||||
{
|
||||
path: internalRoutes.accounts.path,
|
||||
loadChildren: () =>
|
||||
import('./pages/accounts/accounts-page.module').then(
|
||||
(m) => m.AccountsPageModule
|
||||
)
|
||||
import('./pages/accounts/accounts-page.routes').then((m) => m.routes)
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.path,
|
||||
loadChildren: () =>
|
||||
import('./pages/admin/admin-page.module').then((m) => m.AdminPageModule)
|
||||
import('./pages/admin/admin-page.routes').then((m) => m.routes)
|
||||
},
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
@@ -63,7 +61,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: publicRoutes.faq.path,
|
||||
loadChildren: () =>
|
||||
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
|
||||
import('./pages/faq/faq-page.routes').then((m) => m.routes)
|
||||
},
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
@@ -77,7 +75,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: internalRoutes.home.path,
|
||||
loadChildren: () =>
|
||||
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
|
||||
import('./pages/home/home-page.routes').then((m) => m.routes)
|
||||
},
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
@@ -129,9 +127,7 @@ const routes: Routes = [
|
||||
{
|
||||
path: publicRoutes.resources.path,
|
||||
loadChildren: () =>
|
||||
import('./pages/resources/resources-page.module').then(
|
||||
(m) => m.ResourcesPageModule
|
||||
)
|
||||
import('./pages/resources/resources-page.routes').then((m) => m.routes)
|
||||
},
|
||||
{
|
||||
path: publicRoutes.start.path,
|
||||
|
@@ -320,6 +320,10 @@ export class AppComponent implements OnDestroy, OnInit {
|
||||
colorScheme: this.user?.settings?.colorScheme,
|
||||
deviceType: this.deviceType,
|
||||
hasImpersonationId: this.hasImpersonationId,
|
||||
hasPermissionToAccessAdminControl: hasPermission(
|
||||
this.user?.permissions,
|
||||
permissions.accessAdminControl
|
||||
),
|
||||
hasPermissionToCreateOrder:
|
||||
!this.hasImpersonationId &&
|
||||
hasPermission(this.user?.permissions, permissions.createOrder) &&
|
||||
|
@@ -3,17 +3,23 @@ import { NotificationService } from '@ghostfolio/client/core/notification/notifi
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
import { publicRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { Clipboard } from '@angular/cdk/clipboard';
|
||||
import { Clipboard, ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { addIcons } from 'ionicons';
|
||||
import {
|
||||
ellipsisHorizontal,
|
||||
@@ -24,13 +30,22 @@ import {
|
||||
import ms from 'ms';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-access-table',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
ClipboardModule,
|
||||
CommonModule,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-access-table',
|
||||
templateUrl: './access-table.component.html',
|
||||
styleUrls: ['./access-table.component.scss'],
|
||||
standalone: false
|
||||
styleUrls: ['./access-table.component.scss']
|
||||
})
|
||||
export class AccessTableComponent implements OnChanges {
|
||||
export class GfAccessTableComponent implements OnChanges {
|
||||
@Input() accesses: Access[];
|
||||
@Input() showActions: boolean;
|
||||
@Input() user: User;
|
||||
|
@@ -1,26 +0,0 @@
|
||||
import { ClipboardModule } from '@angular/cdk/clipboard';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { AccessTableComponent } from './access-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccessTableComponent],
|
||||
exports: [AccessTableComponent],
|
||||
imports: [
|
||||
ClipboardModule,
|
||||
CommonModule,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPortfolioAccessTableModule {}
|
@@ -1,7 +1,10 @@
|
||||
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
|
||||
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
|
||||
import { getLocale } from '@ghostfolio/common/helper';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -12,30 +15,45 @@ import {
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { addIcons } from 'ionicons';
|
||||
import {
|
||||
arrowRedoOutline,
|
||||
createOutline,
|
||||
documentTextOutline,
|
||||
ellipsisHorizontalOutline,
|
||||
ellipsisHorizontal,
|
||||
eyeOffOutline,
|
||||
trashOutline
|
||||
} from 'ionicons/icons';
|
||||
import { get } from 'lodash';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-accounts-table',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './accounts-table.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfEntityLogoComponent,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
selector: 'gf-accounts-table',
|
||||
styleUrls: ['./accounts-table.component.scss'],
|
||||
standalone: false
|
||||
templateUrl: './accounts-table.component.html'
|
||||
})
|
||||
export class AccountsTableComponent implements OnChanges, OnDestroy {
|
||||
export class GfAccountsTableComponent implements OnChanges, OnDestroy {
|
||||
@Input() accounts: AccountModel[];
|
||||
@Input() baseCurrency: string;
|
||||
@Input() deviceType: string;
|
||||
@@ -72,7 +90,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy {
|
||||
arrowRedoOutline,
|
||||
createOutline,
|
||||
documentTextOutline,
|
||||
ellipsisHorizontalOutline,
|
||||
ellipsisHorizontal,
|
||||
eyeOffOutline,
|
||||
trashOutline
|
||||
});
|
||||
|
@@ -1,33 +0,0 @@
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { AccountsTableComponent } from './accounts-table.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountsTableComponent],
|
||||
exports: [AccountsTableComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfEntityLogoComponent,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAccountsTableModule {}
|
@@ -10,6 +10,7 @@ import {
|
||||
import { getDateWithTimeFormatString } from '@ghostfolio/common/helper';
|
||||
import { AdminJobs, User } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
@@ -17,8 +18,17 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { JobStatus } from 'bull';
|
||||
import { addIcons } from 'ionicons';
|
||||
import {
|
||||
@@ -34,17 +44,28 @@ import {
|
||||
removeCircleOutline,
|
||||
timeOutline
|
||||
} from 'ionicons/icons';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
selector: 'gf-admin-jobs',
|
||||
styleUrls: ['./admin-jobs.scss'],
|
||||
templateUrl: './admin-jobs.html',
|
||||
standalone: false
|
||||
templateUrl: './admin-jobs.html'
|
||||
})
|
||||
export class AdminJobsComponent implements OnDestroy, OnInit {
|
||||
export class GfAdminJobsComponent implements OnDestroy, OnInit {
|
||||
public DATA_GATHERING_QUEUE_PRIORITY_LOW = DATA_GATHERING_QUEUE_PRIORITY_LOW;
|
||||
public DATA_GATHERING_QUEUE_PRIORITY_HIGH =
|
||||
DATA_GATHERING_QUEUE_PRIORITY_HIGH;
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { AdminJobsComponent } from './admin-jobs.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminJobsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminJobsModule {}
|
@@ -1,3 +1,4 @@
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
@@ -14,9 +15,13 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
AfterViewInit,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -26,11 +31,24 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort, Sort, SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import {
|
||||
MatPaginator,
|
||||
MatPaginatorModule,
|
||||
PageEvent
|
||||
} from '@angular/material/paginator';
|
||||
import {
|
||||
MatSort,
|
||||
MatSortModule,
|
||||
Sort,
|
||||
SortDirection
|
||||
} from '@angular/material/sort';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { AssetSubClass, DataSource, SymbolProfile } from '@prisma/client';
|
||||
import { isUUID } from 'class-validator';
|
||||
import { addIcons } from 'ionicons';
|
||||
@@ -44,40 +62,51 @@ import {
|
||||
trashOutline
|
||||
} from 'ionicons/icons';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { Subject } from 'rxjs';
|
||||
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { AdminMarketDataService } from './admin-market-data.service';
|
||||
import { AssetProfileDialog } from './asset-profile-dialog/asset-profile-dialog.component';
|
||||
import { GfAssetProfileDialogComponent } from './asset-profile-dialog/asset-profile-dialog.component';
|
||||
import { AssetProfileDialogParams } from './asset-profile-dialog/interfaces/interfaces';
|
||||
import { CreateAssetProfileDialog } from './create-asset-profile-dialog/create-asset-profile-dialog.component';
|
||||
import { GfCreateAssetProfileDialogComponent } from './create-asset-profile-dialog/create-asset-profile-dialog.component';
|
||||
import { CreateAssetProfileDialogParams } from './create-asset-profile-dialog/interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'has-fab' },
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesFilterComponent,
|
||||
GfPremiumIndicatorComponent,
|
||||
GfSymbolModule,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [AdminMarketDataService],
|
||||
selector: 'gf-admin-market-data',
|
||||
styleUrls: ['./admin-market-data.scss'],
|
||||
templateUrl: './admin-market-data.html',
|
||||
standalone: false
|
||||
templateUrl: './admin-market-data.html'
|
||||
})
|
||||
export class AdminMarketDataComponent
|
||||
export class GfAdminMarketDataComponent
|
||||
implements AfterViewInit, OnDestroy, OnInit
|
||||
{
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public activeFilters: Filter[] = [];
|
||||
public allFilters: Filter[] = [
|
||||
AssetSubClass.BOND,
|
||||
AssetSubClass.COMMODITY,
|
||||
AssetSubClass.CRYPTOCURRENCY,
|
||||
AssetSubClass.ETF,
|
||||
AssetSubClass.MUTUALFUND,
|
||||
AssetSubClass.PRECIOUS_METAL,
|
||||
AssetSubClass.PRIVATE_EQUITY,
|
||||
AssetSubClass.STOCK
|
||||
]
|
||||
public allFilters: Filter[] = Object.keys(AssetSubClass)
|
||||
.filter((assetSubClass) => {
|
||||
return assetSubClass !== 'CASH';
|
||||
})
|
||||
.map((assetSubClass) => {
|
||||
return {
|
||||
id: assetSubClass.toString(),
|
||||
@@ -394,7 +423,7 @@ export class AdminMarketDataComponent
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
const dialogRef = this.dialog.open(AssetProfileDialog, {
|
||||
const dialogRef = this.dialog.open(GfAssetProfileDialogComponent, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
dataSource,
|
||||
@@ -429,14 +458,17 @@ export class AdminMarketDataComponent
|
||||
.subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
const dialogRef = this.dialog.open(CreateAssetProfileDialog, {
|
||||
autoFocus: false,
|
||||
data: {
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale
|
||||
} as CreateAssetProfileDialogParams,
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
const dialogRef = this.dialog.open(
|
||||
GfCreateAssetProfileDialogComponent,
|
||||
{
|
||||
autoFocus: false,
|
||||
data: {
|
||||
deviceType: this.deviceType,
|
||||
locale: this.user?.settings?.locale
|
||||
} as CreateAssetProfileDialogParams,
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
}
|
||||
);
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
|
@@ -1,46 +0,0 @@
|
||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||
import { GfActivitiesFilterComponent } from '@ghostfolio/ui/activities-filter';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { AdminMarketDataComponent } from './admin-market-data.component';
|
||||
import { AdminMarketDataService } from './admin-market-data.service';
|
||||
import { GfAssetProfileDialogModule } from './asset-profile-dialog/asset-profile-dialog.module';
|
||||
import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/create-asset-profile-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminMarketDataComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfActivitiesFilterComponent,
|
||||
GfAssetProfileDialogModule,
|
||||
GfCreateAssetProfileDialogModule,
|
||||
GfPremiumIndicatorComponent,
|
||||
GfSymbolModule,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [AdminMarketDataService],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminMarketDataModule {}
|
@@ -5,7 +5,11 @@ import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
|
||||
import {
|
||||
ASSET_CLASS_MAPPING,
|
||||
ghostfolioScraperApiSymbolPrefix,
|
||||
PROPERTY_IS_DATA_GATHERING_ENABLED
|
||||
} from '@ghostfolio/common/config';
|
||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
AdminMarketDataDetails,
|
||||
@@ -14,8 +18,17 @@ import {
|
||||
ScraperConfiguration,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor';
|
||||
import { translate } from '@ghostfolio/ui/i18n';
|
||||
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
||||
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -32,12 +45,27 @@ import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import {
|
||||
MatCheckboxChange,
|
||||
MatCheckboxModule
|
||||
} from '@angular/material/checkbox';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogModule,
|
||||
MatDialogRef
|
||||
} from '@angular/material/dialog';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import {
|
||||
AssetClass,
|
||||
AssetSubClass,
|
||||
@@ -45,6 +73,7 @@ import {
|
||||
Prisma,
|
||||
SymbolProfile
|
||||
} from '@prisma/client';
|
||||
import { isUUID } from 'class-validator';
|
||||
import { format } from 'date-fns';
|
||||
import { StatusCodes } from 'http-status-codes';
|
||||
import { addIcons } from 'ionicons';
|
||||
@@ -53,17 +82,42 @@ import ms from 'ms';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { AssetProfileDialogParams } from './interfaces/interfaces';
|
||||
import {
|
||||
AssetClassSelectorOption,
|
||||
AssetProfileDialogParams
|
||||
} from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'd-flex flex-column h-100' },
|
||||
selector: 'gf-asset-profile-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: 'asset-profile-dialog.html',
|
||||
host: { class: 'd-flex flex-column h-100' },
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfCurrencySelectorComponent,
|
||||
GfEntityLogoComponent,
|
||||
GfHistoricalMarketDataEditorComponent,
|
||||
GfLineChartComponent,
|
||||
GfPortfolioProportionChartComponent,
|
||||
GfSymbolAutocompleteComponent,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDialogModule,
|
||||
MatExpansionModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
MatSnackBarModule,
|
||||
ReactiveFormsModule,
|
||||
TextFieldModule
|
||||
],
|
||||
providers: [AdminMarketDataService],
|
||||
selector: 'gf-asset-profile-dialog',
|
||||
styleUrls: ['./asset-profile-dialog.component.scss'],
|
||||
standalone: false
|
||||
templateUrl: 'asset-profile-dialog.html'
|
||||
})
|
||||
export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
export class GfAssetProfileDialogComponent implements OnDestroy, OnInit {
|
||||
private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format(
|
||||
new Date(),
|
||||
DATE_FORMAT
|
||||
@@ -72,15 +126,18 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
@ViewChild('assetProfileFormElement')
|
||||
assetProfileFormElement: ElementRef<HTMLFormElement>;
|
||||
|
||||
public assetProfileClass: string;
|
||||
public assetClassLabel: string;
|
||||
public assetSubClassLabel: string;
|
||||
|
||||
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
|
||||
return { id: assetClass, label: translate(assetClass) };
|
||||
});
|
||||
public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass)
|
||||
.map((id) => {
|
||||
return { id, label: translate(id) } as AssetClassSelectorOption;
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
|
||||
public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => {
|
||||
return { id: assetSubClass, label: translate(assetSubClass) };
|
||||
});
|
||||
public assetSubClassOptions: AssetClassSelectorOption[] = [];
|
||||
|
||||
public assetProfile: AdminMarketDataDetails['assetProfile'];
|
||||
|
||||
@@ -122,7 +179,6 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
);
|
||||
|
||||
public assetProfileSubClass: string;
|
||||
public benchmarks: Partial<SymbolProfile>[];
|
||||
|
||||
public countries: {
|
||||
@@ -133,7 +189,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public isBenchmark = false;
|
||||
public isDataGatheringEnabled: boolean;
|
||||
public isEditAssetProfileIdentifierMode = false;
|
||||
public isUUID = isUUID;
|
||||
public marketDataItems: MarketData[] = [];
|
||||
|
||||
public modeValues = [
|
||||
@@ -163,7 +221,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<AssetProfileDialog>,
|
||||
public dialogRef: MatDialogRef<GfAssetProfileDialogComponent>,
|
||||
private formBuilder: FormBuilder,
|
||||
private notificationService: NotificationService,
|
||||
private snackBar: MatSnackBar,
|
||||
@@ -196,6 +254,16 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
public initialize() {
|
||||
this.historicalDataItems = undefined;
|
||||
|
||||
this.adminService
|
||||
.fetchAdminData()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(({ settings }) => {
|
||||
this.isDataGatheringEnabled =
|
||||
settings[PROPERTY_IS_DATA_GATHERING_ENABLED] === false ? false : true;
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.userService.stateChanged
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((state) => {
|
||||
@@ -204,6 +272,26 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
}
|
||||
});
|
||||
|
||||
this.assetProfileForm
|
||||
.get('assetClass')
|
||||
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((assetClass) => {
|
||||
const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? [];
|
||||
|
||||
this.assetSubClassOptions = assetSubClasses
|
||||
.map((assetSubClass) => {
|
||||
return {
|
||||
id: assetSubClass,
|
||||
label: translate(assetSubClass)
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.label.localeCompare(b.label));
|
||||
|
||||
this.assetProfileForm.get('assetSubClass').setValue(null);
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchMarketDataBySymbol({
|
||||
dataSource: this.data.dataSource,
|
||||
@@ -213,8 +301,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
.subscribe(({ assetProfile, marketData }) => {
|
||||
this.assetProfile = assetProfile;
|
||||
|
||||
this.assetProfileClass = translate(this.assetProfile?.assetClass);
|
||||
this.assetProfileSubClass = translate(this.assetProfile?.assetSubClass);
|
||||
this.assetClassLabel = translate(this.assetProfile?.assetClass);
|
||||
this.assetSubClassLabel = translate(this.assetProfile?.assetSubClass);
|
||||
this.countries = {};
|
||||
|
||||
this.isBenchmark = this.benchmarks.some(({ id }) => {
|
||||
@@ -260,7 +348,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
|
||||
),
|
||||
currency: this.assetProfile?.currency,
|
||||
historicalData: {
|
||||
csvString: AssetProfileDialog.HISTORICAL_DATA_TEMPLATE
|
||||
csvString: GfAssetProfileDialogComponent.HISTORICAL_DATA_TEMPLATE
|
||||
},
|
||||
isActive: this.assetProfile?.isActive,
|
||||
name: this.assetProfile.name ?? this.assetProfile.symbol,
|
||||
|
@@ -72,10 +72,12 @@
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="true"
|
||||
[label]="
|
||||
isUUID(data.symbol) ? (assetProfile?.name ?? data.symbol) : data.symbol
|
||||
"
|
||||
[locale]="data.locale"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[symbol]="data.symbol"
|
||||
/>
|
||||
<div class="mb-3">
|
||||
<mat-accordion class="my-3">
|
||||
@@ -209,8 +211,8 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[hidden]="!assetProfileClass"
|
||||
[value]="assetProfileClass"
|
||||
[hidden]="!assetClassLabel"
|
||||
[value]="assetClassLabel"
|
||||
>Asset Class</gf-value
|
||||
>
|
||||
</div>
|
||||
@@ -218,8 +220,8 @@
|
||||
<gf-value
|
||||
i18n
|
||||
size="medium"
|
||||
[hidden]="!assetProfileSubClass"
|
||||
[value]="assetProfileSubClass"
|
||||
[hidden]="!assetSubClassLabel"
|
||||
[value]="assetSubClassLabel"
|
||||
>Asset Sub Class</gf-value
|
||||
>
|
||||
</div>
|
||||
@@ -304,9 +306,12 @@
|
||||
<mat-label i18n>Asset Class</mat-label>
|
||||
<mat-select formControlName="assetClass">
|
||||
<mat-option [value]="null" />
|
||||
@for (assetClass of assetClasses; track assetClass) {
|
||||
<mat-option [value]="assetClass.id">{{
|
||||
assetClass.label
|
||||
@for (
|
||||
assetClassOption of assetClassOptions;
|
||||
track assetClassOption.id
|
||||
) {
|
||||
<mat-option [value]="assetClassOption.id">{{
|
||||
assetClassOption.label
|
||||
}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@@ -317,9 +322,12 @@
|
||||
<mat-label i18n>Asset Sub Class</mat-label>
|
||||
<mat-select formControlName="assetSubClass">
|
||||
<mat-option [value]="null" />
|
||||
@for (assetSubClass of assetSubClasses; track assetSubClass) {
|
||||
<mat-option [value]="assetSubClass.id">{{
|
||||
assetSubClass.label
|
||||
@for (
|
||||
assetSubClassOption of assetSubClassOptions;
|
||||
track assetSubClassOption.id
|
||||
) {
|
||||
<mat-option [value]="assetSubClassOption.id">{{
|
||||
assetSubClassOption.label
|
||||
}}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@@ -534,8 +542,8 @@
|
||||
<div class="gf-spacer">
|
||||
<mat-checkbox
|
||||
color="primary"
|
||||
[checked]="assetProfile?.isActive ?? false"
|
||||
[disabled]="isEditAssetProfileIdentifierMode"
|
||||
[checked]="isDataGatheringEnabled && (assetProfile?.isActive ?? false)"
|
||||
[disabled]="!isDataGatheringEnabled || isEditAssetProfileIdentifierMode"
|
||||
(change)="onToggleIsActive($event)"
|
||||
>
|
||||
<ng-container i18n>Data Gathering</ng-container>
|
||||
|
@@ -1,53 +0,0 @@
|
||||
import { AdminMarketDataService } from '@ghostfolio/client/components/admin-market-data/admin-market-data.service';
|
||||
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor';
|
||||
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
|
||||
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { AssetProfileDialog } from './asset-profile-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AssetProfileDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfCurrencySelectorComponent,
|
||||
GfEntityLogoComponent,
|
||||
GfHistoricalMarketDataEditorComponent,
|
||||
GfLineChartComponent,
|
||||
GfPortfolioProportionChartComponent,
|
||||
GfSymbolAutocompleteComponent,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDialogModule,
|
||||
MatExpansionModule,
|
||||
MatInputModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
MatSnackBarModule,
|
||||
ReactiveFormsModule,
|
||||
TextFieldModule
|
||||
],
|
||||
providers: [AdminMarketDataService],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAssetProfileDialogModule {}
|
@@ -1,6 +1,11 @@
|
||||
import { ColorScheme } from '@ghostfolio/common/types';
|
||||
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
|
||||
|
||||
export interface AssetClassSelectorOption {
|
||||
id: AssetClass | AssetSubClass;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface AssetProfileDialogParams {
|
||||
colorScheme: ColorScheme;
|
||||
|
@@ -1,6 +1,10 @@
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
||||
import {
|
||||
ghostfolioPrefix,
|
||||
PROPERTY_CURRENCIES
|
||||
} from '@ghostfolio/common/config';
|
||||
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -14,11 +18,17 @@ import {
|
||||
FormBuilder,
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { isISO4217CurrencyCode } from 'class-validator';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@@ -27,12 +37,21 @@ import { CreateAssetProfileDialogMode } from './interfaces/interfaces';
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
imports: [
|
||||
FormsModule,
|
||||
GfSymbolAutocompleteComponent,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
selector: 'gf-create-asset-profile-dialog',
|
||||
styleUrls: ['./create-asset-profile-dialog.component.scss'],
|
||||
templateUrl: 'create-asset-profile-dialog.html',
|
||||
standalone: false
|
||||
templateUrl: 'create-asset-profile-dialog.html'
|
||||
})
|
||||
export class CreateAssetProfileDialog implements OnInit, OnDestroy {
|
||||
export class GfCreateAssetProfileDialogComponent implements OnInit, OnDestroy {
|
||||
public createAssetProfileForm: FormGroup;
|
||||
public mode: CreateAssetProfileDialogMode;
|
||||
|
||||
@@ -43,7 +62,7 @@ export class CreateAssetProfileDialog implements OnInit, OnDestroy {
|
||||
public readonly adminService: AdminService,
|
||||
private readonly changeDetectorRef: ChangeDetectorRef,
|
||||
private readonly dataService: DataService,
|
||||
public readonly dialogRef: MatDialogRef<CreateAssetProfileDialog>,
|
||||
public readonly dialogRef: MatDialogRef<GfCreateAssetProfileDialogComponent>,
|
||||
public readonly formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
@@ -55,7 +74,9 @@ export class CreateAssetProfileDialog implements OnInit, OnDestroy {
|
||||
addCurrency: new FormControl(null, [
|
||||
this.iso4217CurrencyCodeValidator()
|
||||
]),
|
||||
addSymbol: new FormControl(null, [Validators.required]),
|
||||
addSymbol: new FormControl(`${ghostfolioPrefix}_`, [
|
||||
Validators.required
|
||||
]),
|
||||
searchSymbol: new FormControl(null, [Validators.required])
|
||||
},
|
||||
{
|
||||
|
@@ -1,29 +0,0 @@
|
||||
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
|
||||
import { CreateAssetProfileDialog } from './create-asset-profile-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateAssetProfileDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfSymbolAutocompleteComponent,
|
||||
MatDialogModule,
|
||||
MatButtonModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatRadioModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfCreateAssetProfileDialogModule {}
|
@@ -12,6 +12,7 @@ import {
|
||||
PROPERTY_SYSTEM_MESSAGE,
|
||||
ghostfolioPrefix
|
||||
} from '@ghostfolio/common/config';
|
||||
import { getDateFnsLocale } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Coupon,
|
||||
InfoItem,
|
||||
@@ -19,11 +20,24 @@ import {
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import {
|
||||
MatSlideToggleChange,
|
||||
MatSlideToggleModule
|
||||
} from '@angular/material/slide-toggle';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import {
|
||||
addMilliseconds,
|
||||
differenceInSeconds,
|
||||
formatDistanceToNowStrict,
|
||||
parseISO
|
||||
@@ -41,12 +55,25 @@ import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
MatSnackBarModule,
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
selector: 'gf-admin-overview',
|
||||
styleUrls: ['./admin-overview.scss'],
|
||||
templateUrl: './admin-overview.html',
|
||||
standalone: false
|
||||
templateUrl: './admin-overview.html'
|
||||
})
|
||||
export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
export class GfAdminOverviewComponent implements OnDestroy, OnInit {
|
||||
public couponDuration: StringValue = '14 days';
|
||||
public coupons: Coupon[];
|
||||
public hasPermissionForSubscription: boolean;
|
||||
@@ -131,6 +158,15 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
return '';
|
||||
}
|
||||
|
||||
public formatStringValue(aStringValue: StringValue) {
|
||||
return formatDistanceToNowStrict(
|
||||
addMilliseconds(new Date(), ms(aStringValue)),
|
||||
{
|
||||
locale: getDateFnsLocale(this.user?.settings?.language)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public onAddCoupon() {
|
||||
const coupons = [
|
||||
...this.coupons,
|
||||
|
@@ -105,7 +105,9 @@
|
||||
@for (coupon of coupons; track coupon) {
|
||||
<tr>
|
||||
<td class="text-monospace">{{ coupon.code }}</td>
|
||||
<td class="pl-2 text-right">{{ coupon.duration }}</td>
|
||||
<td class="pl-2 text-right">
|
||||
{{ formatStringValue(coupon.duration) }}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
class="mx-1 no-min-width px-2"
|
||||
@@ -145,12 +147,24 @@
|
||||
[value]="couponDuration"
|
||||
(selectionChange)="onChangeCouponDuration($event.value)"
|
||||
>
|
||||
<mat-option value="7 days">7 Days</mat-option>
|
||||
<mat-option value="14 days">14 Days</mat-option>
|
||||
<mat-option value="30 days">30 Days</mat-option>
|
||||
<mat-option value="90 days">90 Days</mat-option>
|
||||
<mat-option value="180 days">180 Days</mat-option>
|
||||
<mat-option value="1 year">1 Year</mat-option>
|
||||
<mat-option value="7 days">{{
|
||||
formatStringValue('7 days')
|
||||
}}</mat-option>
|
||||
<mat-option value="14 days">{{
|
||||
formatStringValue('14 days')
|
||||
}}</mat-option>
|
||||
<mat-option value="30 days">{{
|
||||
formatStringValue('30 days')
|
||||
}}</mat-option>
|
||||
<mat-option value="90 days">{{
|
||||
formatStringValue('90 days')
|
||||
}}</mat-option>
|
||||
<mat-option value="180 days">{{
|
||||
formatStringValue('180 days')
|
||||
}}</mat-option>
|
||||
<mat-option value="1 year">{{
|
||||
formatStringValue('1 year')
|
||||
}}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button
|
||||
|
@@ -1,38 +0,0 @@
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { AdminOverviewComponent } from './admin-overview.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminOverviewComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatSelectModule,
|
||||
MatSnackBarModule,
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [CacheService],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminOverviewModule {}
|
@@ -5,6 +5,7 @@ import { NotificationService } from '@ghostfolio/client/core/notification/notifi
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -14,10 +15,13 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { Platform } from '@prisma/client';
|
||||
import { addIcons } from 'ionicons';
|
||||
import {
|
||||
@@ -29,16 +33,24 @@ import { get } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog/create-or-update-platform-dialog.component';
|
||||
import { GfCreateOrUpdatePlatformDialogComponent } from './create-or-update-platform-dialog/create-or-update-platform-dialog.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
GfEntityLogoComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
selector: 'gf-admin-platform',
|
||||
styleUrls: ['./admin-platform.component.scss'],
|
||||
templateUrl: './admin-platform.component.html',
|
||||
standalone: false
|
||||
templateUrl: './admin-platform.component.html'
|
||||
})
|
||||
export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
export class GfAdminPlatformComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public dataSource = new MatTableDataSource<Platform>();
|
||||
@@ -141,16 +153,19 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private openCreatePlatformDialog() {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdatePlatformDialog, {
|
||||
data: {
|
||||
platform: {
|
||||
name: null,
|
||||
url: null
|
||||
}
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '98vh' : undefined,
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
const dialogRef = this.dialog.open(
|
||||
GfCreateOrUpdatePlatformDialogComponent,
|
||||
{
|
||||
data: {
|
||||
platform: {
|
||||
name: null,
|
||||
url: null
|
||||
}
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '98vh' : undefined,
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
}
|
||||
);
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
@@ -177,17 +192,20 @@ export class AdminPlatformComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private openUpdatePlatformDialog({ id, name, url }) {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdatePlatformDialog, {
|
||||
data: {
|
||||
platform: {
|
||||
id,
|
||||
name,
|
||||
url
|
||||
}
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '98vh' : undefined,
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
});
|
||||
const dialogRef = this.dialog.open(
|
||||
GfCreateOrUpdatePlatformDialogComponent,
|
||||
{
|
||||
data: {
|
||||
platform: {
|
||||
id,
|
||||
name,
|
||||
url
|
||||
}
|
||||
},
|
||||
height: this.deviceType === 'mobile' ? '98vh' : undefined,
|
||||
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
|
||||
}
|
||||
);
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
|
@@ -1,31 +0,0 @@
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { AdminPlatformComponent } from './admin-platform.component';
|
||||
import { GfCreateOrUpdatePlatformDialogModule } from './create-or-update-platform-dialog/create-or-update-platform-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminPlatformComponent],
|
||||
exports: [AdminPlatformComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfCreateOrUpdatePlatformDialogModule,
|
||||
GfEntityLogoComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminPlatformModule {}
|
@@ -1,6 +1,7 @@
|
||||
import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto';
|
||||
import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto';
|
||||
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -8,8 +9,21 @@ import {
|
||||
Inject,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogModule,
|
||||
MatDialogRef
|
||||
} from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces';
|
||||
@@ -17,24 +31,32 @@ import { CreateOrUpdatePlatformDialogParams } from './interfaces/interfaces';
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
imports: [
|
||||
FormsModule,
|
||||
GfEntityLogoComponent,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
selector: 'gf-create-or-update-platform-dialog',
|
||||
styleUrls: ['./create-or-update-platform-dialog.scss'],
|
||||
templateUrl: 'create-or-update-platform-dialog.html',
|
||||
standalone: false
|
||||
templateUrl: 'create-or-update-platform-dialog.html'
|
||||
})
|
||||
export class CreateOrUpdatePlatformDialog implements OnDestroy {
|
||||
export class GfCreateOrUpdatePlatformDialogComponent implements OnDestroy {
|
||||
public platformForm: FormGroup;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdatePlatformDialogParams,
|
||||
public dialogRef: MatDialogRef<CreateOrUpdatePlatformDialog>,
|
||||
public dialogRef: MatDialogRef<GfCreateOrUpdatePlatformDialogComponent>,
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
this.platformForm = this.formBuilder.group({
|
||||
name: [this.data.platform.name, Validators.required],
|
||||
url: [this.data.platform.url, Validators.required]
|
||||
url: [this.data.platform.url ?? 'https://', Validators.required]
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -28,8 +28,12 @@
|
||||
matInput
|
||||
(keydown.enter)="$event.stopPropagation()"
|
||||
/>
|
||||
@if (data.platform.url) {
|
||||
<gf-entity-logo class="mr-3" matSuffix [url]="data.platform.url" />
|
||||
@if (platformForm.get('url')?.value) {
|
||||
<gf-entity-logo
|
||||
class="mr-3"
|
||||
matSuffix
|
||||
[url]="platformForm.get('url').value"
|
||||
/>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
@@ -1,26 +0,0 @@
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
import { CreateOrUpdatePlatformDialog } from './create-or-update-platform-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateOrUpdatePlatformDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfEntityLogoComponent,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
]
|
||||
})
|
||||
export class GfCreateOrUpdatePlatformDialogModule {}
|
@@ -1,3 +1,6 @@
|
||||
import { GfAdminPlatformComponent } from '@ghostfolio/client/components/admin-platform/admin-platform.component';
|
||||
import { GfAdminTagComponent } from '@ghostfolio/client/components/admin-tag/admin-tag.component';
|
||||
import { GfDataProviderStatusComponent } from '@ghostfolio/client/components/data-provider-status/data-provider-status.component';
|
||||
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
|
||||
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
@@ -11,7 +14,11 @@ import {
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { publicRoutes } from '@ghostfolio/common/routes/routes';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
@@ -19,19 +26,42 @@ import {
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { ellipsisHorizontal, trashOutline } from 'ionicons/icons';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { catchError, filter, of, Subject, takeUntil } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAdminPlatformComponent,
|
||||
GfAdminTagComponent,
|
||||
GfDataProviderStatusComponent,
|
||||
GfEntityLogoComponent,
|
||||
GfPremiumIndicatorComponent,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
selector: 'gf-admin-settings',
|
||||
styleUrls: ['./admin-settings.component.scss'],
|
||||
templateUrl: './admin-settings.component.html',
|
||||
standalone: false
|
||||
templateUrl: './admin-settings.component.html'
|
||||
})
|
||||
export class AdminSettingsComponent implements OnDestroy, OnInit {
|
||||
export class GfAdminSettingsComponent implements OnDestroy, OnInit {
|
||||
public dataSource = new MatTableDataSource<DataProviderInfo>();
|
||||
public defaultDateFormat: string;
|
||||
public displayedColumns = [
|
||||
|
@@ -1,42 +0,0 @@
|
||||
import { GfAdminPlatformModule } from '@ghostfolio/client/components/admin-platform/admin-platform.module';
|
||||
import { GfAdminTagModule } from '@ghostfolio/client/components/admin-tag/admin-tag.module';
|
||||
import { GfDataProviderStatusComponent } from '@ghostfolio/client/components/data-provider-status/data-provider-status.component';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { AdminSettingsComponent } from './admin-settings.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminSettingsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAdminPlatformModule,
|
||||
GfAdminTagModule,
|
||||
GfDataProviderStatusComponent,
|
||||
GfEntityLogoComponent,
|
||||
GfPremiumIndicatorComponent,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatMenuModule,
|
||||
MatProgressBarModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminSettingsModule {}
|
@@ -13,10 +13,13 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSort, MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { Tag } from '@prisma/client';
|
||||
import { addIcons } from 'ionicons';
|
||||
import {
|
||||
@@ -28,16 +31,23 @@ import { get } from 'lodash';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, takeUntil } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdateTagDialog } from './create-or-update-tag-dialog/create-or-update-tag-dialog.component';
|
||||
import { GfCreateOrUpdateTagDialogComponent } from './create-or-update-tag-dialog/create-or-update-tag-dialog.component';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
selector: 'gf-admin-tag',
|
||||
styleUrls: ['./admin-tag.component.scss'],
|
||||
templateUrl: './admin-tag.component.html',
|
||||
standalone: false
|
||||
templateUrl: './admin-tag.component.html'
|
||||
})
|
||||
export class AdminTagComponent implements OnInit, OnDestroy {
|
||||
export class GfAdminTagComponent implements OnInit, OnDestroy {
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public dataSource = new MatTableDataSource<Tag>();
|
||||
@@ -139,7 +149,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private openCreateTagDialog() {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTagDialog, {
|
||||
const dialogRef = this.dialog.open(GfCreateOrUpdateTagDialogComponent, {
|
||||
data: {
|
||||
tag: {
|
||||
name: null
|
||||
@@ -174,7 +184,7 @@ export class AdminTagComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private openUpdateTagDialog({ id, name }) {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateTagDialog, {
|
||||
const dialogRef = this.dialog.open(GfCreateOrUpdateTagDialogComponent, {
|
||||
data: {
|
||||
tag: {
|
||||
id,
|
||||
|
@@ -1,28 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { AdminTagComponent } from './admin-tag.component';
|
||||
import { GfCreateOrUpdateTagDialogModule } from './create-or-update-tag-dialog/create-or-update-tag-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminTagComponent],
|
||||
exports: [AdminTagComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfCreateOrUpdateTagDialogModule,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatSortModule,
|
||||
MatTableModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminTagModule {}
|
@@ -8,8 +8,20 @@ import {
|
||||
Inject,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, FormGroup } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogModule,
|
||||
MatDialogRef
|
||||
} from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces';
|
||||
@@ -17,19 +29,26 @@ import { CreateOrUpdateTagDialogParams } from './interfaces/interfaces';
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
imports: [
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
selector: 'gf-create-or-update-tag-dialog',
|
||||
styleUrls: ['./create-or-update-tag-dialog.scss'],
|
||||
templateUrl: 'create-or-update-tag-dialog.html',
|
||||
standalone: false
|
||||
templateUrl: 'create-or-update-tag-dialog.html'
|
||||
})
|
||||
export class CreateOrUpdateTagDialog implements OnDestroy {
|
||||
export class GfCreateOrUpdateTagDialogComponent implements OnDestroy {
|
||||
public tagForm: FormGroup;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTagDialogParams,
|
||||
public dialogRef: MatDialogRef<CreateOrUpdateTagDialog>,
|
||||
public dialogRef: MatDialogRef<GfCreateOrUpdateTagDialogComponent>,
|
||||
private formBuilder: FormBuilder
|
||||
) {
|
||||
this.tagForm = this.formBuilder.group({
|
||||
|
@@ -1,23 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
import { CreateOrUpdateTagDialog } from './create-or-update-tag-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateOrUpdateTagDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
]
|
||||
})
|
||||
export class GfCreateOrUpdateTagDialogModule {}
|
@@ -7,7 +7,10 @@ import {
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { AdminUsers, InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -15,8 +18,15 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import {
|
||||
MatPaginator,
|
||||
MatPaginatorModule,
|
||||
PageEvent
|
||||
} from '@angular/material/paginator';
|
||||
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import {
|
||||
differenceInSeconds,
|
||||
formatDistanceToNowStrict,
|
||||
@@ -29,6 +39,7 @@ import {
|
||||
keyOutline,
|
||||
trashOutline
|
||||
} from 'ionicons/icons';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@@ -40,12 +51,22 @@ import { ImpersonationStorageService } from '../../services/impersonation-storag
|
||||
import { UserService } from '../../services/user/user.service';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPremiumIndicatorComponent,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
selector: 'gf-admin-users',
|
||||
standalone: false,
|
||||
styleUrls: ['./admin-users.scss'],
|
||||
templateUrl: './admin-users.html'
|
||||
})
|
||||
export class AdminUsersComponent implements OnDestroy, OnInit {
|
||||
export class GfAdminUsersComponent implements OnDestroy, OnInit {
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
|
||||
public dataSource = new MatTableDataSource<AdminUsers['users'][0]>();
|
||||
|
@@ -1,31 +0,0 @@
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
import { AdminUsersComponent } from './admin-users.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminUsersComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfPremiumIndicatorComponent,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatMenuModule,
|
||||
MatPaginatorModule,
|
||||
MatTableModule,
|
||||
NgxSkeletonLoaderModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfAdminUsersModule {}
|
@@ -1,5 +1,5 @@
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||
import { GfAccountsTableComponent } from '@ghostfolio/client/components/accounts-table/accounts-table.component';
|
||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
@@ -46,12 +46,14 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { SortDirection } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { Router } from '@angular/router';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { Account, MarketData, Tag } from '@prisma/client';
|
||||
import { isUUID } from 'class-validator';
|
||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||
import { addIcons } from 'ionicons';
|
||||
import {
|
||||
createOutline,
|
||||
flagOutline,
|
||||
readerOutline,
|
||||
serverOutline,
|
||||
@@ -69,7 +71,7 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
|
||||
host: { class: 'd-flex flex-column h-100' },
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAccountsTableModule,
|
||||
GfAccountsTableComponent,
|
||||
GfActivitiesTableComponent,
|
||||
GfDataProviderCreditsComponent,
|
||||
GfDialogFooterModule,
|
||||
@@ -85,7 +87,8 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces';
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatTabsModule,
|
||||
NgxSkeletonLoaderModule
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-holding-detail-dialog',
|
||||
@@ -99,6 +102,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
public assetSubClass: string;
|
||||
public averagePrice: number;
|
||||
public benchmarkDataItems: LineChartItem[];
|
||||
public benchmarkLabel = $localize`Average Unit Price`;
|
||||
public countries: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
@@ -114,6 +118,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public investmentInBaseCurrencyWithCurrencyEffect: number;
|
||||
public investmentInBaseCurrencyWithCurrencyEffectPrecision = 2;
|
||||
public isUUID = isUUID;
|
||||
public marketDataItems: MarketData[] = [];
|
||||
public marketPrice: number;
|
||||
public marketPriceMax: number;
|
||||
@@ -127,6 +132,8 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
public quantity: number;
|
||||
public quantityPrecision = 2;
|
||||
public reportDataGlitchMail: string;
|
||||
public routerLinkAdminControlMarketData =
|
||||
internalRoutes.adminControl.subRoutes.marketData.routerLink;
|
||||
public sectors: {
|
||||
[name: string]: { name: string; value: number };
|
||||
};
|
||||
@@ -152,6 +159,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
|
||||
private userService: UserService
|
||||
) {
|
||||
addIcons({
|
||||
createOutline,
|
||||
flagOutline,
|
||||
readerOutline,
|
||||
serverOutline,
|
||||
|
@@ -21,18 +21,20 @@
|
||||
</div>
|
||||
|
||||
<gf-line-chart
|
||||
benchmarkLabel="Average Unit Price"
|
||||
class="mb-4"
|
||||
[benchmarkDataItems]="benchmarkDataItems"
|
||||
[benchmarkLabel]="benchmarkLabel"
|
||||
[colorScheme]="data.colorScheme"
|
||||
[currency]="SymbolProfile?.currency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="true"
|
||||
[label]="
|
||||
isUUID(data.symbol) ? (SymbolProfile?.name ?? data.symbol) : data.symbol
|
||||
"
|
||||
[locale]="data.locale"
|
||||
[showGradient]="true"
|
||||
[showXAxis]="true"
|
||||
[showYAxis]="true"
|
||||
[symbol]="data.symbol"
|
||||
/>
|
||||
|
||||
<mat-tab-group
|
||||
@@ -412,16 +414,37 @@
|
||||
/>
|
||||
|
||||
@if (
|
||||
dataSource?.data.length > 0 &&
|
||||
data.hasPermissionToReportDataGlitch === true
|
||||
data.hasPermissionToAccessAdminControl ||
|
||||
(dataSource?.data.length > 0 &&
|
||||
data.hasPermissionToReportDataGlitch === true)
|
||||
) {
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<hr />
|
||||
<a color="warn" mat-stroked-button [href]="reportDataGlitchMail"
|
||||
><ion-icon class="mr-1" name="flag-outline"></ion-icon
|
||||
><span i18n>Report Data Glitch</span></a
|
||||
>
|
||||
@if (data.hasPermissionToAccessAdminControl) {
|
||||
<a
|
||||
class="mr-2"
|
||||
mat-stroked-button
|
||||
[queryParams]="{
|
||||
assetProfileDialog: true,
|
||||
dataSource: SymbolProfile?.dataSource,
|
||||
symbol: SymbolProfile?.symbol
|
||||
}"
|
||||
[routerLink]="routerLinkAdminControlMarketData"
|
||||
(click)="onClose()"
|
||||
><ion-icon class="mr-1" name="create-outline"></ion-icon
|
||||
><span i18n>Manage Asset Profile</span>...</a
|
||||
>
|
||||
}
|
||||
@if (
|
||||
dataSource?.data.length > 0 &&
|
||||
data.hasPermissionToReportDataGlitch === true
|
||||
) {
|
||||
<a color="warn" mat-stroked-button [href]="reportDataGlitchMail"
|
||||
><ion-icon class="mr-1" name="flag-outline"></ion-icon
|
||||
><span i18n>Report Data Glitch</span>...</a
|
||||
>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ export interface HoldingDetailDialogParams {
|
||||
dataSource: DataSource;
|
||||
deviceType: string;
|
||||
hasImpersonationId: boolean;
|
||||
hasPermissionToAccessAdminControl: boolean;
|
||||
hasPermissionToCreateOrder: boolean;
|
||||
hasPermissionToReportDataGlitch: boolean;
|
||||
hasPermissionToUpdateOrder: boolean;
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
@@ -10,10 +11,22 @@ import {
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { internalRoutes } from '@ghostfolio/common/routes/routes';
|
||||
import { HoldingType, HoldingsViewMode } from '@ghostfolio/common/types';
|
||||
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
|
||||
import { GfTreemapChartComponent } from '@ghostfolio/ui/treemap-chart';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { Router, RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { gridOutline, reorderFourOutline } from 'ionicons/icons';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
@@ -21,12 +34,24 @@ import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfHoldingsTableComponent,
|
||||
GfToggleModule,
|
||||
GfTreemapChartComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-home-holdings',
|
||||
styleUrls: ['./home-holdings.scss'],
|
||||
templateUrl: './home-holdings.html',
|
||||
standalone: false
|
||||
templateUrl: './home-holdings.html'
|
||||
})
|
||||
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE';
|
||||
|
||||
public deviceType: string;
|
||||
@@ -43,7 +68,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
internalRoutes.portfolio.subRoutes.activities.routerLink;
|
||||
public user: User;
|
||||
public viewModeFormControl = new FormControl<HoldingsViewMode>(
|
||||
HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE
|
||||
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE
|
||||
);
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@@ -153,14 +178,14 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.viewModeFormControl.setValue(
|
||||
this.deviceType === 'mobile'
|
||||
? HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE
|
||||
? GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE
|
||||
: this.user?.settings?.holdingsViewMode ||
|
||||
HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE,
|
||||
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE,
|
||||
{ emitEvent: false }
|
||||
);
|
||||
} else if (this.holdingType === 'CLOSED') {
|
||||
this.viewModeFormControl.setValue(
|
||||
HomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE,
|
||||
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE,
|
||||
{ emitEvent: false }
|
||||
);
|
||||
}
|
||||
|
@@ -1,31 +0,0 @@
|
||||
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
|
||||
import { GfTreemapChartComponent } from '@ghostfolio/ui/treemap-chart';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { HomeHoldingsComponent } from './home-holdings.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeHoldingsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfHoldingsTableComponent,
|
||||
GfToggleModule,
|
||||
GfTreemapChartComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatButtonToggleModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeHoldingsModule {}
|
@@ -1,3 +1,4 @@
|
||||
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
||||
@@ -9,17 +10,30 @@ import {
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
|
||||
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
GfBenchmarkComponent,
|
||||
GfFearAndGreedIndexModule,
|
||||
GfLineChartComponent
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-home-market',
|
||||
styleUrls: ['./home-market.scss'],
|
||||
templateUrl: './home-market.html',
|
||||
standalone: false
|
||||
templateUrl: './home-market.html'
|
||||
})
|
||||
export class HomeMarketComponent implements OnDestroy, OnInit {
|
||||
public benchmarks: Benchmark[];
|
||||
|
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<gf-line-chart
|
||||
class="mb-3"
|
||||
symbol="Fear & Greed Index"
|
||||
label="Fear & Greed Index"
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="true"
|
||||
|
@@ -1,21 +0,0 @@
|
||||
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
|
||||
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
|
||||
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
|
||||
import { HomeMarketComponent } from './home-market.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeMarketComponent],
|
||||
exports: [HomeMarketComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfBenchmarkComponent,
|
||||
GfFearAndGreedIndexModule,
|
||||
GfLineChartComponent
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeMarketModule {}
|
@@ -1,3 +1,4 @@
|
||||
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
|
||||
import { LayoutService } from '@ghostfolio/client/core/layout.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
@@ -12,19 +13,36 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { internalRoutes } from '@ghostfolio/common/routes/routes';
|
||||
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfLineChartComponent,
|
||||
GfPortfolioPerformanceModule,
|
||||
MatButtonModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-home-overview',
|
||||
styleUrls: ['./home-overview.scss'],
|
||||
templateUrl: './home-overview.html',
|
||||
standalone: false
|
||||
templateUrl: './home-overview.html'
|
||||
})
|
||||
export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
export class GfHomeOverviewComponent implements OnDestroy, OnInit {
|
||||
public dateRangeOptions = ToggleComponent.DEFAULT_DATE_RANGE_OPTIONS;
|
||||
public deviceType: string;
|
||||
public errors: AssetProfileIdentifier[];
|
||||
@@ -36,6 +54,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||
public isAllTimeLow: boolean;
|
||||
public isLoadingPerformance = true;
|
||||
public performance: PortfolioPerformance;
|
||||
public performanceLabel = $localize`Performance`;
|
||||
public precision = 2;
|
||||
public routerLinkAccounts = internalRoutes.accounts.routerLink;
|
||||
public routerLinkPortfolio = internalRoutes.portfolio.routerLink;
|
||||
|
@@ -65,12 +65,12 @@
|
||||
<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"
|
||||
[label]="performanceLabel"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
||||
[showGradient]="true"
|
||||
|
@@ -1,24 +0,0 @@
|
||||
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
|
||||
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
|
||||
import { GfNoTransactionsInfoComponent } from '@ghostfolio/ui/no-transactions-info';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { HomeOverviewComponent } from './home-overview.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeOverviewComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfLineChartComponent,
|
||||
GfNoTransactionsInfoComponent,
|
||||
GfPortfolioPerformanceModule,
|
||||
MatButtonModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeOverviewModule {}
|
@@ -1,3 +1,4 @@
|
||||
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
@@ -8,18 +9,26 @@ import {
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
imports: [GfPortfolioSummaryModule, MatCardModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-home-summary',
|
||||
styleUrls: ['./home-summary.scss'],
|
||||
templateUrl: './home-summary.html',
|
||||
standalone: false
|
||||
templateUrl: './home-summary.html'
|
||||
})
|
||||
export class HomeSummaryComponent implements OnDestroy, OnInit {
|
||||
export class GfHomeSummaryComponent implements OnDestroy, OnInit {
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
|
@@ -1,14 +0,0 @@
|
||||
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
|
||||
import { HomeSummaryComponent } from './home-summary.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [HomeSummaryComponent],
|
||||
imports: [CommonModule, GfPortfolioSummaryModule, MatCardModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfHomeSummaryModule {}
|
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
<gf-line-chart
|
||||
class="mb-3"
|
||||
symbol="Fear & Greed Index"
|
||||
label="Fear & Greed Index"
|
||||
[colorScheme]="user?.settings?.colorScheme"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[isAnimated]="true"
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<div mat-dialog-title>{{ data.rule.name }}</div>
|
||||
<div mat-dialog-title>{{ data.rule.categoryName }} › {{ data.rule.name }}</div>
|
||||
|
||||
<div class="py-3" mat-dialog-content>
|
||||
@if (
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
|
||||
import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component';
|
||||
import { ConfirmationDialogType } from '@ghostfolio/client/core/notification/confirmation-dialog/confirmation-dialog.type';
|
||||
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
@@ -6,17 +7,24 @@ import { TokenStorageService } from '@ghostfolio/client/services/token-storage.s
|
||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, Validators } from '@angular/forms';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { addOutline, eyeOffOutline, eyeOutline } from 'ionicons/icons';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
@@ -24,16 +32,30 @@ import { EMPTY, Subject } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component';
|
||||
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'has-fab' },
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfAccessTableComponent,
|
||||
GfCreateOrUpdateAccessDialogModule,
|
||||
GfPremiumIndicatorComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-user-account-access',
|
||||
styleUrls: ['./user-account-access.scss'],
|
||||
templateUrl: './user-account-access.html',
|
||||
standalone: false
|
||||
templateUrl: './user-account-access.html'
|
||||
})
|
||||
export class UserAccountAccessComponent implements OnDestroy, OnInit {
|
||||
export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
|
||||
public accessesGet: Access[];
|
||||
public accessesGive: Access[];
|
||||
public deviceType: string;
|
||||
|
@@ -1,35 +0,0 @@
|
||||
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-dialog/create-or-update-access-dialog.module';
|
||||
import { UserAccountAccessComponent } from './user-account-access.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [UserAccountAccessComponent],
|
||||
exports: [UserAccountAccessComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfCreateOrUpdateAccessDialogModule,
|
||||
GfPortfolioAccessTableModule,
|
||||
GfPremiumIndicatorComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfUserAccountAccessModule {}
|
@@ -6,14 +6,20 @@ import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { publicRoutes } from '@ghostfolio/common/routes/routes';
|
||||
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
OnDestroy
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import ms, { StringValue } from 'ms';
|
||||
import { StripeService } from 'ngx-stripe';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
@@ -21,12 +27,19 @@ import { catchError, switchMap, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfMembershipCardComponent,
|
||||
GfPremiumIndicatorComponent,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
],
|
||||
selector: 'gf-user-account-membership',
|
||||
styleUrls: ['./user-account-membership.scss'],
|
||||
templateUrl: './user-account-membership.html',
|
||||
standalone: false
|
||||
templateUrl: './user-account-membership.html'
|
||||
})
|
||||
export class UserAccountMembershipComponent implements OnDestroy {
|
||||
export class GfUserAccountMembershipComponent implements OnDestroy {
|
||||
public baseCurrency: string;
|
||||
public coupon: number;
|
||||
public couponId: string;
|
||||
|
@@ -1,26 +0,0 @@
|
||||
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
|
||||
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
import { UserAccountMembershipComponent } from './user-account-membership.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [UserAccountMembershipComponent],
|
||||
exports: [UserAccountMembershipComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfMembershipCardComponent,
|
||||
GfPremiumIndicatorComponent,
|
||||
GfValueComponent,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
]
|
||||
})
|
||||
export class GfUserAccountMembershipModule {}
|
@@ -13,16 +13,33 @@ import { downloadAsFile } from '@ghostfolio/common/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { FormBuilder, Validators } from '@angular/forms';
|
||||
import { MatSlideToggleChange } from '@angular/material/slide-toggle';
|
||||
import {
|
||||
FormBuilder,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import {
|
||||
MatSlideToggleChange,
|
||||
MatSlideToggleModule
|
||||
} from '@angular/material/slide-toggle';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { format, parseISO } from 'date-fns';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { eyeOffOutline, eyeOutline } from 'ionicons/icons';
|
||||
@@ -32,12 +49,25 @@ import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-user-account-settings',
|
||||
styleUrls: ['./user-account-settings.scss'],
|
||||
templateUrl: './user-account-settings.html',
|
||||
standalone: false
|
||||
templateUrl: './user-account-settings.html'
|
||||
})
|
||||
export class UserAccountSettingsComponent implements OnDestroy, OnInit {
|
||||
export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
|
||||
public appearancePlaceholder = $localize`Auto`;
|
||||
public baseCurrency: string;
|
||||
public currencies: string[] = [];
|
||||
|
@@ -1,36 +0,0 @@
|
||||
import { GfValueComponent } from '@ghostfolio/ui/value';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { UserAccountSettingsComponent } from './user-account-settings.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [UserAccountSettingsComponent],
|
||||
exports: [UserAccountSettingsComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfValueComponent,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfUserAccountSettingsModule {}
|
@@ -1,22 +0,0 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { internalRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AccountsPageComponent } from './accounts-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: AccountsPageComponent,
|
||||
path: '',
|
||||
title: internalRoutes.accounts.title
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AccountsPageRoutingModule {}
|
@@ -2,7 +2,9 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
|
||||
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
|
||||
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
|
||||
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
|
||||
import { GfAccountsTableComponent } from '@ghostfolio/client/components/accounts-table/accounts-table.component';
|
||||
import { NotificationService } from '@ghostfolio/client/core/notification/notification.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
@@ -11,8 +13,9 @@ import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { addOutline } from 'ionicons/icons';
|
||||
@@ -20,17 +23,22 @@ import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { EMPTY, Subject, Subscription } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
|
||||
import { TransferBalanceDialog } from './transfer-balance/transfer-balance-dialog.component';
|
||||
import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
|
||||
import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-balance-dialog.component';
|
||||
|
||||
@Component({
|
||||
host: { class: 'has-fab page' },
|
||||
imports: [
|
||||
GfAccountDetailDialogModule,
|
||||
GfAccountsTableComponent,
|
||||
MatButtonModule,
|
||||
RouterModule
|
||||
],
|
||||
selector: 'gf-accounts-page',
|
||||
styleUrls: ['./accounts-page.scss'],
|
||||
templateUrl: './accounts-page.html',
|
||||
standalone: false
|
||||
templateUrl: './accounts-page.html'
|
||||
})
|
||||
export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
export class GfAccountsPageComponent implements OnDestroy, OnInit {
|
||||
public accounts: AccountModel[];
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
@@ -177,7 +185,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
name,
|
||||
platformId
|
||||
}: AccountModel) {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
const dialogRef = this.dialog.open(GfCreateOrUpdateAccountDialogComponent, {
|
||||
data: {
|
||||
account: {
|
||||
balance,
|
||||
@@ -251,7 +259,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private openCreateAccountDialog() {
|
||||
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
|
||||
const dialogRef = this.dialog.open(GfCreateOrUpdateAccountDialogComponent, {
|
||||
data: {
|
||||
account: {
|
||||
balance: 0,
|
||||
@@ -293,7 +301,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
private openTransferBalanceDialog() {
|
||||
const dialogRef = this.dialog.open(TransferBalanceDialog, {
|
||||
const dialogRef = this.dialog.open(GfTransferBalanceDialogComponent, {
|
||||
data: {
|
||||
accounts: this.accounts
|
||||
},
|
||||
|
@@ -1,30 +0,0 @@
|
||||
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
|
||||
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
|
||||
import { AccountsPageComponent } from './accounts-page.component';
|
||||
import { GfCreateOrUpdateAccountDialogModule } from './create-or-update-account-dialog/create-or-update-account-dialog.module';
|
||||
import { GfTransferBalanceDialogModule } from './transfer-balance/transfer-balance-dialog.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AccountsPageComponent],
|
||||
imports: [
|
||||
AccountsPageRoutingModule,
|
||||
CommonModule,
|
||||
GfAccountDetailDialogModule,
|
||||
GfAccountsTableModule,
|
||||
GfCreateOrUpdateAccountDialogModule,
|
||||
GfTransferBalanceDialogModule,
|
||||
IonIcon,
|
||||
MatButtonModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AccountsPageModule {}
|
15
apps/client/src/app/pages/accounts/accounts-page.routes.ts
Normal file
15
apps/client/src/app/pages/accounts/accounts-page.routes.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { internalRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { GfAccountsPageComponent } from './accounts-page.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: GfAccountsPageComponent,
|
||||
path: '',
|
||||
title: internalRoutes.accounts.title
|
||||
}
|
||||
];
|
@@ -2,7 +2,10 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { validateObjectForForm } from '@ghostfolio/client/util/form.util';
|
||||
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
|
||||
import { CommonModule, NgClass } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -13,10 +16,20 @@ import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
ValidatorFn,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogModule,
|
||||
MatDialogRef
|
||||
} from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Platform } from '@prisma/client';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
import { map, startWith } from 'rxjs/operators';
|
||||
@@ -24,14 +37,26 @@ import { map, startWith } from 'rxjs/operators';
|
||||
import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'h-100' },
|
||||
selector: 'gf-create-or-update-account-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfCurrencySelectorComponent,
|
||||
GfEntityLogoComponent,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
NgClass,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
selector: 'gf-create-or-update-account-dialog',
|
||||
styleUrls: ['./create-or-update-account-dialog.scss'],
|
||||
templateUrl: 'create-or-update-account-dialog.html',
|
||||
standalone: false
|
||||
templateUrl: 'create-or-update-account-dialog.html'
|
||||
})
|
||||
export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
export class GfCreateOrUpdateAccountDialogComponent implements OnDestroy {
|
||||
public accountForm: FormGroup;
|
||||
public currencies: string[] = [];
|
||||
public filteredPlatforms: Observable<Platform[]>;
|
||||
@@ -42,7 +67,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||
public constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams,
|
||||
private dataService: DataService,
|
||||
public dialogRef: MatDialogRef<CreateOrUpdateAccountDialog>,
|
||||
public dialogRef: MatDialogRef<GfCreateOrUpdateAccountDialogComponent>,
|
||||
private formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
|
@@ -1,32 +0,0 @@
|
||||
import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
|
||||
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [CreateOrUpdateAccountDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
GfCurrencySelectorComponent,
|
||||
GfEntityLogoComponent,
|
||||
MatAutocompleteModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
ReactiveFormsModule
|
||||
]
|
||||
})
|
||||
export class GfCreateOrUpdateAccountDialogModule {}
|
@@ -1,4 +1,5 @@
|
||||
import { TransferBalanceDto } from '@ghostfolio/api/app/account/transfer-balance.dto';
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
@@ -10,24 +11,41 @@ import {
|
||||
AbstractControl,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
ValidationErrors,
|
||||
Validators
|
||||
} from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import {
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogModule,
|
||||
MatDialogRef
|
||||
} from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { Account } from '@prisma/client';
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
import { TransferBalanceDialogParams } from './interfaces/interfaces';
|
||||
|
||||
@Component({
|
||||
host: { class: 'h-100' },
|
||||
selector: 'gf-transfer-balance-dialog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'h-100' },
|
||||
imports: [
|
||||
GfEntityLogoComponent,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
selector: 'gf-transfer-balance-dialog',
|
||||
styleUrls: ['./transfer-balance-dialog.scss'],
|
||||
templateUrl: 'transfer-balance-dialog.html',
|
||||
standalone: false
|
||||
templateUrl: 'transfer-balance-dialog.html'
|
||||
})
|
||||
export class TransferBalanceDialog implements OnDestroy {
|
||||
export class GfTransferBalanceDialogComponent implements OnDestroy {
|
||||
public accounts: Account[] = [];
|
||||
public currency: string;
|
||||
public transferBalanceForm: FormGroup;
|
||||
@@ -36,7 +54,7 @@ export class TransferBalanceDialog implements OnDestroy {
|
||||
|
||||
public constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: TransferBalanceDialogParams,
|
||||
public dialogRef: MatDialogRef<TransferBalanceDialog>,
|
||||
public dialogRef: MatDialogRef<GfTransferBalanceDialogComponent>,
|
||||
private formBuilder: FormBuilder
|
||||
) {}
|
||||
|
||||
|
@@ -1,27 +0,0 @@
|
||||
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
|
||||
import { TransferBalanceDialog } from './transfer-balance-dialog.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [TransferBalanceDialog],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfEntityLogoComponent,
|
||||
MatButtonModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
ReactiveFormsModule
|
||||
]
|
||||
})
|
||||
export class GfTransferBalanceDialogModule {}
|
@@ -1,53 +0,0 @@
|
||||
import { AdminJobsComponent } from '@ghostfolio/client/components/admin-jobs/admin-jobs.component';
|
||||
import { AdminMarketDataComponent } from '@ghostfolio/client/components/admin-market-data/admin-market-data.component';
|
||||
import { AdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component';
|
||||
import { AdminSettingsComponent } from '@ghostfolio/client/components/admin-settings/admin-settings.component';
|
||||
import { AdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { internalRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { AdminPageComponent } from './admin-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: AdminOverviewComponent,
|
||||
title: internalRoutes.adminControl.title
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.subRoutes.jobs.path,
|
||||
component: AdminJobsComponent,
|
||||
title: internalRoutes.adminControl.subRoutes.jobs.title
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.subRoutes.marketData.path,
|
||||
component: AdminMarketDataComponent,
|
||||
title: internalRoutes.adminControl.subRoutes.marketData.title
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.subRoutes.settings.path,
|
||||
component: AdminSettingsComponent,
|
||||
title: internalRoutes.adminControl.subRoutes.settings.title
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.subRoutes.users.path,
|
||||
component: AdminUsersComponent,
|
||||
title: internalRoutes.adminControl.subRoutes.users.title
|
||||
}
|
||||
],
|
||||
component: AdminPageComponent,
|
||||
path: ''
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AdminPageRoutingModule {}
|
@@ -2,6 +2,9 @@ import { TabConfiguration } from '@ghostfolio/common/interfaces';
|
||||
import { internalRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { addIcons } from 'ionicons';
|
||||
import {
|
||||
flashOutline,
|
||||
@@ -15,10 +18,10 @@ import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
imports: [IonIcon, MatTabsModule, RouterModule],
|
||||
selector: 'gf-admin-page',
|
||||
styleUrls: ['./admin-page.scss'],
|
||||
templateUrl: './admin-page.html',
|
||||
standalone: false
|
||||
templateUrl: './admin-page.html'
|
||||
})
|
||||
export class AdminPageComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
|
@@ -1,33 +0,0 @@
|
||||
import { GfAdminJobsModule } from '@ghostfolio/client/components/admin-jobs/admin-jobs.module';
|
||||
import { GfAdminMarketDataModule } from '@ghostfolio/client/components/admin-market-data/admin-market-data.module';
|
||||
import { GfAdminOverviewModule } from '@ghostfolio/client/components/admin-overview/admin-overview.module';
|
||||
import { GfAdminSettingsModule } from '@ghostfolio/client/components/admin-settings/admin-settings.module';
|
||||
import { GfAdminUsersModule } from '@ghostfolio/client/components/admin-users/admin-users.module';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { AdminPageRoutingModule } from './admin-page-routing.module';
|
||||
import { AdminPageComponent } from './admin-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AdminPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
AdminPageRoutingModule,
|
||||
CommonModule,
|
||||
GfAdminJobsModule,
|
||||
GfAdminMarketDataModule,
|
||||
GfAdminOverviewModule,
|
||||
GfAdminSettingsModule,
|
||||
GfAdminUsersModule,
|
||||
IonIcon,
|
||||
MatTabsModule
|
||||
],
|
||||
providers: [CacheService],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class AdminPageModule {}
|
46
apps/client/src/app/pages/admin/admin-page.routes.ts
Normal file
46
apps/client/src/app/pages/admin/admin-page.routes.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { GfAdminJobsComponent } from '@ghostfolio/client/components/admin-jobs/admin-jobs.component';
|
||||
import { GfAdminMarketDataComponent } from '@ghostfolio/client/components/admin-market-data/admin-market-data.component';
|
||||
import { GfAdminOverviewComponent } from '@ghostfolio/client/components/admin-overview/admin-overview.component';
|
||||
import { GfAdminSettingsComponent } from '@ghostfolio/client/components/admin-settings/admin-settings.component';
|
||||
import { GfAdminUsersComponent } from '@ghostfolio/client/components/admin-users/admin-users.component';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { internalRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { AdminPageComponent } from './admin-page.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: GfAdminOverviewComponent,
|
||||
title: internalRoutes.adminControl.title
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.subRoutes.jobs.path,
|
||||
component: GfAdminJobsComponent,
|
||||
title: internalRoutes.adminControl.subRoutes.jobs.title
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.subRoutes.marketData.path,
|
||||
component: GfAdminMarketDataComponent,
|
||||
title: internalRoutes.adminControl.subRoutes.marketData.title
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.subRoutes.settings.path,
|
||||
component: GfAdminSettingsComponent,
|
||||
title: internalRoutes.adminControl.subRoutes.settings.title
|
||||
},
|
||||
{
|
||||
path: internalRoutes.adminControl.subRoutes.users.path,
|
||||
component: GfAdminUsersComponent,
|
||||
title: internalRoutes.adminControl.subRoutes.users.title
|
||||
}
|
||||
],
|
||||
component: AdminPageComponent,
|
||||
path: ''
|
||||
}
|
||||
];
|
@@ -1,43 +0,0 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { publicRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { FaqPageComponent } from './faq-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./overview/faq-overview-page.module').then(
|
||||
(m) => m.FaqOverviewPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: publicRoutes.faq.subRoutes.saas.path,
|
||||
loadChildren: () =>
|
||||
import('./saas/saas-page.module').then((m) => m.SaasPageModule)
|
||||
},
|
||||
{
|
||||
path: publicRoutes.faq.subRoutes.selfHosting.path,
|
||||
loadChildren: () =>
|
||||
import('./self-hosting/self-hosting-page.module').then(
|
||||
(m) => m.SelfHostingPageModule
|
||||
)
|
||||
}
|
||||
],
|
||||
component: FaqPageComponent,
|
||||
path: '',
|
||||
title: publicRoutes.faq.title
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class FaqPageRoutingModule {}
|
@@ -3,7 +3,15 @@ import { TabConfiguration } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { publicRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import {
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
Component,
|
||||
OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
import { addIcons } from 'ionicons';
|
||||
import { cloudyOutline, readerOutline, serverOutline } from 'ionicons/icons';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
@@ -11,12 +19,13 @@ import { Subject } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
host: { class: 'page has-tabs' },
|
||||
imports: [IonIcon, MatTabsModule, RouterModule],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
selector: 'gf-faq-page',
|
||||
styleUrls: ['./faq-page.scss'],
|
||||
templateUrl: './faq-page.html',
|
||||
standalone: false
|
||||
templateUrl: './faq-page.html'
|
||||
})
|
||||
export class FaqPageComponent implements OnDestroy, OnInit {
|
||||
export class GfFaqPageComponent implements OnDestroy, OnInit {
|
||||
public deviceType: string;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public tabs: TabConfiguration[] = [];
|
||||
|
@@ -1,21 +0,0 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatTabsModule } from '@angular/material/tabs';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { IonIcon } from '@ionic/angular/standalone';
|
||||
|
||||
import { FaqPageRoutingModule } from './faq-page-routing.module';
|
||||
import { FaqPageComponent } from './faq-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [FaqPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FaqPageRoutingModule,
|
||||
IonIcon,
|
||||
MatTabsModule,
|
||||
RouterModule
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class FaqPageModule {}
|
34
apps/client/src/app/pages/faq/faq-page.routes.ts
Normal file
34
apps/client/src/app/pages/faq/faq-page.routes.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
import { publicRoutes } from '@ghostfolio/common/routes/routes';
|
||||
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
import { GfFaqPageComponent } from './faq-page.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./overview/faq-overview-page.routes').then((m) => m.routes)
|
||||
},
|
||||
{
|
||||
path: publicRoutes.faq.subRoutes.saas.path,
|
||||
loadChildren: () =>
|
||||
import('./saas/saas-page.routes').then((m) => m.routes)
|
||||
},
|
||||
{
|
||||
path: publicRoutes.faq.subRoutes.selfHosting.path,
|
||||
loadChildren: () =>
|
||||
import('./self-hosting/self-hosting-page.routes').then(
|
||||
(m) => m.routes
|
||||
)
|
||||
}
|
||||
],
|
||||
component: GfFaqPageComponent,
|
||||
path: '',
|
||||
title: publicRoutes.faq.title
|
||||
}
|
||||
];
|
@@ -1,21 +0,0 @@
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { FaqOverviewPageComponent } from './faq-overview-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
canActivate: [AuthGuard],
|
||||
component: FaqOverviewPageComponent,
|
||||
path: '',
|
||||
title: $localize`Frequently Asked Questions (FAQ)`
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class FaqOverviewPageRoutingModule {}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user