Feature/support additional currencies (#517)
* Support additional currencies * Update changelog
This commit is contained in:
parent
9bc3505ded
commit
1beb4de62f
@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Supported the management of additional currencies in the admin control panel
|
||||||
|
|
||||||
## 1.86.0 - 04.12.2021
|
## 1.86.0 - 04.12.2021
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||||
|
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
||||||
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
|
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
@ -11,12 +14,14 @@ import {
|
|||||||
} from '@ghostfolio/common/permissions';
|
} from '@ghostfolio/common/permissions';
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
|
Body,
|
||||||
Controller,
|
Controller,
|
||||||
Get,
|
Get,
|
||||||
HttpException,
|
HttpException,
|
||||||
Inject,
|
Inject,
|
||||||
Param,
|
Param,
|
||||||
Post,
|
Post,
|
||||||
|
Put,
|
||||||
UseGuards
|
UseGuards
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
@ -31,6 +36,7 @@ export class AdminController {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private readonly adminService: AdminService,
|
private readonly adminService: AdminService,
|
||||||
private readonly dataGatheringService: DataGatheringService,
|
private readonly dataGatheringService: DataGatheringService,
|
||||||
|
private readonly propertyService: PropertyService,
|
||||||
@Inject(REQUEST) private readonly request: RequestWithUser
|
@Inject(REQUEST) private readonly request: RequestWithUser
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -153,4 +159,25 @@ export class AdminController {
|
|||||||
|
|
||||||
return this.adminService.getMarketDataBySymbol(symbol);
|
return this.adminService.getMarketDataBySymbol(symbol);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Put('settings/:key')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async updateProperty(
|
||||||
|
@Param('key') key: string,
|
||||||
|
@Body() data: PropertyDto
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
getPermissions(this.request.user.role),
|
||||||
|
permissions.accessAdminControl
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.adminService.putSetting(key, data.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-
|
|||||||
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
import { ExchangeRateDataModule } from '@ghostfolio/api/services/exchange-rate-data.module';
|
||||||
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
import { MarketDataModule } from '@ghostfolio/api/services/market-data.module';
|
||||||
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
|
import { PropertyModule } from '@ghostfolio/api/services/property/property.module';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from './admin.controller';
|
||||||
@ -18,6 +19,7 @@ import { AdminService } from './admin.service';
|
|||||||
ExchangeRateDataModule,
|
ExchangeRateDataModule,
|
||||||
MarketDataModule,
|
MarketDataModule,
|
||||||
PrismaModule,
|
PrismaModule,
|
||||||
|
PropertyModule,
|
||||||
SubscriptionModule
|
SubscriptionModule
|
||||||
],
|
],
|
||||||
controllers: [AdminController],
|
controllers: [AdminController],
|
||||||
|
@ -4,7 +4,8 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
|
|||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { baseCurrency } from '@ghostfolio/common/config';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
|
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
|
||||||
import {
|
import {
|
||||||
AdminData,
|
AdminData,
|
||||||
AdminMarketData,
|
AdminMarketData,
|
||||||
@ -21,6 +22,7 @@ export class AdminService {
|
|||||||
private readonly exchangeRateDataService: ExchangeRateDataService,
|
private readonly exchangeRateDataService: ExchangeRateDataService,
|
||||||
private readonly marketDataService: MarketDataService,
|
private readonly marketDataService: MarketDataService,
|
||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly propertyService: PropertyService,
|
||||||
private readonly subscriptionService: SubscriptionService
|
private readonly subscriptionService: SubscriptionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -45,6 +47,7 @@ export class AdminService {
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
lastDataGathering: await this.getLastDataGathering(),
|
lastDataGathering: await this.getLastDataGathering(),
|
||||||
|
settings: await this.propertyService.get(),
|
||||||
transactionCount: await this.prismaService.order.count(),
|
transactionCount: await this.prismaService.order.count(),
|
||||||
userCount: await this.prismaService.user.count(),
|
userCount: await this.prismaService.user.count(),
|
||||||
users: await this.getUsersWithAnalytics()
|
users: await this.getUsersWithAnalytics()
|
||||||
@ -76,6 +79,17 @@ export class AdminService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async putSetting(key: string, value: string) {
|
||||||
|
const response = await this.propertyService.put({ key, value });
|
||||||
|
|
||||||
|
if (key === PROPERTY_CURRENCIES) {
|
||||||
|
await this.exchangeRateDataService.initialize();
|
||||||
|
await this.dataGatheringService.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
private async getLastDataGathering() {
|
private async getLastDataGathering() {
|
||||||
const lastDataGathering =
|
const lastDataGathering =
|
||||||
await this.dataGatheringService.getLastDataGathering();
|
await this.dataGatheringService.getLastDataGathering();
|
||||||
|
@ -4,6 +4,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
|
|||||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { PROPERTY_STRIPE_CONFIG } from '@ghostfolio/common/config';
|
||||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||||
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
|
||||||
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
|
||||||
@ -222,7 +223,7 @@ export class InfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const stripeConfig = await this.prismaService.property.findUnique({
|
const stripeConfig = await this.prismaService.property.findUnique({
|
||||||
where: { key: 'STRIPE_CONFIG' }
|
where: { key: PROPERTY_STRIPE_CONFIG }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (stripeConfig) {
|
if (stripeConfig) {
|
||||||
|
@ -73,7 +73,7 @@ describe('CurrentRateService', () => {
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
dataProviderService = new DataProviderService(null, [], null);
|
dataProviderService = new DataProviderService(null, [], null);
|
||||||
exchangeRateDataService = new ExchangeRateDataService(null, null);
|
exchangeRateDataService = new ExchangeRateDataService(null, null, null);
|
||||||
marketDataService = new MarketDataService(null);
|
marketDataService = new MarketDataService(null);
|
||||||
|
|
||||||
await exchangeRateDataService.initialize();
|
await exchangeRateDataService.initialize();
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||||
import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config';
|
import {
|
||||||
|
PROPERTY_LAST_DATA_GATHERING,
|
||||||
|
PROPERTY_LOCKED_DATA_GATHERING,
|
||||||
|
ghostfolioFearAndGreedIndexSymbol
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
import { DataSource } from '@prisma/client';
|
import { DataSource } from '@prisma/client';
|
||||||
@ -43,7 +47,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
key: 'LOCKED_DATA_GATHERING',
|
key: PROPERTY_LOCKED_DATA_GATHERING,
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -55,11 +59,11 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.upsert({
|
await this.prismaService.property.upsert({
|
||||||
create: {
|
create: {
|
||||||
key: 'LAST_DATA_GATHERING',
|
key: PROPERTY_LAST_DATA_GATHERING,
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
},
|
},
|
||||||
update: { value: new Date().toISOString() },
|
update: { value: new Date().toISOString() },
|
||||||
where: { key: 'LAST_DATA_GATHERING' }
|
where: { key: PROPERTY_LAST_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error);
|
Logger.error(error);
|
||||||
@ -67,7 +71,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.delete({
|
await this.prismaService.property.delete({
|
||||||
where: {
|
where: {
|
||||||
key: 'LOCKED_DATA_GATHERING'
|
key: PROPERTY_LOCKED_DATA_GATHERING
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -78,7 +82,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
public async gatherMax() {
|
public async gatherMax() {
|
||||||
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
where: { key: PROPERTY_LOCKED_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isDataGatheringLocked) {
|
if (!isDataGatheringLocked) {
|
||||||
@ -87,7 +91,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
key: 'LOCKED_DATA_GATHERING',
|
key: PROPERTY_LOCKED_DATA_GATHERING,
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -99,11 +103,11 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.upsert({
|
await this.prismaService.property.upsert({
|
||||||
create: {
|
create: {
|
||||||
key: 'LAST_DATA_GATHERING',
|
key: PROPERTY_LAST_DATA_GATHERING,
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
},
|
},
|
||||||
update: { value: new Date().toISOString() },
|
update: { value: new Date().toISOString() },
|
||||||
where: { key: 'LAST_DATA_GATHERING' }
|
where: { key: PROPERTY_LAST_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error);
|
Logger.error(error);
|
||||||
@ -111,7 +115,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.delete({
|
await this.prismaService.property.delete({
|
||||||
where: {
|
where: {
|
||||||
key: 'LOCKED_DATA_GATHERING'
|
key: PROPERTY_LOCKED_DATA_GATHERING
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -128,7 +132,7 @@ export class DataGatheringService {
|
|||||||
symbol: string;
|
symbol: string;
|
||||||
}) {
|
}) {
|
||||||
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
where: { key: PROPERTY_LOCKED_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isDataGatheringLocked) {
|
if (!isDataGatheringLocked) {
|
||||||
@ -137,7 +141,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.create({
|
await this.prismaService.property.create({
|
||||||
data: {
|
data: {
|
||||||
key: 'LOCKED_DATA_GATHERING',
|
key: PROPERTY_LOCKED_DATA_GATHERING,
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -156,11 +160,11 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.upsert({
|
await this.prismaService.property.upsert({
|
||||||
create: {
|
create: {
|
||||||
key: 'LAST_DATA_GATHERING',
|
key: PROPERTY_LAST_DATA_GATHERING,
|
||||||
value: new Date().toISOString()
|
value: new Date().toISOString()
|
||||||
},
|
},
|
||||||
update: { value: new Date().toISOString() },
|
update: { value: new Date().toISOString() },
|
||||||
where: { key: 'LAST_DATA_GATHERING' }
|
where: { key: PROPERTY_LAST_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(error);
|
Logger.error(error);
|
||||||
@ -168,7 +172,7 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.delete({
|
await this.prismaService.property.delete({
|
||||||
where: {
|
where: {
|
||||||
key: 'LOCKED_DATA_GATHERING'
|
key: PROPERTY_LOCKED_DATA_GATHERING
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -351,13 +355,13 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
public async getIsInProgress() {
|
public async getIsInProgress() {
|
||||||
return await this.prismaService.property.findUnique({
|
return await this.prismaService.property.findUnique({
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
where: { key: PROPERTY_LOCKED_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLastDataGathering() {
|
public async getLastDataGathering() {
|
||||||
const lastDataGathering = await this.prismaService.property.findUnique({
|
const lastDataGathering = await this.prismaService.property.findUnique({
|
||||||
where: { key: 'LAST_DATA_GATHERING' }
|
where: { key: PROPERTY_LAST_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
|
|
||||||
if (lastDataGathering?.value) {
|
if (lastDataGathering?.value) {
|
||||||
@ -418,7 +422,10 @@ export class DataGatheringService {
|
|||||||
|
|
||||||
await this.prismaService.property.deleteMany({
|
await this.prismaService.property.deleteMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [{ key: 'LAST_DATA_GATHERING' }, { key: 'LOCKED_DATA_GATHERING' }]
|
OR: [
|
||||||
|
{ key: PROPERTY_LAST_DATA_GATHERING },
|
||||||
|
{ key: PROPERTY_LOCKED_DATA_GATHERING }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -496,7 +503,7 @@ export class DataGatheringService {
|
|||||||
const lastDataGathering = await this.getLastDataGathering();
|
const lastDataGathering = await this.getLastDataGathering();
|
||||||
|
|
||||||
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
const isDataGatheringLocked = await this.prismaService.property.findUnique({
|
||||||
where: { key: 'LOCKED_DATA_GATHERING' }
|
where: { key: PROPERTY_LOCKED_DATA_GATHERING }
|
||||||
});
|
});
|
||||||
|
|
||||||
const diffInHours = differenceInHours(new Date(), lastDataGathering);
|
const diffInHours = differenceInHours(new Date(), lastDataGathering);
|
||||||
|
@ -3,9 +3,10 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
import { PrismaModule } from './prisma.module';
|
import { PrismaModule } from './prisma.module';
|
||||||
|
import { PropertyModule } from './property/property.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DataProviderModule, PrismaModule],
|
imports: [DataProviderModule, PrismaModule, PropertyModule],
|
||||||
providers: [ExchangeRateDataService],
|
providers: [ExchangeRateDataService],
|
||||||
exports: [ExchangeRateDataService]
|
exports: [ExchangeRateDataService]
|
||||||
})
|
})
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { baseCurrency } from '@ghostfolio/common/config';
|
import { PROPERTY_CURRENCIES, baseCurrency } from '@ghostfolio/common/config';
|
||||||
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, getYesterday } from '@ghostfolio/common/helper';
|
||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
@ -7,6 +7,7 @@ import { isEmpty, isNumber, uniq } from 'lodash';
|
|||||||
import { DataProviderService } from './data-provider/data-provider.service';
|
import { DataProviderService } from './data-provider/data-provider.service';
|
||||||
import { IDataGatheringItem } from './interfaces/interfaces';
|
import { IDataGatheringItem } from './interfaces/interfaces';
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from './prisma.service';
|
||||||
|
import { PropertyService } from './property/property.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ExchangeRateDataService {
|
export class ExchangeRateDataService {
|
||||||
@ -16,7 +17,8 @@ export class ExchangeRateDataService {
|
|||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly dataProviderService: DataProviderService,
|
private readonly dataProviderService: DataProviderService,
|
||||||
private readonly prismaService: PrismaService
|
private readonly prismaService: PrismaService,
|
||||||
|
private readonly propertyService: PropertyService
|
||||||
) {
|
) {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
}
|
}
|
||||||
@ -149,7 +151,7 @@ export class ExchangeRateDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async prepareCurrencies(): Promise<string[]> {
|
private async prepareCurrencies(): Promise<string[]> {
|
||||||
const currencies: string[] = [];
|
let currencies: string[] = [];
|
||||||
|
|
||||||
(
|
(
|
||||||
await this.prismaService.account.findMany({
|
await this.prismaService.account.findMany({
|
||||||
@ -181,6 +183,14 @@ export class ExchangeRateDataService {
|
|||||||
currencies.push(symbolProfile.currency);
|
currencies.push(symbolProfile.currency);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const customCurrencies = (await this.propertyService.getByKey(
|
||||||
|
PROPERTY_CURRENCIES
|
||||||
|
)) as string[];
|
||||||
|
|
||||||
|
if (customCurrencies?.length > 0) {
|
||||||
|
currencies = currencies.concat(customCurrencies);
|
||||||
|
}
|
||||||
|
|
||||||
return uniq(currencies).sort();
|
return uniq(currencies).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
6
apps/api/src/services/property/property.dto.ts
Normal file
6
apps/api/src/services/property/property.dto.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class PropertyDto {
|
||||||
|
@IsString()
|
||||||
|
value: string;
|
||||||
|
}
|
11
apps/api/src/services/property/property.module.ts
Normal file
11
apps/api/src/services/property/property.module.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { PropertyService } from './property.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
exports: [PropertyService],
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [PropertyService]
|
||||||
|
})
|
||||||
|
export class PropertyModule {}
|
43
apps/api/src/services/property/property.service.ts
Normal file
43
apps/api/src/services/property/property.service.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
|
import { PROPERTY_CURRENCIES } from '@ghostfolio/common/config';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PropertyService {
|
||||||
|
public constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
|
public async get() {
|
||||||
|
const response: {
|
||||||
|
[key: string]: object | string | string[];
|
||||||
|
} = {
|
||||||
|
[PROPERTY_CURRENCIES]: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const properties = await this.prismaService.property.findMany();
|
||||||
|
|
||||||
|
for (const property of properties) {
|
||||||
|
let value = property.value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
value = JSON.parse(property.value);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
response[property.key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getByKey(aKey: string) {
|
||||||
|
const properties = await this.get();
|
||||||
|
return properties?.[aKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async put({ key, value }: { key: string; value: string }) {
|
||||||
|
return this.prismaService.property.upsert({
|
||||||
|
create: { key, value },
|
||||||
|
update: { value },
|
||||||
|
where: { key }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,10 @@ import { AdminService } from '@ghostfolio/client/services/admin.service';
|
|||||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import {
|
||||||
|
DEFAULT_DATE_FORMAT,
|
||||||
|
PROPERTY_CURRENCIES
|
||||||
|
} from '@ghostfolio/common/config';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import {
|
import {
|
||||||
differenceInSeconds,
|
differenceInSeconds,
|
||||||
@ -11,6 +14,7 @@ import {
|
|||||||
isValid,
|
isValid,
|
||||||
parseISO
|
parseISO
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
|
import { uniq } from 'lodash';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -20,6 +24,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './admin-overview.html'
|
templateUrl: './admin-overview.html'
|
||||||
})
|
})
|
||||||
export class AdminOverviewComponent implements OnDestroy, OnInit {
|
export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||||
|
public customCurrencies: string[];
|
||||||
public dataGatheringInProgress: boolean;
|
public dataGatheringInProgress: boolean;
|
||||||
public dataGatheringProgress: number;
|
public dataGatheringProgress: number;
|
||||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||||
@ -57,6 +62,26 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onAddCurrency() {
|
||||||
|
const currency = prompt('Please add a currency:');
|
||||||
|
|
||||||
|
if (currency) {
|
||||||
|
const currencies = uniq([...this.customCurrencies, currency]);
|
||||||
|
this.putCurrencies(currencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDeleteCurrency(aCurrency: string) {
|
||||||
|
const confirmation = confirm('Do you really want to delete this currency?');
|
||||||
|
|
||||||
|
if (confirmation) {
|
||||||
|
const currencies = this.customCurrencies.filter((currency) => {
|
||||||
|
return currency !== aCurrency;
|
||||||
|
});
|
||||||
|
this.putCurrencies(currencies);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onFlushCache() {
|
public onFlushCache() {
|
||||||
this.cacheService
|
this.cacheService
|
||||||
.flush()
|
.flush()
|
||||||
@ -121,9 +146,11 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
dataGatheringProgress,
|
dataGatheringProgress,
|
||||||
exchangeRates,
|
exchangeRates,
|
||||||
lastDataGathering,
|
lastDataGathering,
|
||||||
|
settings,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
userCount
|
userCount
|
||||||
}) => {
|
}) => {
|
||||||
|
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
|
||||||
this.dataGatheringProgress = dataGatheringProgress;
|
this.dataGatheringProgress = dataGatheringProgress;
|
||||||
this.exchangeRates = exchangeRates;
|
this.exchangeRates = exchangeRates;
|
||||||
|
|
||||||
@ -147,4 +174,17 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private putCurrencies(aCurrencies: string[]) {
|
||||||
|
this.dataService
|
||||||
|
.putAdminSetting(PROPERTY_CURRENCIES, {
|
||||||
|
value: JSON.stringify(aCurrencies)
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,75 @@
|
|||||||
<div class="col">
|
<div class="col">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div
|
<div class="d-flex my-3">
|
||||||
*ngIf="exchangeRates?.length > 0"
|
<div class="w-50" i18n>User Count</div>
|
||||||
class="align-items-start d-flex my-3"
|
<div class="w-50">{{ userCount }}</div>
|
||||||
>
|
</div>
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<div class="w-50" i18n>Transaction Count</div>
|
||||||
|
<div class="w-50">
|
||||||
|
<ng-container *ngIf="transactionCount">
|
||||||
|
{{ transactionCount }} ({{ transactionCount / userCount | number
|
||||||
|
: '1.2-2' }} <span i18n>per User</span>)
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex my-3">
|
||||||
|
<div class="w-50" i18n>Data Gathering</div>
|
||||||
|
<div class="w-50">
|
||||||
|
<div>
|
||||||
|
<ng-container *ngIf="lastDataGathering"
|
||||||
|
>{{ lastDataGathering }}</ng-container
|
||||||
|
>
|
||||||
|
<ng-container *ngIf="dataGatheringInProgress" i18n
|
||||||
|
>In Progress ({{ dataGatheringProgress | percent : '1.2-2'
|
||||||
|
}})</ng-container
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 overflow-hidden">
|
||||||
|
<div class="mb-2">
|
||||||
|
<button
|
||||||
|
color="accent"
|
||||||
|
mat-flat-button
|
||||||
|
(click)="onFlushCache()"
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1"
|
||||||
|
name="close-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span i18n>Reset Data Gathering</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<button
|
||||||
|
color="warn"
|
||||||
|
mat-flat-button
|
||||||
|
[disabled]="dataGatheringInProgress"
|
||||||
|
(click)="onGatherMax()"
|
||||||
|
>
|
||||||
|
<ion-icon class="mr-1" name="warning-outline"></ion-icon>
|
||||||
|
<span i18n>Gather All Data</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="mb-2 mr-2"
|
||||||
|
color="accent"
|
||||||
|
mat-flat-button
|
||||||
|
[disabled]="dataGatheringInProgress"
|
||||||
|
(click)="onGatherProfileData()"
|
||||||
|
>
|
||||||
|
<ion-icon
|
||||||
|
class="mr-1"
|
||||||
|
name="cloud-download-outline"
|
||||||
|
></ion-icon>
|
||||||
|
<span i18n>Gather Profile Data</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="align-items-start d-flex my-3">
|
||||||
<div class="w-50" i18n>Exchange Rates</div>
|
<div class="w-50" i18n>Exchange Rates</div>
|
||||||
<div class="w-50">
|
<div class="w-50">
|
||||||
<table>
|
<table>
|
||||||
@ -27,77 +92,30 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</td>
|
</td>
|
||||||
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
<td class="pl-1">{{ exchangeRate.label2 }}</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
*ngIf="customCurrencies.includes(exchangeRate.label2)"
|
||||||
|
class="mini-icon mx-1 no-min-width px-2"
|
||||||
|
mat-button
|
||||||
|
[disabled]="dataGatheringInProgress"
|
||||||
|
(click)="onDeleteCurrency(exchangeRate.label2)"
|
||||||
|
>
|
||||||
|
<ion-icon name="trash-outline"></ion-icon>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
<div class="mt-2">
|
||||||
</div>
|
<button
|
||||||
<div class="d-flex my-3">
|
color="primary"
|
||||||
<div class="w-50" i18n>Data Gathering</div>
|
mat-flat-button
|
||||||
<div class="w-50">
|
[disabled]="dataGatheringInProgress"
|
||||||
<div>
|
(click)="onAddCurrency()"
|
||||||
<ng-container *ngIf="lastDataGathering"
|
|
||||||
>{{ lastDataGathering }}</ng-container
|
|
||||||
>
|
|
||||||
<ng-container *ngIf="dataGatheringInProgress" i18n
|
|
||||||
>In Progress ({{ dataGatheringProgress | percent : '1.2-2'
|
|
||||||
}})</ng-container
|
|
||||||
>
|
>
|
||||||
|
<ion-icon class="mr-1" name="add-outline"></ion-icon>
|
||||||
|
<span i18n>Add Currency</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 overflow-hidden">
|
|
||||||
<div class="mb-2">
|
|
||||||
<button
|
|
||||||
class="mw-100"
|
|
||||||
color="accent"
|
|
||||||
mat-flat-button
|
|
||||||
(click)="onFlushCache()"
|
|
||||||
>
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1"
|
|
||||||
name="close-circle-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span i18n>Reset Data Gathering</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<button
|
|
||||||
class="mw-100"
|
|
||||||
color="warn"
|
|
||||||
mat-flat-button
|
|
||||||
[disabled]="dataGatheringInProgress"
|
|
||||||
(click)="onGatherMax()"
|
|
||||||
>
|
|
||||||
<ion-icon class="mr-1" name="warning-outline"></ion-icon>
|
|
||||||
<span i18n>Gather All Data</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="mb-2 mr-2 mw-100"
|
|
||||||
color="accent"
|
|
||||||
mat-flat-button
|
|
||||||
(click)="onGatherProfileData()"
|
|
||||||
>
|
|
||||||
<ion-icon
|
|
||||||
class="mr-1"
|
|
||||||
name="cloud-download-outline"
|
|
||||||
></ion-icon>
|
|
||||||
<span i18n>Gather Profile Data</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex my-3">
|
|
||||||
<div class="w-50" i18n>User Count</div>
|
|
||||||
<div class="w-50">{{ userCount }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex my-3">
|
|
||||||
<div class="w-50" i18n>Transaction Count</div>
|
|
||||||
<div class="w-50">
|
|
||||||
<ng-container *ngIf="transactionCount">
|
|
||||||
{{ transactionCount }} ({{ transactionCount / userCount | number
|
|
||||||
: '1.2-2' }} <span i18n>per User</span>)
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
|
@ -3,6 +3,12 @@
|
|||||||
:host {
|
:host {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
|
.mat-button {
|
||||||
|
&.mini-icon {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mat-flat-button {
|
.mat-flat-button {
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-button-wrapper {
|
.mat-button-wrapper {
|
||||||
|
@ -12,6 +12,7 @@ import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.in
|
|||||||
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
||||||
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
|
||||||
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
||||||
|
import { PropertyDto } from '@ghostfolio/api/services/property/property.dto';
|
||||||
import {
|
import {
|
||||||
Access,
|
Access,
|
||||||
Accounts,
|
Accounts,
|
||||||
@ -239,6 +240,11 @@ export class DataService {
|
|||||||
return this.http.put<UserItem>(`/api/account/${aAccount.id}`, aAccount);
|
return this.http.put<UserItem>(`/api/account/${aAccount.id}`, aAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public putAdminSetting(key: string, aData: PropertyDto) {
|
||||||
|
console.log(key, aData);
|
||||||
|
return this.http.put<void>(`/api/admin/settings/${key}`, aData);
|
||||||
|
}
|
||||||
|
|
||||||
public putOrder(aOrder: UpdateOrderDto) {
|
public putOrder(aOrder: UpdateOrderDto) {
|
||||||
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
|
return this.http.put<UserItem>(`/api/order/${aOrder.id}`, aOrder);
|
||||||
}
|
}
|
||||||
|
@ -30,4 +30,9 @@ export const warnColorRgb = {
|
|||||||
export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy';
|
export const DEFAULT_DATE_FORMAT = 'dd.MM.yyyy';
|
||||||
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
export const DEFAULT_DATE_FORMAT_MONTH_YEAR = 'MMM yyyy';
|
||||||
|
|
||||||
|
export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
||||||
|
export const PROPERTY_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING';
|
||||||
|
export const PROPERTY_LOCKED_DATA_GATHERING = 'LOCKED_DATA_GATHERING';
|
||||||
|
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';
|
||||||
|
|
||||||
export const UNKNOWN_KEY = 'UNKNOWN';
|
export const UNKNOWN_KEY = 'UNKNOWN';
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
|
import { Property } from '@prisma/client';
|
||||||
|
|
||||||
export interface AdminData {
|
export interface AdminData {
|
||||||
dataGatheringProgress?: number;
|
dataGatheringProgress?: number;
|
||||||
exchangeRates: { label1: string; label2: string; value: number }[];
|
exchangeRates: { label1: string; label2: string; value: number }[];
|
||||||
lastDataGathering?: Date | 'IN_PROGRESS';
|
lastDataGathering?: Date | 'IN_PROGRESS';
|
||||||
|
settings: { [key: string]: object | string | string[] };
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
userCount: number;
|
userCount: number;
|
||||||
users: {
|
users: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user