update
Some checks failed
Docker image CD / build_and_push (push) Failing after 4m1s

This commit is contained in:
2025-07-21 18:27:29 -07:00
176 changed files with 9520 additions and 5139 deletions

View File

@@ -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 dialogs asset sub class selector of the admin control panel to update the options dynamically based on the selected asset class
- Improved the asset profile dialogs 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

View File

@@ -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
);

View File

@@ -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;

View File

@@ -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

View File

@@ -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;
})

View File

@@ -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
};
}

View File

@@ -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
};
}

View File

@@ -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
})
);
});

View File

@@ -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 activitys 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

View File

@@ -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;
}

View File

@@ -10,6 +10,7 @@ export interface TransactionPointSymbol {
firstBuyDate: string;
investment: Big;
quantity: Big;
skipErrors: boolean;
symbol: string;
tags?: Tag[];
transactionCount: number;

View File

@@ -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
)

View File

@@ -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()

View File

@@ -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

View File

@@ -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']
>;

View File

@@ -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: {

View File

@@ -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 {

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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;
}

View File

@@ -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: {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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: {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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) &&

View File

@@ -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;

View File

@@ -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 {}

View File

@@ -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
});

View File

@@ -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 {}

View File

@@ -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;

View File

@@ -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 {}

View File

@@ -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()

View File

@@ -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 {}

View File

@@ -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,

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -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;

View File

@@ -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])
},
{

View File

@@ -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 {}

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {}

View File

@@ -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()

View File

@@ -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 {}

View File

@@ -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]
});
}

View File

@@ -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>

View File

@@ -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 {}

View File

@@ -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 = [

View File

@@ -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 {}

View File

@@ -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,

View File

@@ -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 {}

View File

@@ -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({

View File

@@ -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 {}

View File

@@ -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]>();

View File

@@ -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 {}

View File

@@ -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,

View File

@@ -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>
}

View File

@@ -8,6 +8,7 @@ export interface HoldingDetailDialogParams {
dataSource: DataSource;
deviceType: string;
hasImpersonationId: boolean;
hasPermissionToAccessAdminControl: boolean;
hasPermissionToCreateOrder: boolean;
hasPermissionToReportDataGlitch: boolean;
hasPermissionToUpdateOrder: boolean;

View File

@@ -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 }
);
}

View File

@@ -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 {}

View File

@@ -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[];

View File

@@ -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"

View File

@@ -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 {}

View File

@@ -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;

View File

@@ -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"

View File

@@ -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 {}

View File

@@ -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;

View File

@@ -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 {}

View File

@@ -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"

View File

@@ -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 (

View File

@@ -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;

View File

@@ -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 {}

View File

@@ -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;

View File

@@ -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 {}

View File

@@ -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[] = [];

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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
},

View File

@@ -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 {}

View 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
}
];

View File

@@ -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
) {}

View File

@@ -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 {}

View File

@@ -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
) {}

View File

@@ -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 {}

View File

@@ -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 {}

View File

@@ -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;

View File

@@ -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 {}

View 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: ''
}
];

View File

@@ -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 {}

View File

@@ -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[] = [];

View File

@@ -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 {}

View 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
}
];

View File

@@ -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