Compare commits

..

12 Commits

Author SHA1 Message Date
40c95a541d Add data source to transaction model (#53) 2021-04-28 21:30:49 +02:00
2d04d7b8d5 Release 0.94.0 (#52) 2021-04-27 21:04:51 +02:00
94e0feac68 Add scraper symbols to symbol lookup results (#51) 2021-04-27 21:03:22 +02:00
cd9e974c40 Release 0.93.0 (#50) 2021-04-26 22:09:20 +02:00
11700f75d9 Improve colors in dark mode, improve user table (#49) 2021-04-26 21:54:19 +02:00
0439a7beaa Release 0.92.0 (#48) 2021-04-25 21:27:13 +02:00
608b195ba9 Extend user table (#47)
* Column: accounts
* Horizontal scrollbar
2021-04-25 21:25:23 +02:00
8cb5fd64dd Prepare for multi accounts support: store account for new transactions (#46) 2021-04-25 21:22:35 +02:00
ef317a86ed Setup angular compiler options (#45) 2021-04-25 17:41:53 +02:00
19ada83d0b Fix header and about page (#44) 2021-04-25 17:29:04 +02:00
6ecf66ea2a Fix types 2021-04-25 12:34:34 +02:00
65ba16c07c Fix tests 2021-04-25 12:30:28 +02:00
40 changed files with 440 additions and 204 deletions

View File

@ -5,6 +5,37 @@ 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).
## 0.95.0 - 28.04.2021
### Added
- Added a data source attribute to the transactions model
## 0.94.0 - 27.04.2021
### Added
- Added the generic scraper symbols to the symbol lookup results
## 0.93.0 - 26.04.2021
### Changed
- Improved the user table styling of the admin control panel
- Improved the background colors in the dark mode
## 0.92.0 - 25.04.2021
### Added
- Prepared further for multi accounts support: store account for new transactions
- Added a horizontal scrollbar to the user table of the admin control panel
### Fixed
- Fixed an issue in the header with outdated data
- Fixed an issue on the about page with outdated data
## 0.91.0 - 25.04.2021 ## 0.91.0 - 25.04.2021
### Added ### Added

View File

@ -97,7 +97,7 @@ export class AdminService {
}, },
select: { select: {
_count: { _count: {
select: { Order: true } select: { Account: true, Order: true }
}, },
alias: true, alias: true,
Analytics: { Analytics: {

View File

@ -36,7 +36,10 @@ export class ExperimentalService {
const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => { const ordersWithPlatform: OrderWithPlatform[] = aOrders.map((order) => {
return { return {
...order, ...order,
accountId: undefined,
accountUserId: undefined,
createdAt: new Date(), createdAt: new Date(),
dataSource: undefined,
date: parseISO(order.date), date: parseISO(order.date),
fee: 0, fee: 0,
id: undefined, id: undefined,

View File

@ -1,3 +1,6 @@
import { Settings, User } from '@prisma/client'; import { Account, Settings, User } from '@prisma/client';
export type UserWithSettings = User & { Settings: Settings }; export type UserWithSettings = User & {
Account: Account[];
Settings: Settings;
};

View File

@ -1,10 +1,16 @@
import { Currency, Type } from '@prisma/client'; import { Currency, DataSource, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator'; import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
export class CreateOrderDto { export class CreateOrderDto {
@IsString()
accountId: string;
@IsString() @IsString()
currency: Currency; currency: Currency;
@IsString()
dataSource: DataSource;
@IsISO8601() @IsISO8601()
date: string; date: string;

View File

@ -118,6 +118,9 @@ export class OrderController {
const date = parseISO(data.date); const date = parseISO(data.date);
const accountId = data.accountId;
delete data.accountId;
if (data.platformId) { if (data.platformId) {
const platformId = data.platformId; const platformId = data.platformId;
delete data.platformId; delete data.platformId;
@ -126,6 +129,11 @@ export class OrderController {
{ {
...data, ...data,
date, date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: { connect: { id: platformId } }, Platform: { connect: { id: platformId } },
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}, },
@ -138,6 +146,11 @@ export class OrderController {
{ {
...data, ...data,
date, date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}, },
this.request.user.id this.request.user.id
@ -169,6 +182,9 @@ export class OrderController {
const date = parseISO(data.date); const date = parseISO(data.date);
const accountId = data.accountId;
delete data.accountId;
if (data.platformId) { if (data.platformId) {
const platformId = data.platformId; const platformId = data.platformId;
delete data.platformId; delete data.platformId;
@ -178,6 +194,11 @@ export class OrderController {
data: { data: {
...data, ...data,
date, date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: { connect: { id: platformId } }, Platform: { connect: { id: platformId } },
User: { connect: { id: this.request.user.id } } User: { connect: { id: this.request.user.id } }
}, },
@ -199,6 +220,11 @@ export class OrderController {
data: { data: {
...data, ...data,
date, date,
Account: {
connect: {
id_userId: { id: accountId, userId: this.request.user.id }
}
},
Platform: originalOrder.platformId Platform: originalOrder.platformId
? { disconnect: true } ? { disconnect: true }
: undefined, : undefined,

View File

@ -1,10 +1,16 @@
import { Currency, Type } from '@prisma/client'; import { Currency, DataSource, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator'; import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator';
export class UpdateOrderDto { export class UpdateOrderDto {
@IsString()
accountId: string;
@IsString() @IsString()
currency: Currency; currency: Currency;
@IsString()
dataSource: DataSource;
@IsISO8601() @IsISO8601()
date: string; date: string;

View File

@ -1,6 +1,8 @@
import { Currency } from '@prisma/client';
export interface PortfolioPositionDetail { export interface PortfolioPositionDetail {
averagePrice: number; averagePrice: number;
currency: string; currency: Currency;
firstBuyDate: string; firstBuyDate: string;
grossPerformance: number; grossPerformance: number;
grossPerformancePercent: number; grossPerformancePercent: number;

View File

@ -1,6 +1,7 @@
import { Currency } from '@prisma/client'; import { Currency, DataSource } from '@prisma/client';
export interface SymbolItem { export interface SymbolItem {
currency: Currency; currency: Currency;
dataSource: DataSource;
marketPrice: number; marketPrice: number;
} }

View File

@ -1,4 +1,6 @@
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { convertFromYahooSymbol } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
@ -10,31 +12,51 @@ import { SymbolItem } from './interfaces/symbol-item.interface';
@Injectable() @Injectable()
export class SymbolService { export class SymbolService {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService private readonly dataProviderService: DataProviderService,
private readonly ghostfolioScraperApiService: GhostfolioScraperApiService
) {} ) {}
public async get(aSymbol: string): Promise<SymbolItem> { public async get(aSymbol: string): Promise<SymbolItem> {
const response = await this.dataProviderService.get([aSymbol]); const response = await this.dataProviderService.get([aSymbol]);
const { currency, marketPrice } = response[aSymbol]; const { currency, dataSource, marketPrice } = response[aSymbol];
return { return {
dataSource,
marketPrice, marketPrice,
currency: <Currency>(<unknown>currency) currency: <Currency>(<unknown>currency)
}; };
} }
public async lookup(aQuery: string): Promise<LookupItem[]> { public async lookup(aQuery = ''): Promise<LookupItem[]> {
const query = aQuery.toLowerCase();
const results: LookupItem[] = [];
if (!query) {
return results;
}
const get = bent( const get = bent(
`https://query1.finance.yahoo.com/v1/finance/search?q=${aQuery}&lang=en-US&region=US&quotesCount=8&newsCount=0&enableFuzzyQuery=false&quotesQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`, `https://query1.finance.yahoo.com/v1/finance/search?q=${query}&lang=en-US&region=US&quotesCount=8&newsCount=0&enableFuzzyQuery=false&quotesQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
'GET', 'GET',
'json', 'json',
200 200
); );
// Add custom symbols
const scraperConfigurations = await this.ghostfolioScraperApiService.getScraperConfigurations();
scraperConfigurations.forEach((scraperConfiguration) => {
if (scraperConfiguration.name.toLowerCase().startsWith(query)) {
results.push({
name: scraperConfiguration.name,
symbol: scraperConfiguration.symbol
});
}
});
try { try {
const { quotes } = await get(); const { quotes } = await get();
return quotes const searchResult = quotes
.filter(({ isYahooFinance }) => { .filter(({ isYahooFinance }) => {
return isYahooFinance; return isYahooFinance;
}) })
@ -59,6 +81,8 @@ export class SymbolService {
symbol: convertFromYahooSymbol(symbol) symbol: convertFromYahooSymbol(symbol)
}; };
}); });
return results.concat(searchResult);
} catch (error) { } catch (error) {
console.error(error); console.error(error);

View File

@ -1,9 +1,10 @@
import { Currency } from '@prisma/client'; import { Account, Currency } from '@prisma/client';
import { Access } from './access.interface'; import { Access } from './access.interface';
export interface User { export interface User {
access: Access[]; access: Access[];
accounts: Account[];
alias?: string; alias?: string;
id: string; id: string;
permissions: string[]; permissions: string[];

View File

@ -25,6 +25,7 @@ export class UserService {
) {} ) {}
public async getUser({ public async getUser({
Account,
alias, alias,
id, id,
role, role,
@ -53,6 +54,7 @@ export class UserService {
id: accessItem.id id: accessItem.id
}; };
}), }),
accounts: Account,
permissions: currentPermissions, permissions: currentPermissions,
settings: { settings: {
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY, baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
@ -69,7 +71,7 @@ export class UserService {
userWhereUniqueInput: Prisma.UserWhereUniqueInput userWhereUniqueInput: Prisma.UserWhereUniqueInput
): Promise<UserWithSettings | null> { ): Promise<UserWithSettings | null> {
const user = await this.prisma.user.findUnique({ const user = await this.prisma.user.findUnique({
include: { Settings: true }, include: { Account: true, Settings: true },
where: userWhereUniqueInput where: userWhereUniqueInput
}); });

View File

@ -14,6 +14,9 @@ import { PrismaService } from '../services/prisma.service';
import { RulesService } from '../services/rules.service'; import { RulesService } from '../services/rules.service';
import { Portfolio } from './portfolio'; import { Portfolio } from './portfolio';
const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269';
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
describe('Portfolio', () => { describe('Portfolio', () => {
let alphaVantageService: AlphaVantageService; let alphaVantageService: AlphaVantageService;
let configurationService: ConfigurationService; let configurationService: ConfigurationService;
@ -69,13 +72,13 @@ describe('Portfolio', () => {
accessToken: null, accessToken: null,
alias: 'Test', alias: 'Test',
createdAt: new Date(), createdAt: new Date(),
id: '', id: USER_ID,
provider: null, provider: null,
role: Role.USER, role: Role.USER,
Settings: { Settings: {
currency: Currency.CHF, currency: Currency.CHF,
updatedAt: new Date(), updatedAt: new Date(),
userId: '' userId: USER_ID
}, },
thirdPartyId: null, thirdPartyId: null,
updatedAt: new Date() updatedAt: new Date()
@ -126,6 +129,8 @@ describe('Portfolio', () => {
it('should return ["BTC"]', async () => { it('should return ["BTC"]', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 0, fee: 0,
@ -137,7 +142,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 49631.24, unitPrice: 49631.24,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);
@ -224,6 +229,8 @@ describe('Portfolio', () => {
it('should return ["ETHUSD"]', async () => { it('should return ["ETHUSD"]', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 0, fee: 0,
@ -235,7 +242,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 991.49, unitPrice: 991.49,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);
@ -276,7 +283,7 @@ describe('Portfolio', () => {
} }
}, },
quantity: 0.2, quantity: 0.2,
shareCurrent: 1, // shareCurrent: 1,
shareInvestment: 1, shareInvestment: 1,
symbol: 'ETHUSD', symbol: 'ETHUSD',
type: 'Cryptocurrency' type: 'Cryptocurrency'
@ -316,6 +323,8 @@ describe('Portfolio', () => {
it('should return ["ETHUSD"]', async () => { it('should return ["ETHUSD"]', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 0, fee: 0,
@ -327,9 +336,11 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 991.49, unitPrice: 991.49,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
}, },
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 0, fee: 0,
@ -341,7 +352,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 1050, unitPrice: 1050,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);
@ -388,6 +399,8 @@ describe('Portfolio', () => {
it('should return ["BTCUSD", "ETHUSD"]', async () => { it('should return ["BTCUSD", "ETHUSD"]', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.EUR, currency: Currency.EUR,
date: new Date(getUtc('2017-08-16')), date: new Date(getUtc('2017-08-16')),
@ -399,9 +412,11 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 3562.089535970158, unitPrice: 3562.089535970158,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
}, },
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 2.99, fee: 2.99,
@ -413,7 +428,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 991.49, unitPrice: 991.49,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);
@ -473,6 +488,8 @@ describe('Portfolio', () => {
it('should work with buy and sell', async () => { it('should work with buy and sell', async () => {
await portfolio.setOrders([ await portfolio.setOrders([
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 1.0, fee: 1.0,
@ -484,9 +501,11 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 991.49, unitPrice: 991.49,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
}, },
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 1.0, fee: 1.0,
@ -498,9 +517,11 @@ describe('Portfolio', () => {
type: Type.SELL, type: Type.SELL,
unitPrice: 1050, unitPrice: 1050,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
}, },
{ {
accountId: DEFAULT_ACCOUNT_ID,
accountUserId: USER_ID,
createdAt: null, createdAt: null,
currency: Currency.USD, currency: Currency.USD,
fee: 1.0, fee: 1.0,
@ -512,7 +533,7 @@ describe('Portfolio', () => {
type: Type.BUY, type: Type.BUY,
unitPrice: 1050, unitPrice: 1050,
updatedAt: null, updatedAt: null,
userId: null userId: USER_ID
} }
]); ]);

View File

@ -18,6 +18,7 @@ import {
import { ConfigurationService } from './configuration.service'; import { ConfigurationService } from './configuration.service';
import { DataProviderService } from './data-provider.service'; import { DataProviderService } from './data-provider.service';
import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { PrismaService } from './prisma.service'; import { PrismaService } from './prisma.service';
@Injectable() @Injectable()
@ -25,6 +26,7 @@ export class DataGatheringService {
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly ghostfolioScraperApi: GhostfolioScraperApiService,
private prisma: PrismaService private prisma: PrismaService
) {} ) {}
@ -183,6 +185,17 @@ export class DataGatheringService {
} }
} }
public async getCustomSymbolsToGather(startDate?: Date) {
const scraperConfigurations = await this.ghostfolioScraperApi.getScraperConfigurations();
return scraperConfigurations.map((scraperConfiguration) => {
return {
date: startDate,
symbol: scraperConfiguration.symbol
};
});
}
private getBenchmarksToGather(startDate: Date) { private getBenchmarksToGather(startDate: Date) {
const benchmarksToGather = benchmarks.map((symbol) => { const benchmarksToGather = benchmarks.map((symbol) => {
return { return {
@ -201,32 +214,6 @@ export class DataGatheringService {
return benchmarksToGather; return benchmarksToGather;
} }
private async getCustomSymbolsToGather(startDate: Date) {
const customSymbolsToGather = [];
if (this.configurationService.get('ENABLE_FEATURE_CUSTOM_SYMBOLS')) {
try {
const {
value: scraperConfigString
} = await this.prisma.property.findFirst({
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
JSON.parse(scraperConfigString).forEach((item) => {
customSymbolsToGather.push({
date: startDate,
symbol: item.symbol
});
});
} catch {}
}
return customSymbolsToGather;
}
private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> { private async getSymbols7D(): Promise<{ date: Date; symbol: string }[]> {
const startDate = subDays(resetHours(new Date()), 7); const startDate = subDays(resetHours(new Date()), 7);

View File

@ -1,3 +1,4 @@
import { DataSource } from '.prisma/client';
import { getYesterday } from '@ghostfolio/helper'; import { getYesterday } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as bent from 'bent'; import * as bent from 'bent';
@ -12,6 +13,7 @@ import {
MarketState MarketState
} from '../../interfaces/interfaces'; } from '../../interfaces/interfaces';
import { PrismaService } from '../../prisma.service'; import { PrismaService } from '../../prisma.service';
import { ScraperConfig } from './interfaces/scraper-config.interface';
@Injectable() @Injectable()
export class GhostfolioScraperApiService implements DataProviderInterface { export class GhostfolioScraperApiService implements DataProviderInterface {
@ -29,7 +31,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
try { try {
const symbol = aSymbols[0]; const symbol = aSymbols[0];
const scraperConfig = await this.getScraperConfig(symbol); const scraperConfig = await this.getScraperConfigurationBySymbol(symbol);
const { marketPrice } = await this.prisma.marketData.findFirst({ const { marketPrice } = await this.prisma.marketData.findFirst({
orderBy: { orderBy: {
@ -44,6 +46,7 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
[symbol]: { [symbol]: {
marketPrice, marketPrice,
currency: scraperConfig?.currency, currency: scraperConfig?.currency,
dataSource: DataSource.GHOSTFOLIO,
marketState: MarketState.delayed, marketState: MarketState.delayed,
name: scraperConfig?.name name: scraperConfig?.name
} }
@ -70,15 +73,17 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
try { try {
const symbol = aSymbols[0]; const symbol = aSymbols[0];
const scraperConfig = await this.getScraperConfig(symbol); const scraperConfiguration = await this.getScraperConfigurationBySymbol(
symbol
);
const get = bent(scraperConfig?.url, 'GET', 'string', 200, {}); const get = bent(scraperConfiguration?.url, 'GET', 'string', 200, {});
const html = await get(); const html = await get();
const $ = cheerio.load(html); const $ = cheerio.load(html);
const value = this.extractNumberFromString( const value = this.extractNumberFromString(
$(scraperConfig?.selector).text() $(scraperConfiguration?.selector).text()
); );
return { return {
@ -95,6 +100,23 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
return {}; return {};
} }
public async getScraperConfigurations(): Promise<ScraperConfig[]> {
try {
const {
value: scraperConfigString
} = await this.prisma.property.findFirst({
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
return JSON.parse(scraperConfigString);
} catch {}
return [];
}
private extractNumberFromString(aString: string): number { private extractNumberFromString(aString: string): number {
try { try {
const [numberString] = aString.match( const [numberString] = aString.match(
@ -106,22 +128,10 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
} }
} }
private async getScraperConfig(aSymbol: string) { private async getScraperConfigurationBySymbol(aSymbol: string) {
try { const scraperConfigurations = await this.getScraperConfigurations();
const { return scraperConfigurations.find((scraperConfiguration) => {
value: scraperConfigString return scraperConfiguration.symbol === aSymbol;
} = await this.prisma.property.findFirst({ });
select: {
value: true
},
where: { key: 'SCRAPER_CONFIG' }
});
return JSON.parse(scraperConfigString).find((item) => {
return item.symbol === aSymbol;
});
} catch {}
return {};
} }
} }

View File

@ -0,0 +1,9 @@
import { Currency } from '.prisma/client';
export interface ScraperConfig {
currency: Currency;
name: string;
selector: string;
symbol: string;
url: string;
}

View File

@ -1,3 +1,4 @@
import { DataSource } from '.prisma/client';
import { getToday, getYesterday } from '@ghostfolio/helper'; import { getToday, getYesterday } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import * as bent from 'bent'; import * as bent from 'bent';
@ -39,6 +40,7 @@ export class RakutenRapidApiService implements DataProviderInterface {
return { return {
'GF.FEAR_AND_GREED_INDEX': { 'GF.FEAR_AND_GREED_INDEX': {
currency: undefined, currency: undefined,
dataSource: DataSource.RAKUTEN,
marketPrice: fgi.now.value, marketPrice: fgi.now.value,
marketState: MarketState.open, marketState: MarketState.open,
name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME

View File

@ -1,3 +1,4 @@
import { DataSource } from '.prisma/client';
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper'; import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/helper';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { format } from 'date-fns'; import { format } from 'date-fns';
@ -49,6 +50,7 @@ export class YahooFinanceService implements DataProviderInterface {
response[symbol] = { response[symbol] = {
currency: parseCurrency(value.price?.currency), currency: parseCurrency(value.price?.currency),
dataSource: DataSource.YAHOO,
exchange: this.parseExchange(value.price?.exchangeName), exchange: this.parseExchange(value.price?.exchangeName),
marketState: marketState:
value.price?.marketState === 'REGULAR' || isCrypto(symbol) value.price?.marketState === 'REGULAR' || isCrypto(symbol)

View File

@ -1,4 +1,4 @@
import { Currency, Platform } from '@prisma/client'; import { Currency, DataSource, Platform } from '@prisma/client';
import { OrderType } from '../../models/order-type'; import { OrderType } from '../../models/order-type';
@ -51,6 +51,7 @@ export interface IDataProviderHistoricalResponse {
export interface IDataProviderResponse { export interface IDataProviderResponse {
currency: Currency; currency: Currency;
dataSource: DataSource;
exchange?: string; exchange?: string;
industry?: Industry; industry?: Industry;
marketChange?: number; marketChange?: number;

View File

@ -150,7 +150,7 @@
>Account</a >Account</a
> >
<a <a
*ngIf="hasPermissionForAdminControl" *ngIf="hasPermissionToAccessAdminControl"
class="d-block d-sm-none" class="d-block d-sm-none"
[routerLink]="['/admin']" [routerLink]="['/admin']"
i18n i18n

View File

@ -34,16 +34,16 @@
<div *ngIf="showDetails" class="row"> <div *ngIf="showDetails" class="row">
<div class="d-flex col justify-content-end"> <div class="d-flex col justify-content-end">
<gf-value <gf-value
colorizeSign="true" [colorizeSign]="true"
isCurrency="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance" [value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value> ></gf-value>
</div> </div>
<div class="col"> <div class="col">
<gf-value <gf-value
colorizeSign="true" [colorizeSign]="true"
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : performance?.currentNetPerformancePercent isLoading ? undefined : performance?.currentNetPerformancePercent

View File

@ -16,17 +16,17 @@
<div class="d-flex flex-column flex-wrap justify-content-end"> <div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value <gf-value
class="justify-content-end mb-2" class="justify-content-end mb-2"
colorizeSign="true"
position="end" position="end"
[colorizeSign]="true"
[currency]="baseCurrency" [currency]="baseCurrency"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentGrossPerformance" [value]="isLoading ? undefined : performance?.currentGrossPerformance"
></gf-value> ></gf-value>
<gf-value <gf-value
class="justify-content-end" class="justify-content-end"
colorizeSign="true"
isPercent="true"
position="end" position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : performance?.currentGrossPerformancePercent isLoading ? undefined : performance?.currentGrossPerformancePercent
@ -39,17 +39,17 @@
<div class="d-flex flex-column flex-wrap justify-content-end"> <div class="d-flex flex-column flex-wrap justify-content-end">
<gf-value <gf-value
class="justify-content-end mb-2" class="justify-content-end mb-2"
colorizeSign="true"
position="end" position="end"
[colorizeSign]="true"
[currency]="baseCurrency" [currency]="baseCurrency"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : performance?.currentNetPerformance" [value]="isLoading ? undefined : performance?.currentNetPerformance"
></gf-value> ></gf-value>
<gf-value <gf-value
class="justify-content-end" class="justify-content-end"
colorizeSign="true"
isPercent="true"
position="end" position="end"
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale" [locale]="locale"
[value]=" [value]="
isLoading ? undefined : performance?.currentNetPerformancePercent isLoading ? undefined : performance?.currentNetPerformancePercent

View File

@ -21,10 +21,10 @@
<div class="row"> <div class="row">
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
colorizeSign="true"
isPercent="true"
label="Performance" label="Performance"
size="medium" size="medium"
[colorizeSign]="true"
[isPercent]="true"
[locale]="data.locale" [locale]="data.locale"
[value]="grossPerformancePercent" [value]="grossPerformancePercent"
></gf-value> ></gf-value>
@ -76,9 +76,9 @@
</div> </div>
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value <gf-value
isCurrency="true"
label="Quantity" label="Quantity"
size="medium" size="medium"
[isCurrency]="true"
[value]="quantity" [value]="quantity"
></gf-value> ></gf-value>
</div> </div>

View File

@ -42,14 +42,14 @@
<div class="d-flex mt-1"> <div class="d-flex mt-1">
<gf-value <gf-value
class="mr-3" class="mr-3"
colorizeSign="true" [colorizeSign]="true"
[currency]="position?.currency" [currency]="position?.currency"
[locale]="locale" [locale]="locale"
[value]="position?.grossPerformance" [value]="position?.grossPerformance"
></gf-value> ></gf-value>
<gf-value <gf-value
colorizeSign="true" [colorizeSign]="true"
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="position?.grossPerformancePercent" [value]="position?.grossPerformancePercent"
></gf-value> ></gf-value>

View File

@ -28,8 +28,8 @@
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element"> <td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
colorizeSign="true" [colorizeSign]="true"
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.grossPerformancePercent" [value]="isLoading ? undefined : element.grossPerformancePercent"
></gf-value> ></gf-value>
@ -50,7 +50,7 @@
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.shareInvestment" [value]="isLoading ? undefined : element.shareInvestment"
></gf-value> ></gf-value>
@ -71,7 +71,7 @@
<td mat-cell *matCellDef="let element"> <td mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
isPercent="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.shareCurrent" [value]="isLoading ? undefined : element.shareCurrent"
></gf-value> ></gf-value>

View File

@ -116,7 +116,7 @@
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
isCurrency="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.quantity" [value]="isLoading ? undefined : element.quantity"
></gf-value> ></gf-value>
@ -137,7 +137,7 @@
<td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell> <td *matCellDef="let element" class="d-none d-lg-table-cell" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
isCurrency="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.unitPrice" [value]="isLoading ? undefined : element.unitPrice"
></gf-value> ></gf-value>
@ -158,7 +158,7 @@
<td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element"> <td class="d-none d-lg-table-cell" mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
isCurrency="true" [isCurrency]="true"
[locale]="locale" [locale]="locale"
[value]="isLoading ? undefined : element.fee" [value]="isLoading ? undefined : element.fee"
></gf-value> ></gf-value>

View File

@ -42,6 +42,8 @@ export class AboutPageComponent implements OnInit {
info.globalPermissions, info.globalPermissions,
permissions.enableSubscription permissions.enableSubscription
); );
this.cd.markForCheck();
}); });
this.isLoggedIn = !!this.tokenStorageService.getToken(); this.isLoggedIn = !!this.tokenStorageService.getToken();

View File

@ -70,42 +70,36 @@
<div class="col"> <div class="col">
<h3 class="mb-3 text-center" i18n>Users</h3> <h3 class="mb-3 text-center" i18n>Users</h3>
<mat-card class="px-0"> <mat-card class="px-0">
<mat-card-content> <mat-card-content class="users">
<table class="users w-100"> <table>
<thead> <thead>
<tr class="mat-header-row"> <tr class="mat-header-row">
<th class="mat-header-cell pl-2 py-2 text-truncate" i18n> <th class="mat-header-cell pl-3 py-2" i18n>User</th>
User <th class="mat-header-cell pr-2 py-2" i18n>
</th>
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
Registration Date Registration Date
</th> </th>
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n> <th class="mat-header-cell pr-2 py-2" i18n>Accounts</th>
Transactions <th class="mat-header-cell pr-2 py-2" i18n>Transactions</th>
</th> <th class="mat-header-cell pr-2 py-2" i18n>Engagement</th>
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n> <th class="mat-header-cell pr-3 py-2" i18n>Last Activitiy</th>
Engagement
</th>
<th class="mat-header-cell pr-2 py-2 text-truncate" i18n>
Last Activitiy
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let userItem of users" class="mat-row"> <tr *ngFor="let userItem of users" class="mat-row">
<td class="mat-cell pl-2 py-2 text-truncate"> <td class="mat-cell pl-3 py-2">
{{ userItem.alias || userItem.id }} {{ userItem.alias || userItem.id }}
</td> </td>
<td class="mat-cell pr-2 py-2 text-truncate"> <td class="mat-cell pr-2 py-2">
{{ userItem.createdAt | date: defaultDateFormat }} {{ userItem.createdAt | date: defaultDateFormat }}
</td> </td>
<td class="mat-cell pr-2 py-2 text-truncate"> <td class="mat-cell pr-2 py-2">
{{ userItem._count?.Order }} {{ userItem._count?.Account }}
</td> </td>
<td class="mat-cell pr-2 py-2 text-truncate"> <td class="mat-cell pr-2 py-2">{{ userItem._count?.Order }}</td>
<td class="mat-cell pr-2 py-2">
{{ userItem.Analytics?.activityCount }} {{ userItem.Analytics?.activityCount }}
</td> </td>
<td class="mat-cell pr-2 py-2 text-truncate"> <td class="mat-cell pr-3 py-2">
{{ formatDistanceToNow(userItem.Analytics?.updatedAt) }} {{ formatDistanceToNow(userItem.Analytics?.updatedAt) }}
</td> </td>
</tr> </tr>

View File

@ -2,16 +2,25 @@
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
display: block; display: block;
table { .mat-card-content {
&.users { &.users {
table-layout: fixed; overflow-x: auto;
tr { table {
&:nth-child(even) { min-width: 100%;
background-color: rgba(
var(--dark-primary-text), tr {
var(--palette-background-hover-alpha) &:nth-child(even) {
); background-color: rgba(
var(--dark-primary-text),
var(--palette-background-hover-alpha)
);
}
}
.mat-row,
.mat-header-row {
width: 100%;
} }
} }
} }

View File

@ -8,7 +8,7 @@ import { FormControl, Validators } from '@angular/forms';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { Currency, Order as OrderModel } from '@prisma/client'; import { Currency } from '@prisma/client';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { import {
debounceTime, debounceTime,
@ -19,6 +19,7 @@ import {
} from 'rxjs/operators'; } from 'rxjs/operators';
import { DataService } from '../../../services/data.service'; import { DataService } from '../../../services/data.service';
import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
@Component({ @Component({
host: { class: 'h-100' }, host: { class: 'h-100' },
@ -33,7 +34,7 @@ export class CreateOrUpdateTransactionDialog {
public isLoading = false; public isLoading = false;
public platforms: { id: string; name: string }[]; public platforms: { id: string; name: string }[];
public searchSymbolCtrl = new FormControl( public searchSymbolCtrl = new FormControl(
this.data.symbol, this.data.transaction.symbol,
Validators.required Validators.required
); );
@ -43,7 +44,7 @@ export class CreateOrUpdateTransactionDialog {
private cd: ChangeDetectorRef, private cd: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>, public dialogRef: MatDialogRef<CreateOrUpdateTransactionDialog>,
@Inject(MAT_DIALOG_DATA) public data: OrderModel @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateTransactionDialogParams
) {} ) {}
ngOnInit() { ngOnInit() {
@ -72,14 +73,15 @@ export class CreateOrUpdateTransactionDialog {
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) { public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
this.isLoading = true; this.isLoading = true;
this.data.symbol = event.option.value; this.data.transaction.symbol = event.option.value;
this.dataService this.dataService
.fetchSymbolItem(this.data.symbol) .fetchSymbolItem(this.data.transaction.symbol)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ currency, marketPrice }) => { .subscribe(({ currency, dataSource, marketPrice }) => {
this.data.currency = currency; this.data.transaction.currency = currency;
this.data.unitPrice = marketPrice; this.data.transaction.dataSource = dataSource;
this.data.transaction.unitPrice = marketPrice;
this.isLoading = false; this.isLoading = false;
@ -88,10 +90,11 @@ export class CreateOrUpdateTransactionDialog {
} }
public onUpdateSymbolByTyping(value: string) { public onUpdateSymbolByTyping(value: string) {
this.data.currency = null; this.data.transaction.currency = null;
this.data.unitPrice = null; this.data.transaction.dataSource = null;
this.data.transaction.unitPrice = null;
this.data.symbol = value; this.data.transaction.symbol = value;
} }
public ngOnDestroy() { public ngOnDestroy() {

View File

@ -1,6 +1,6 @@
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100"> <form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
<h1 *ngIf="data.id" mat-dialog-title i18n>Update transaction</h1> <h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
<h1 *ngIf="!data.id" mat-dialog-title i18n>Add transaction</h1> <h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
@ -25,7 +25,7 @@
class="autocomplete" class="autocomplete"
[value]="lookupItem.symbol" [value]="lookupItem.symbol"
> >
<span class="mr-2 symbol">{{ lookupItem.symbol }}</span <span class="mr-2 symbol">{{ lookupItem.symbol | gfSymbol }}</span
><span><b>{{ lookupItem.name }}</b></span> ><span><b>{{ lookupItem.name }}</b></span>
</mat-option> </mat-option>
</ng-container> </ng-container>
@ -36,7 +36,7 @@
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Type</mat-label> <mat-label i18n>Type</mat-label>
<mat-select name="type" required [(value)]="data.type"> <mat-select name="type" required [(value)]="data.transaction.type">
<mat-option value="BUY" i18n> BUY </mat-option> <mat-option value="BUY" i18n> BUY </mat-option>
<mat-option value="SELL" i18n> SELL </mat-option> <mat-option value="SELL" i18n> SELL </mat-option>
</mat-select> </mat-select>
@ -50,7 +50,7 @@
disabled disabled
name="currency" name="currency"
required required
[(value)]="data.currency" [(value)]="data.transaction.currency"
> >
<mat-option *ngFor="let currency of currencies" [value]="currency" <mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option >{{ currency }}</mat-option
@ -58,6 +58,18 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="d-none">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Data Source</mat-label>
<input
disabled
matInput
name="dataSource"
required
[(ngModel)]="data.transaction.dataSource"
/>
</mat-form-field>
</div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label> <mat-label i18n>Date</mat-label>
@ -67,7 +79,7 @@
name="date" name="date"
required required
[matDatepicker]="date" [matDatepicker]="date"
[(ngModel)]="data.date" [(ngModel)]="data.transaction.date"
/> />
<mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="date"></mat-datepicker-toggle>
<mat-datepicker #date disabled="false"></mat-datepicker> <mat-datepicker #date disabled="false"></mat-datepicker>
@ -81,7 +93,7 @@
name="fee" name="fee"
required required
type="number" type="number"
[(ngModel)]="data.fee" [(ngModel)]="data.transaction.fee"
/> />
</mat-form-field> </mat-form-field>
</div> </div>
@ -93,7 +105,7 @@
name="quantity" name="quantity"
required required
type="number" type="number"
[(ngModel)]="data.quantity" [(ngModel)]="data.transaction.quantity"
/> />
</mat-form-field> </mat-form-field>
</div> </div>
@ -105,14 +117,29 @@
name="unitPrice" name="unitPrice"
required required
type="number" type="number"
[(ngModel)]="data.unitPrice" [(ngModel)]="data.transaction.unitPrice"
/> />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="d-none">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account</mat-label>
<mat-select
disabled
name="accountId"
required
[(value)]="data.transaction.accountId"
>
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
>{{ account.name }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Platform</mat-label> <mat-label i18n>Platform</mat-label>
<mat-select name="platformId" [(value)]="data.platformId"> <mat-select name="platformId" [(value)]="data.transaction.platformId">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
<mat-option *ngFor="let platform of platforms" [value]="platform.id" <mat-option *ngFor="let platform of platforms" [value]="platform.id"
>{{ platform.name }}</mat-option >{{ platform.name }}</mat-option
@ -127,7 +154,7 @@
color="primary" color="primary"
i18n i18n
mat-flat-button mat-flat-button
[disabled]="!(addTransactionForm.form.valid && data.symbol)" [disabled]="!(addTransactionForm.form.valid && data.transaction.symbol)"
[mat-dialog-close]="data" [mat-dialog-close]="data"
> >
Save Save

View File

@ -9,6 +9,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component'; import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
@ -17,6 +18,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
exports: [], exports: [],
imports: [ imports: [
CommonModule, CommonModule,
GfSymbolModule,
FormsModule, FormsModule,
MatAutocompleteModule, MatAutocompleteModule,
MatButtonModule, MatButtonModule,

View File

@ -0,0 +1,9 @@
import { Account } from '@prisma/client';
import { Order } from '../../interfaces/order.interface';
export interface CreateOrUpdateTransactionDialogParams {
accountId: string;
accounts: Account[];
transaction: Order;
}

View File

@ -1,8 +1,14 @@
import { Currency, DataSource } from '.prisma/client';
export interface Order { export interface Order {
currency: string; accountId: string;
currency: Currency;
dataSource: DataSource;
date: Date; date: Date;
fee: number; fee: number;
id: string;
quantity: number; quantity: number;
platformId: string;
symbol: string; symbol: string;
type: string; type: string;
unitPrice: number; unitPrice: number;

View File

@ -122,37 +122,10 @@ export class TransactionsPageComponent implements OnInit {
}); });
} }
private openCreateTransactionDialog(): void {
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
data: {
currency: null,
date: new Date(),
fee: 0,
platformId: null,
quantity: null,
symbol: null,
type: 'BUY',
unitPrice: null
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef.afterClosed().subscribe((order: UpdateOrderDto) => {
if (order) {
this.dataService.postOrder(order).subscribe({
next: () => {
this.fetchOrders();
}
});
}
this.router.navigate(['.'], { relativeTo: this.route });
});
}
public openUpdateTransactionDialog({ public openUpdateTransactionDialog({
accountId,
currency, currency,
dataSource,
date, date,
fee, fee,
id, id,
@ -164,23 +137,30 @@ export class TransactionsPageComponent implements OnInit {
}: OrderModel): void { }: OrderModel): void {
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
data: { data: {
currency, accounts: this.user.accounts,
date, transaction: {
fee, accountId,
id, currency,
platformId, dataSource,
quantity, date,
symbol, fee,
type, id,
unitPrice platformId,
quantity,
symbol,
type,
unitPrice
}
}, },
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef.afterClosed().subscribe((order: UpdateOrderDto) => { dialogRef.afterClosed().subscribe((data: any) => {
if (order) { const transaction: UpdateOrderDto = data?.transaction;
this.dataService.putOrder(order).subscribe({
if (transaction) {
this.dataService.putOrder(transaction).subscribe({
next: () => { next: () => {
this.fetchOrders(); this.fetchOrders();
} }
@ -195,4 +175,41 @@ export class TransactionsPageComponent implements OnInit {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private openCreateTransactionDialog(): void {
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
data: {
accounts: this.user?.accounts,
transaction: {
accountId: this.user?.accounts.find((account) => {
return account.isDefault;
})?.id,
currency: null,
date: new Date(),
fee: 0,
platformId: null,
quantity: null,
symbol: null,
type: 'BUY',
unitPrice: null
}
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef.afterClosed().subscribe((data: any) => {
const transaction: UpdateOrderDto = data?.transaction;
if (transaction) {
this.dataService.postOrder(transaction).subscribe({
next: () => {
this.fetchOrders();
}
});
}
this.router.navigate(['.'], { relativeTo: this.route });
});
}
} }

View File

@ -57,11 +57,20 @@ body {
} }
} }
.mat-card:not([class*='mat-elevation-z']) { .mat-card {
border-color: rgba( background: var(--dark-background);
var(--light-primary-text),
var(--palette-foreground-divider-alpha) &:not([class*='mat-elevation-z']) {
); border-color: rgba(
var(--light-primary-text),
var(--palette-foreground-divider-alpha)
);
box-shadow: none;
}
}
.mat-dialog-container {
background: var(--dark-background);
} }
.mat-fab, .mat-fab,

View File

@ -12,5 +12,9 @@
{ {
"path": "./tsconfig.editor.json" "path": "./tsconfig.editor.json"
} }
] ],
"angularCompilerOptions": {
"strictInjectionParameters": true,
"strictTemplates": false
}
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "0.91.0", "version": "0.95.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {

View File

@ -59,22 +59,23 @@ model MarketData {
} }
model Order { model Order {
Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId]) Account Account? @relation(fields: [accountId, accountUserId], references: [id, userId])
accountId String? accountId String?
accountUserId String? accountUserId String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
currency Currency currency Currency
dataSource DataSource @default(YAHOO)
date DateTime date DateTime
fee Float fee Float
id String @default(uuid()) id String @default(uuid())
Platform Platform? @relation(fields: [platformId], references: [id]) Platform Platform? @relation(fields: [platformId], references: [id])
platformId String? platformId String?
quantity Float quantity Float
symbol String symbol String
type Type type Type
unitPrice Float unitPrice Float
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
User User @relation(fields: [userId], references: [id]) User User @relation(fields: [userId], references: [id])
userId String userId String
@@id([id, userId]) @@id([id, userId])
@ -128,6 +129,12 @@ enum Currency {
USD USD
} }
enum DataSource {
GHOSTFOLIO
RAKUTEN
YAHOO
}
enum Provider { enum Provider {
ANONYMOUS ANONYMOUS
GOOGLE GOOGLE

View File

@ -82,6 +82,16 @@ async function main() {
create: { create: {
accessToken: accessToken:
'c689bcc894e4a420cb609ee34271f3e07f200594f7d199c50d75add7102889eb60061a04cd2792ebc853c54e37308271271e7bf588657c9e0c37faacbc28c3c6', 'c689bcc894e4a420cb609ee34271f3e07f200594f7d199c50d75add7102889eb60061a04cd2792ebc853c54e37308271271e7bf588657c9e0c37faacbc28c3c6',
Account: {
create: [
{
accountType: AccountType.SECURITIES,
id: 'f4425b66-9ba9-4ac4-93d7-fdf9a145e8cb',
isDefault: true,
name: 'Default Account'
}
]
},
alias: 'Admin', alias: 'Admin',
id: '4e1af723-95f6-44f8-92a7-464df17f6ec3', id: '4e1af723-95f6-44f8-92a7-464df17f6ec3',
role: Role.ADMIN role: Role.ADMIN