Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
a1460a98fd | |||
1a553a296f | |||
f5bd6b0d58 | |||
78a4946e8b | |||
702ee956a2 | |||
200a7d2d65 | |||
79edc09710 | |||
77255df4be | |||
277133fa1a | |||
abd0e08566 | |||
561d8dbc70 | |||
c973ffd3ba | |||
368de7dedc | |||
e56514629f | |||
7a8a25c4c0 | |||
5d36d3a6bb | |||
0ef35fd31f |
2
.env
2
.env
@ -11,6 +11,6 @@ POSTGRES_DB=ghostfolio-db
|
||||
|
||||
ACCESS_TOKEN_SALT=GHOSTFOLIO
|
||||
ALPHA_VANTAGE_API_KEY=
|
||||
DATABASE_URL=postgresql://user:password@localhost:5432/ghostfolio-db?sslmode=prefer
|
||||
DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=prefer
|
||||
JWT_SECRET_KEY=123456
|
||||
PORT=3333
|
||||
|
@ -7,3 +7,4 @@ before_script:
|
||||
- yarn
|
||||
script:
|
||||
- yarn format:check
|
||||
- yarn test
|
||||
|
33
CHANGELOG.md
33
CHANGELOG.md
@ -5,6 +5,39 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## 1.6.0 - 22.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added an index in the user table of the admin control panel
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the alignment in the user table of the admin control panel
|
||||
|
||||
## 1.5.0 - 22.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added _Zen Mode_: the distraction-free view
|
||||
|
||||
## 1.4.0 - 20.05.2021
|
||||
|
||||
### Added
|
||||
|
||||
- Added filtering by year in the transaction filtering component
|
||||
|
||||
### Changed
|
||||
|
||||
- Renamed _Ghostfolio Account_ to _My Ghostfolio_
|
||||
- Hid unknown exchange in the position overview
|
||||
- Disable the base currency selector for the demo user
|
||||
- Refactored the portfolio unit tests to work without database
|
||||
- Refactored the search functionality of the data management (aligned with data source)
|
||||
- Renamed shared helper to `@ghostfolio/common/helper`
|
||||
- Moved shared interfaces to `@ghostfolio/common/interfaces`
|
||||
- Moved shared types to `@ghostfolio/common/types`
|
||||
|
||||
## 1.3.0 - 15.05.2021
|
||||
|
||||
### Changed
|
||||
|
12
angular.json
12
angular.json
@ -208,22 +208,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"helper": {
|
||||
"root": "libs/helper",
|
||||
"sourceRoot": "libs/helper/src",
|
||||
"common": {
|
||||
"root": "libs/common",
|
||||
"sourceRoot": "libs/common/src",
|
||||
"projectType": "library",
|
||||
"architect": {
|
||||
"lint": {
|
||||
"builder": "@nrwl/linter:eslint",
|
||||
"options": {
|
||||
"lintFilePatterns": ["libs/helper/**/*.ts"]
|
||||
"lintFilePatterns": ["libs/common/**/*.ts"]
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@nrwl/jest:jest",
|
||||
"outputs": ["coverage/libs/helper"],
|
||||
"outputs": ["coverage/libs/common"],
|
||||
"options": {
|
||||
"jestConfig": "libs/helper/jest.config.js",
|
||||
"jestConfig": "libs/common/jest.config.js",
|
||||
"passWithNoTests": true
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { Controller, Get, Inject, UseGuards } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
import { AccessService } from './access.service';
|
||||
import { Access } from './interfaces/access.interface';
|
||||
|
||||
@Controller('access')
|
||||
export class AccessController {
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { AccessWithGranteeUser } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
import { AccessWithGranteeUser } from './interfaces/access-with-grantee-user.type';
|
||||
|
||||
@Injectable()
|
||||
export class AccessService {
|
||||
public constructor(private prisma: PrismaService) {}
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
getPermissions,
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@ -17,7 +21,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Account as AccountModel, Order } from '@prisma/client';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { AccountService } from './account.service';
|
||||
|
@ -1,6 +1,11 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import { AdminData } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
getPermissions,
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@ -14,7 +19,6 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { AdminService } from './admin.service';
|
||||
import { AdminData } from './interfaces/admin-data.interface';
|
||||
|
||||
@Controller('admin')
|
||||
export class AdminController {
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { AdminData } from '@ghostfolio/common/interfaces';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
import { AdminData } from './interfaces/admin-data.interface';
|
||||
|
||||
@Injectable()
|
||||
export class AdminService {
|
||||
public constructor(
|
||||
@ -109,7 +108,7 @@ export class AdminService {
|
||||
createdAt: true,
|
||||
id: true
|
||||
},
|
||||
take: 20,
|
||||
take: 30,
|
||||
where: {
|
||||
NOT: {
|
||||
Analytics: null
|
||||
|
4
apps/api/src/app/cache/cache.controller.ts
vendored
4
apps/api/src/app/cache/cache.controller.ts
vendored
@ -1,5 +1,5 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { Controller, Inject, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { Controller, Inject, Post, UseGuards } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import {
|
||||
baseCurrency,
|
||||
benchmarks,
|
||||
isApiTokenAuthorized
|
||||
} from '@ghostfolio/helper';
|
||||
import { baseCurrency, benchmarks } from '@ghostfolio/common/config';
|
||||
import { isApiTokenAuthorized } from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -3,11 +3,11 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider.serv
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Currency, Type } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
|
||||
import { OrderWithAccount } from '../order/interfaces/order-with-account.type';
|
||||
import { CreateOrderDto } from './create-order.dto';
|
||||
import { Data } from './interfaces/data.interface';
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
|
||||
import { InfoService } from './info.service';
|
||||
import { InfoItem } from './interfaces/info-item.interface';
|
||||
|
||||
@Controller('info')
|
||||
export class InfoController {
|
||||
|
@ -1,12 +1,11 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { permissions } from '@ghostfolio/helper';
|
||||
import { InfoItem } from '@ghostfolio/common/interfaces';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
import { InfoItem } from './interfaces/info-item.interface';
|
||||
|
||||
@Injectable()
|
||||
export class InfoService {
|
||||
private static DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
||||
|
@ -1,7 +1,11 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { nullifyValuesInObjects } from '@ghostfolio/api/helper/object.helper';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
getPermissions,
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Order, Prisma } from '@prisma/client';
|
||||
|
||||
import { CacheService } from '../cache/cache.service';
|
||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||
import { OrderWithAccount } from './interfaces/order-with-account.type';
|
||||
|
||||
@Injectable()
|
||||
export class OrderService {
|
||||
|
@ -4,7 +4,19 @@ import {
|
||||
} from '@ghostfolio/api/helper/object.helper';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
PortfolioItem,
|
||||
PortfolioOverview,
|
||||
PortfolioPerformance,
|
||||
PortfolioPosition,
|
||||
PortfolioReport
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
getPermissions,
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@ -21,16 +33,10 @@ import { AuthGuard } from '@nestjs/passport';
|
||||
import { Response } from 'express';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { RequestWithUser } from '../interfaces/request-with-user.type';
|
||||
import { PortfolioItem } from './interfaces/portfolio-item.interface';
|
||||
import { PortfolioOverview } from './interfaces/portfolio-overview.interface';
|
||||
import { PortfolioPerformance } from './interfaces/portfolio-performance.interface';
|
||||
import {
|
||||
HistoricalDataItem,
|
||||
PortfolioPositionDetail
|
||||
} from './interfaces/portfolio-position-detail.interface';
|
||||
import { PortfolioPosition } from './interfaces/portfolio-position.interface';
|
||||
import { PortfolioReport } from './interfaces/portfolio-report.interface';
|
||||
import { PortfolioService } from './portfolio.service';
|
||||
|
||||
@Controller('portfolio')
|
||||
|
@ -1,10 +1,14 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { Portfolio } from '@ghostfolio/api/models/portfolio';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
|
||||
import { IOrder } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { RulesService } from '@ghostfolio/api/services/rules.service';
|
||||
import {
|
||||
PortfolioItem,
|
||||
PortfolioOverview
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { REQUEST } from '@nestjs/core';
|
||||
import {
|
||||
@ -26,9 +30,6 @@ import * as roundTo from 'round-to';
|
||||
import { OrderService } from '../order/order.service';
|
||||
import { RedisCacheService } from '../redis-cache/redis-cache.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { DateRange } from './interfaces/date-range.type';
|
||||
import { PortfolioItem } from './interfaces/portfolio-item.interface';
|
||||
import { PortfolioOverview } from './interfaces/portfolio-overview.interface';
|
||||
import {
|
||||
HistoricalDataItem,
|
||||
PortfolioPositionDetail
|
||||
|
@ -1,4 +1,7 @@
|
||||
import { DataSource } from '@prisma/client';
|
||||
|
||||
export interface LookupItem {
|
||||
dataSource: DataSource;
|
||||
name: string;
|
||||
symbol: string;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@ -28,9 +28,12 @@ export class SymbolController {
|
||||
*/
|
||||
@Get('lookup')
|
||||
@UseGuards(AuthGuard('jwt'))
|
||||
public async lookupSymbol(@Query() { query }): Promise<LookupItem[]> {
|
||||
public async lookupSymbol(
|
||||
@Query() { query = '' }
|
||||
): Promise<{ items: LookupItem[] }> {
|
||||
try {
|
||||
return this.symbolService.lookup(query);
|
||||
const encodedQuery = encodeURIComponent(query.toLowerCase());
|
||||
return this.symbolService.lookup(encodedQuery);
|
||||
} catch {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),
|
||||
|
@ -1,10 +1,8 @@
|
||||
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.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 { Injectable } from '@nestjs/common';
|
||||
import { Currency } from '@prisma/client';
|
||||
import * as bent from 'bent';
|
||||
import { Currency, DataSource } from '@prisma/client';
|
||||
|
||||
import { LookupItem } from './interfaces/lookup-item.interface';
|
||||
import { SymbolItem } from './interfaces/symbol-item.interface';
|
||||
@ -27,62 +25,30 @@ export class SymbolService {
|
||||
};
|
||||
}
|
||||
|
||||
public async lookup(aQuery = ''): Promise<LookupItem[]> {
|
||||
const query = aQuery.toLowerCase();
|
||||
const results: LookupItem[] = [];
|
||||
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> {
|
||||
const results: { items: LookupItem[] } = { items: [] };
|
||||
|
||||
if (!query) {
|
||||
if (!aQuery) {
|
||||
return results;
|
||||
}
|
||||
|
||||
const get = bent(
|
||||
`https://query1.finance.yahoo.com/v1/finance/search?q=${query}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||
'GET',
|
||||
'json',
|
||||
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 {
|
||||
const { quotes } = await get();
|
||||
const { items } = await this.dataProviderService.search(aQuery);
|
||||
results.items = items;
|
||||
|
||||
const searchResult = quotes
|
||||
.filter(({ isYahooFinance }) => {
|
||||
return isYahooFinance;
|
||||
})
|
||||
.filter(({ quoteType }) => {
|
||||
return (
|
||||
quoteType === 'CRYPTOCURRENCY' ||
|
||||
quoteType === 'EQUITY' ||
|
||||
quoteType === 'ETF'
|
||||
);
|
||||
})
|
||||
.filter(({ quoteType, symbol }) => {
|
||||
if (quoteType === 'CRYPTOCURRENCY') {
|
||||
// Only allow cryptocurrencies in USD
|
||||
return symbol.includes('USD');
|
||||
}
|
||||
// Add custom symbols
|
||||
const scraperConfigurations = await this.ghostfolioScraperApiService.getScraperConfigurations();
|
||||
scraperConfigurations.forEach((scraperConfiguration) => {
|
||||
if (scraperConfiguration.name.toLowerCase().startsWith(aQuery)) {
|
||||
results.items.push({
|
||||
dataSource: DataSource.GHOSTFOLIO,
|
||||
name: scraperConfiguration.name,
|
||||
symbol: scraperConfiguration.symbol
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(({ longname, shortname, symbol }) => {
|
||||
return {
|
||||
name: longname || shortname,
|
||||
symbol: convertFromYahooSymbol(symbol)
|
||||
};
|
||||
});
|
||||
|
||||
return results.concat(searchResult);
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Currency, ViewMode } from '@prisma/client';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class UpdateUserSettingsDto {
|
||||
@IsString()
|
||||
currency: Currency;
|
||||
baseCurrency: Currency;
|
||||
|
||||
@IsString()
|
||||
viewMode: ViewMode;
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { RequestWithUser } from '@ghostfolio/api/app/interfaces/request-with-user.type';
|
||||
import { getPermissions, hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
getPermissions,
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/common/permissions';
|
||||
import { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
@ -20,7 +25,6 @@ import { User as UserModel } from '@prisma/client';
|
||||
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
import { UserItem } from './interfaces/user-item.interface';
|
||||
import { User } from './interfaces/user.interface';
|
||||
import { UpdateUserSettingsDto } from './update-user-settings.dto';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@ -89,8 +93,9 @@ export class UserController {
|
||||
}
|
||||
|
||||
return await this.userService.updateUserSettings({
|
||||
currency: data.currency,
|
||||
userId: this.request.user.id
|
||||
currency: data.baseCurrency,
|
||||
userId: this.request.user.id,
|
||||
viewMode: data.viewMode
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,13 @@
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import {
|
||||
getPermissions,
|
||||
locale,
|
||||
permissions,
|
||||
resetHours
|
||||
} from '@ghostfolio/helper';
|
||||
import { locale } from '@ghostfolio/common/config';
|
||||
import { resetHours } from '@ghostfolio/common/helper';
|
||||
import { User as IUser, UserWithSettings } from '@ghostfolio/common/interfaces';
|
||||
import { getPermissions, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Currency, Prisma, Provider, User } from '@prisma/client';
|
||||
import { Currency, Prisma, Provider, User, ViewMode } from '@prisma/client';
|
||||
import { add } from 'date-fns';
|
||||
|
||||
import { UserWithSettings } from '../interfaces/user-with-settings';
|
||||
import { User as IUser } from './interfaces/user.interface';
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
@Injectable()
|
||||
@ -57,8 +52,9 @@ export class UserService {
|
||||
accounts: Account,
|
||||
permissions: currentPermissions,
|
||||
settings: {
|
||||
baseCurrency: Settings?.currency || UserService.DEFAULT_CURRENCY,
|
||||
locale
|
||||
locale,
|
||||
baseCurrency: Settings?.currency ?? UserService.DEFAULT_CURRENCY,
|
||||
viewMode: Settings.viewMode ?? ViewMode.DEFAULT
|
||||
},
|
||||
subscription: {
|
||||
expiresAt: resetHours(add(new Date(), { days: 7 })),
|
||||
@ -85,7 +81,8 @@ export class UserService {
|
||||
user.Settings = {
|
||||
currency: UserService.DEFAULT_CURRENCY,
|
||||
updatedAt: new Date(),
|
||||
userId: user?.id
|
||||
userId: user?.id,
|
||||
viewMode: ViewMode.DEFAULT
|
||||
};
|
||||
}
|
||||
|
||||
@ -192,10 +189,12 @@ export class UserService {
|
||||
|
||||
public async updateUserSettings({
|
||||
currency,
|
||||
userId
|
||||
userId,
|
||||
viewMode
|
||||
}: {
|
||||
currency: Currency;
|
||||
currency?: Currency;
|
||||
userId: string;
|
||||
viewMode?: ViewMode;
|
||||
}) {
|
||||
await this.prisma.settings.upsert({
|
||||
create: {
|
||||
@ -204,10 +203,12 @@ export class UserService {
|
||||
connect: {
|
||||
id: userId
|
||||
}
|
||||
}
|
||||
},
|
||||
viewMode
|
||||
},
|
||||
update: {
|
||||
currency
|
||||
currency,
|
||||
viewMode
|
||||
},
|
||||
where: {
|
||||
userId: userId
|
||||
|
@ -1,7 +1,4 @@
|
||||
import {
|
||||
PortfolioItem,
|
||||
Position
|
||||
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { PortfolioItem, Position } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Order } from '../order';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { EvaluationResult } from './evaluation-result.interface';
|
||||
|
||||
|
@ -1,70 +1,102 @@
|
||||
import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config';
|
||||
import { getUtc, getYesterday } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
UNKNOWN_KEY,
|
||||
baseCurrency,
|
||||
getUtc,
|
||||
getYesterday
|
||||
} from '@ghostfolio/helper';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { AccountType, Currency, DataSource, Role, Type } from '@prisma/client';
|
||||
AccountType,
|
||||
Currency,
|
||||
DataSource,
|
||||
Role,
|
||||
Type,
|
||||
ViewMode
|
||||
} from '@prisma/client';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { ConfigurationService } from '../services/configuration.service';
|
||||
import { DataProviderService } from '../services/data-provider.service';
|
||||
import { AlphaVantageService } from '../services/data-provider/alpha-vantage/alpha-vantage.service';
|
||||
import { GhostfolioScraperApiService } from '../services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
|
||||
import { RakutenRapidApiService } from '../services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||
import { YahooFinanceService } from '../services/data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
||||
import { MarketState } from '../services/interfaces/interfaces';
|
||||
import { PrismaService } from '../services/prisma.service';
|
||||
import { RulesService } from '../services/rules.service';
|
||||
import { Portfolio } from './portfolio';
|
||||
|
||||
jest.mock('../services/data-provider.service', () => {
|
||||
return {
|
||||
DataProviderService: jest.fn().mockImplementation(() => {
|
||||
const today = format(new Date(), 'yyyy-MM-dd');
|
||||
const yesterday = format(getYesterday(), 'yyyy-MM-dd');
|
||||
|
||||
return {
|
||||
get: () => {
|
||||
return Promise.resolve({
|
||||
BTCUSD: {
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
exchange: UNKNOWN_KEY,
|
||||
marketPrice: 57973.008,
|
||||
marketState: MarketState.open,
|
||||
name: 'Bitcoin USD',
|
||||
type: 'Cryptocurrency'
|
||||
},
|
||||
ETHUSD: {
|
||||
currency: Currency.USD,
|
||||
dataSource: DataSource.YAHOO,
|
||||
exchange: UNKNOWN_KEY,
|
||||
marketPrice: 3915.337,
|
||||
marketState: MarketState.open,
|
||||
name: 'Ethereum USD',
|
||||
type: 'Cryptocurrency'
|
||||
}
|
||||
});
|
||||
},
|
||||
getHistorical: () => {
|
||||
return Promise.resolve({
|
||||
BTCUSD: {
|
||||
[yesterday]: 56710.122,
|
||||
[today]: 57973.008
|
||||
},
|
||||
ETHUSD: {
|
||||
[yesterday]: 3641.984,
|
||||
[today]: 3915.337
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../services/exchange-rate-data.service', () => {
|
||||
return {
|
||||
ExchangeRateDataService: jest.fn().mockImplementation(() => {
|
||||
return {
|
||||
initialize: () => Promise.resolve(),
|
||||
toCurrency: (value: number) => value
|
||||
};
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../services/data-provider.service');
|
||||
jest.mock('../services/exchange-rate-data.service');
|
||||
jest.mock('../services/rules.service');
|
||||
|
||||
const DEFAULT_ACCOUNT_ID = '693a834b-eb89-42c9-ae47-35196c25d269';
|
||||
const USER_ID = 'ca6ce867-5d31-495a-bce9-5942bbca9237';
|
||||
|
||||
describe('Portfolio', () => {
|
||||
let alphaVantageService: AlphaVantageService;
|
||||
let configurationService: ConfigurationService;
|
||||
let dataProviderService: DataProviderService;
|
||||
let exchangeRateDataService: ExchangeRateDataService;
|
||||
let ghostfolioScraperApiService: GhostfolioScraperApiService;
|
||||
let portfolio: Portfolio;
|
||||
let prismaService: PrismaService;
|
||||
let rakutenRapidApiService: RakutenRapidApiService;
|
||||
let rulesService: RulesService;
|
||||
let yahooFinanceService: YahooFinanceService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [
|
||||
AlphaVantageService,
|
||||
ConfigurationService,
|
||||
DataProviderService,
|
||||
ExchangeRateDataService,
|
||||
GhostfolioScraperApiService,
|
||||
PrismaService,
|
||||
RakutenRapidApiService,
|
||||
RulesService,
|
||||
YahooFinanceService
|
||||
]
|
||||
}).compile();
|
||||
|
||||
alphaVantageService = app.get<AlphaVantageService>(AlphaVantageService);
|
||||
configurationService = app.get<ConfigurationService>(ConfigurationService);
|
||||
dataProviderService = app.get<DataProviderService>(DataProviderService);
|
||||
exchangeRateDataService = app.get<ExchangeRateDataService>(
|
||||
ExchangeRateDataService
|
||||
dataProviderService = new DataProviderService(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
ghostfolioScraperApiService = app.get<GhostfolioScraperApiService>(
|
||||
GhostfolioScraperApiService
|
||||
);
|
||||
prismaService = app.get<PrismaService>(PrismaService);
|
||||
rakutenRapidApiService = app.get<RakutenRapidApiService>(
|
||||
RakutenRapidApiService
|
||||
);
|
||||
rulesService = app.get<RulesService>(RulesService);
|
||||
yahooFinanceService = app.get<YahooFinanceService>(YahooFinanceService);
|
||||
exchangeRateDataService = new ExchangeRateDataService(null);
|
||||
rulesService = new RulesService();
|
||||
|
||||
await exchangeRateDataService.initialize();
|
||||
|
||||
@ -95,7 +127,8 @@ describe('Portfolio', () => {
|
||||
Settings: {
|
||||
currency: Currency.CHF,
|
||||
updatedAt: new Date(),
|
||||
userId: USER_ID
|
||||
userId: USER_ID,
|
||||
viewMode: ViewMode.DEFAULT
|
||||
},
|
||||
thirdPartyId: null,
|
||||
updatedAt: new Date()
|
||||
@ -188,7 +221,7 @@ describe('Portfolio', () => {
|
||||
)
|
||||
}
|
||||
},
|
||||
// allocationCurrent: 0.9999999559148652,
|
||||
allocationCurrent: 1,
|
||||
allocationInvestment: 1,
|
||||
currency: Currency.USD,
|
||||
exchange: UNKNOWN_KEY,
|
||||
@ -199,7 +232,7 @@ describe('Portfolio', () => {
|
||||
Currency.USD,
|
||||
baseCurrency
|
||||
),
|
||||
// marketPrice: 57973.008,
|
||||
marketPrice: 57973.008,
|
||||
marketState: MarketState.open,
|
||||
name: 'Bitcoin USD',
|
||||
quantity: 1,
|
||||
@ -300,7 +333,7 @@ describe('Portfolio', () => {
|
||||
Currency.USD,
|
||||
baseCurrency
|
||||
),
|
||||
// marketPrice: 57973.008,
|
||||
marketPrice: 3915.337,
|
||||
name: 'Ethereum USD',
|
||||
quantity: 0.2,
|
||||
transactionCount: 1,
|
||||
@ -331,7 +364,7 @@ describe('Portfolio', () => {
|
||||
baseCurrency
|
||||
),
|
||||
investmentInOriginalCurrency: 0.2 * 991.49,
|
||||
// marketPrice: 0,
|
||||
// marketPrice: 3915.337,
|
||||
quantity: 0.2
|
||||
}
|
||||
});
|
||||
@ -407,7 +440,7 @@ describe('Portfolio', () => {
|
||||
baseCurrency
|
||||
),
|
||||
investmentInOriginalCurrency: 0.2 * 991.49 + 0.3 * 1050,
|
||||
// marketPrice: 0,
|
||||
// marketPrice: 3641.984,
|
||||
quantity: 0.5
|
||||
}
|
||||
});
|
||||
@ -556,8 +589,7 @@ describe('Portfolio', () => {
|
||||
}
|
||||
]);
|
||||
|
||||
// TODO: Fix
|
||||
/*expect(portfolio.getCommittedFunds()).toEqual(
|
||||
expect(portfolio.getCommittedFunds()).toEqual(
|
||||
exchangeRateDataService.toCurrency(
|
||||
0.2 * 991.49,
|
||||
Currency.USD,
|
||||
@ -573,7 +605,7 @@ describe('Portfolio', () => {
|
||||
Currency.USD,
|
||||
baseCurrency
|
||||
)
|
||||
);*/
|
||||
);
|
||||
|
||||
expect(portfolio.getFees()).toEqual(
|
||||
exchangeRateDataService.toCurrency(3, Currency.USD, baseCurrency)
|
||||
@ -585,12 +617,11 @@ describe('Portfolio', () => {
|
||||
(0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050) / (0.2 - 0.1 + 0.2),
|
||||
currency: Currency.USD,
|
||||
firstBuyDate: '2018-01-05T00:00:00.000Z',
|
||||
// TODO: Fix
|
||||
/*investment: exchangeRateDataService.toCurrency(
|
||||
investment: exchangeRateDataService.toCurrency(
|
||||
0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050,
|
||||
Currency.USD,
|
||||
baseCurrency
|
||||
),*/
|
||||
),
|
||||
investmentInOriginalCurrency: 0.2 * 991.49 - 0.1 * 1050 + 0.2 * 1050,
|
||||
// marketPrice: 0,
|
||||
quantity: 0.2 - 0.1 + 0.2
|
||||
@ -600,8 +631,4 @@ describe('Portfolio', () => {
|
||||
expect(portfolio.getSymbols(getYesterday())).toEqual(['ETHUSD']);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
prismaService.$disconnect();
|
||||
});
|
||||
});
|
||||
|
@ -1,13 +1,14 @@
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { getToday, getYesterday, resetHours } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
PortfolioItem,
|
||||
Position
|
||||
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import {
|
||||
UNKNOWN_KEY,
|
||||
getToday,
|
||||
getYesterday,
|
||||
resetHours
|
||||
} from '@ghostfolio/helper';
|
||||
PortfolioPerformance,
|
||||
PortfolioPosition,
|
||||
PortfolioReport,
|
||||
Position,
|
||||
UserWithSettings
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { DateRange, OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import {
|
||||
add,
|
||||
format,
|
||||
@ -27,12 +28,6 @@ import {
|
||||
import { cloneDeep, isEmpty } from 'lodash';
|
||||
import * as roundTo from 'round-to';
|
||||
|
||||
import { UserWithSettings } from '../app/interfaces/user-with-settings';
|
||||
import { OrderWithAccount } from '../app/order/interfaces/order-with-account.type';
|
||||
import { DateRange } from '../app/portfolio/interfaces/date-range.type';
|
||||
import { PortfolioPerformance } from '../app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import { PortfolioPosition } from '../app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioReport } from '../app/portfolio/interfaces/portfolio-report.interface';
|
||||
import { DataProviderService } from '../services/data-provider.service';
|
||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
||||
import { IOrder } from '../services/interfaces/interfaces';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { groupBy } from '@ghostfolio/helper';
|
||||
import { groupBy } from '@ghostfolio/common/helper';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
import { PortfolioPosition } from '../app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from '../services/exchange-rate-data.service';
|
||||
import { EvaluationResult } from './interfaces/evaluation-result.interface';
|
||||
import { RuleInterface } from './interfaces/rule.interface';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { ExchangeRateDataService } from 'apps/api/src/services/exchange-rate-data.service';
|
||||
|
||||
import { Rule } from '../../rule';
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { bool, cleanEnv, num, port, str } from 'envalid';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { bool, cleanEnv, json, num, port, str } from 'envalid';
|
||||
|
||||
import { Environment } from './interfaces/environment.interface';
|
||||
|
||||
@ -12,6 +13,7 @@ export class ConfigurationService {
|
||||
ACCESS_TOKEN_SALT: str(),
|
||||
ALPHA_VANTAGE_API_KEY: str({ default: '' }),
|
||||
CACHE_TTL: num({ default: 1 }),
|
||||
DATA_SOURCES: json({ default: JSON.stringify([DataSource.YAHOO]) }),
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: bool({ default: false }),
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: bool({ default: false }),
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: bool({ default: false }),
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { benchmarks, currencyPairs } from '@ghostfolio/common/config';
|
||||
import {
|
||||
benchmarks,
|
||||
currencyPairs,
|
||||
getUtc,
|
||||
isGhostfolioScraperApiSymbol,
|
||||
resetHours
|
||||
} from '@ghostfolio/helper';
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
differenceInHours,
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import {
|
||||
isCrypto,
|
||||
isGhostfolioScraperApiSymbol,
|
||||
isRakutenRapidApiSymbol
|
||||
} from '@ghostfolio/helper';
|
||||
} from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { MarketData } from '@prisma/client';
|
||||
import { DataSource, MarketData } from '@prisma/client';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
@ -13,7 +15,6 @@ import { GhostfolioScraperApiService } from './data-provider/ghostfolio-scraper-
|
||||
import { RakutenRapidApiService } from './data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
|
||||
import { YahooFinanceService } from './data-provider/yahoo-finance/yahoo-finance.service';
|
||||
import { DataProviderInterface } from './interfaces/data-provider.interface';
|
||||
import { Granularity } from './interfaces/granularity.type';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
@ -184,4 +185,19 @@ export class DataProviderService implements DataProviderInterface {
|
||||
|
||||
return dataOfYahoo;
|
||||
}
|
||||
|
||||
public async search(aSymbol: string) {
|
||||
return this.getDataProvider().search(aSymbol);
|
||||
}
|
||||
|
||||
private getDataProvider() {
|
||||
switch (this.configurationService.get('DATA_SOURCES')[0]) {
|
||||
case DataSource.ALPHA_VANTAGE:
|
||||
return this.alphaVantageService;
|
||||
case DataSource.YAHOO:
|
||||
return this.yahooFinanceService;
|
||||
default:
|
||||
throw new Error('No data provider has been found.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import { isAfter, isBefore, parse } from 'date-fns';
|
||||
|
||||
import { ConfigurationService } from '../../configuration.service';
|
||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
||||
import { Granularity } from '../../interfaces/granularity.type';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
@ -77,7 +79,17 @@ export class AlphaVantageService implements DataProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
public search(aSymbol: string) {
|
||||
return this.alphaVantage.data.search(aSymbol);
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
const result = await this.alphaVantage.data.search(aSymbol);
|
||||
|
||||
return {
|
||||
items: result?.bestMatches?.map((bestMatch) => {
|
||||
return {
|
||||
dataSource: DataSource.ALPHA_VANTAGE,
|
||||
name: bestMatch['2. name'],
|
||||
symbol: bestMatch['1. symbol']
|
||||
};
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getYesterday } from '@ghostfolio/helper';
|
||||
import { getYesterday } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import * as bent from 'bent';
|
||||
@ -6,7 +7,6 @@ import * as cheerio from 'cheerio';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
||||
import { Granularity } from '../../interfaces/granularity.type';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse,
|
||||
@ -117,6 +117,10 @@ export class GhostfolioScraperApiService implements DataProviderInterface {
|
||||
return [];
|
||||
}
|
||||
|
||||
public async search(aSymbol: string) {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
private extractNumberFromString(aString: string): number {
|
||||
try {
|
||||
const [numberString] = aString.match(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { getToday, getYesterday } from '@ghostfolio/helper';
|
||||
import { getToday, getYesterday } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import * as bent from 'bent';
|
||||
@ -6,7 +7,6 @@ import { format, subMonths, subWeeks, subYears } from 'date-fns';
|
||||
|
||||
import { ConfigurationService } from '../../configuration.service';
|
||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
||||
import { Granularity } from '../../interfaces/granularity.type';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse,
|
||||
@ -117,6 +117,14 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
return {};
|
||||
}
|
||||
|
||||
public async search(aSymbol: string) {
|
||||
return { items: [] };
|
||||
}
|
||||
|
||||
public setPrisma(aPrismaService: PrismaService) {
|
||||
this.prisma = aPrismaService;
|
||||
}
|
||||
|
||||
private async getFearAndGreedIndex(): Promise<{
|
||||
now: { value: number; valueText: string };
|
||||
previousClose: { value: number; valueText: string };
|
||||
@ -147,8 +155,4 @@ export class RakutenRapidApiService implements DataProviderInterface {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public setPrisma(aPrismaService: PrismaService) {
|
||||
this.prisma = aPrismaService;
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
/*
|
||||
import { Test } from '@nestjs/testing';
|
||||
|
||||
import { YahooFinanceService } from './yahoo-finance.service';
|
||||
|
||||
describe('AppService', () => {
|
||||
let service: YahooFinanceService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const app = await Test.createTestingModule({
|
||||
imports: [],
|
||||
providers: [YahooFinanceService]
|
||||
}).compile();
|
||||
|
||||
service = app.get<YahooFinanceService>(YahooFinanceService);
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return data for USDCHF', () => {
|
||||
expect(service.get(['USDCHF'])).toEqual('{}');
|
||||
});
|
||||
});
|
||||
});
|
||||
*/
|
@ -1,16 +1,14 @@
|
||||
import {
|
||||
UNKNOWN_KEY,
|
||||
isCrypto,
|
||||
isCurrency,
|
||||
parseCurrency
|
||||
} from '@ghostfolio/helper';
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { isCrypto, isCurrency, parseCurrency } from '@ghostfolio/common/helper';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from '@prisma/client';
|
||||
import * as bent from 'bent';
|
||||
import { format } from 'date-fns';
|
||||
import * as yahooFinance from 'yahoo-finance';
|
||||
|
||||
import { DataProviderInterface } from '../../interfaces/data-provider.interface';
|
||||
import { Granularity } from '../../interfaces/granularity.type';
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse,
|
||||
@ -26,6 +24,8 @@ import {
|
||||
|
||||
@Injectable()
|
||||
export class YahooFinanceService implements DataProviderInterface {
|
||||
private yahooFinanceHostname = 'https://query1.finance.yahoo.com';
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public async get(
|
||||
@ -140,6 +140,49 @@ export class YahooFinanceService implements DataProviderInterface {
|
||||
}
|
||||
}
|
||||
|
||||
public async search(aSymbol: string): Promise<{ items: LookupItem[] }> {
|
||||
let items = [];
|
||||
|
||||
try {
|
||||
const get = bent(
|
||||
`${this.yahooFinanceHostname}/v1/finance/search?q=${aSymbol}&lang=en-US®ion=US"esCount=8&newsCount=0&enableFuzzyQuery=false"esQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
|
||||
'GET',
|
||||
'json',
|
||||
200
|
||||
);
|
||||
|
||||
const result = await get();
|
||||
items = result.quotes
|
||||
.filter((quote) => {
|
||||
return quote.isYahooFinance;
|
||||
})
|
||||
.filter(({ quoteType }) => {
|
||||
return (
|
||||
quoteType === 'CRYPTOCURRENCY' ||
|
||||
quoteType === 'EQUITY' ||
|
||||
quoteType === 'ETF'
|
||||
);
|
||||
})
|
||||
.filter(({ quoteType, symbol }) => {
|
||||
if (quoteType === 'CRYPTOCURRENCY') {
|
||||
// Only allow cryptocurrencies in USD
|
||||
return symbol.includes('USD');
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(({ longname, shortname, symbol }) => {
|
||||
return {
|
||||
dataSource: DataSource.YAHOO,
|
||||
name: longname || shortname,
|
||||
symbol: convertFromYahooSymbol(symbol)
|
||||
};
|
||||
});
|
||||
} catch {}
|
||||
|
||||
return { items };
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a symbol to a Yahoo symbol
|
||||
*
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { getYesterday } from '@ghostfolio/helper';
|
||||
import { getYesterday } from '@ghostfolio/common/helper';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { format } from 'date-fns';
|
||||
|
@ -1,4 +1,6 @@
|
||||
import { Granularity } from './granularity.type';
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { Granularity } from '@ghostfolio/common/types';
|
||||
|
||||
import {
|
||||
IDataProviderHistoricalResponse,
|
||||
IDataProviderResponse
|
||||
@ -15,4 +17,6 @@ export interface DataProviderInterface {
|
||||
): Promise<{
|
||||
[symbol: string]: { [date: string]: IDataProviderHistoricalResponse };
|
||||
}>;
|
||||
|
||||
search(aSymbol: string): Promise<{ items: LookupItem[] }>;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ export interface Environment extends CleanedEnvAccessors {
|
||||
ACCESS_TOKEN_SALT: string;
|
||||
ALPHA_VANTAGE_API_KEY: string;
|
||||
CACHE_TTL: number;
|
||||
DATA_SOURCES: string | string[]; // string is not correct, error in envalid?
|
||||
ENABLE_FEATURE_CUSTOM_SYMBOLS: boolean;
|
||||
ENABLE_FEATURE_FEAR_AND_GREED_INDEX: boolean;
|
||||
ENABLE_FEATURE_SOCIAL_LOGIN: boolean;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/helper';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { Account, Currency, DataSource } from '@prisma/client';
|
||||
|
||||
import { OrderType } from '../../models/order-type';
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {
|
||||
DEFAULT_DATE_FORMAT,
|
||||
DEFAULT_DATE_FORMAT_MONTH_YEAR
|
||||
} from '@ghostfolio/helper';
|
||||
} from '@ghostfolio/common/config';
|
||||
|
||||
export const DateFormats = {
|
||||
display: {
|
||||
|
@ -78,11 +78,16 @@ const routes: Routes = [
|
||||
(m) => m.TransactionsPageModule
|
||||
)
|
||||
},
|
||||
{
|
||||
path: 'zen',
|
||||
loadChildren: () =>
|
||||
import('./pages/zen/zen-page.module').then((m) => m.ZenPageModule)
|
||||
},
|
||||
{
|
||||
// wildcard, if requested url doesn't match any paths for routes defined
|
||||
// earlier
|
||||
path: '**',
|
||||
redirectTo: '/home',
|
||||
redirectTo: 'home',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
||||
|
@ -6,14 +6,9 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { NavigationEnd, PRIMARY_OUTLET, Router } from '@angular/router';
|
||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import {
|
||||
hasPermission,
|
||||
permissions,
|
||||
primaryColorHex,
|
||||
secondaryColorHex
|
||||
} from '@ghostfolio/helper';
|
||||
import { primaryColorHex, secondaryColorHex } from '@ghostfolio/common/config';
|
||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { MaterialCssVarsService } from 'angular-material-css-vars';
|
||||
import { Subject } from 'rxjs';
|
||||
import { filter, takeUntil } from 'rxjs/operators';
|
||||
|
@ -22,7 +22,7 @@ import { AppComponent } from './app.component';
|
||||
import { GfHeaderModule } from './components/header/header.module';
|
||||
import { authInterceptorProviders } from './core/auth.interceptor';
|
||||
import { httpResponseInterceptorProviders } from './core/http-response.interceptor';
|
||||
import { LanguageManager } from './core/language-manager.service';
|
||||
import { LanguageService } from './core/language.service';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
@ -46,11 +46,11 @@ import { LanguageManager } from './core/language-manager.service';
|
||||
providers: [
|
||||
authInterceptorProviders,
|
||||
httpResponseInterceptorProviders,
|
||||
LanguageManager,
|
||||
LanguageService,
|
||||
{
|
||||
provide: DateAdapter,
|
||||
useClass: CustomDateAdapter,
|
||||
deps: [LanguageManager, MAT_DATE_LOCALE, Platform]
|
||||
deps: [LanguageService, MAT_DATE_LOCALE, Platform]
|
||||
},
|
||||
{ provide: MAT_DATE_FORMATS, useValue: DateFormats }
|
||||
],
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
||||
import { Access } from '@ghostfolio/common/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-access-table',
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
OnChanges,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { resolveFearAndGreedIndex } from '@ghostfolio/helper';
|
||||
import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-fear-and-greed-index',
|
||||
|
@ -8,11 +8,14 @@
|
||||
class="d-none d-sm-block"
|
||||
i18n
|
||||
mat-flat-button
|
||||
[color]="currentRoute === 'home' ? 'primary' : null"
|
||||
[color]="
|
||||
currentRoute === 'home' || currentRoute === 'zen' ? 'primary' : null
|
||||
"
|
||||
[routerLink]="['/']"
|
||||
>Overview</a
|
||||
>
|
||||
<a
|
||||
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
@ -21,6 +24,7 @@
|
||||
>Analysis</a
|
||||
>
|
||||
<a
|
||||
*ngIf="user?.settings?.viewMode === 'DEFAULT'"
|
||||
class="d-none d-sm-block mx-1"
|
||||
i18n
|
||||
mat-flat-button
|
||||
@ -174,7 +178,7 @@
|
||||
mat-menu-item
|
||||
[ngClass]="{ 'font-weight-bold': currentRoute === 'account' }"
|
||||
[routerLink]="['/account']"
|
||||
>Ghostfolio Account</a
|
||||
>My Ghostfolio</a
|
||||
>
|
||||
<a
|
||||
*ngIf="hasPermissionToAccessAdminControl"
|
||||
|
@ -6,13 +6,12 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { Router } from '@angular/router';
|
||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/pages/login/login-with-access-token-dialog/login-with-access-token-dialog.component';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import { InfoItem, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { EMPTY, Subject } from 'rxjs';
|
||||
import { catchError, takeUntil } from 'rxjs/operators';
|
||||
|
||||
|
@ -9,8 +9,8 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { primaryColorRgb } from '@ghostfolio/helper';
|
||||
import { primaryColorRgb } from '@ghostfolio/common/config';
|
||||
import { PortfolioItem } from '@ghostfolio/common/interfaces';
|
||||
import {
|
||||
LineController,
|
||||
LineElement,
|
||||
|
@ -9,11 +9,8 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import {
|
||||
getBackgroundColor,
|
||||
primaryColorRgb,
|
||||
secondaryColorRgb
|
||||
} from '@ghostfolio/helper';
|
||||
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
||||
import { getBackgroundColor } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Chart,
|
||||
Filler,
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
OnChanges,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
|
||||
import { PortfolioOverview } from '@ghostfolio/common/interfaces';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
@Component({
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { PortfolioPerformance } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { CountUp } from 'countup.js';
|
||||
import { isNumber } from 'lodash';
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { PortfolioPerformance } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import { PortfolioPerformance } from '@ghostfolio/common/interfaces';
|
||||
import { Currency } from '@prisma/client';
|
||||
|
||||
@Component({
|
||||
|
@ -7,7 +7,7 @@ import {
|
||||
OnChanges,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { PortfolioItem } from '@ghostfolio/common/interfaces';
|
||||
import { endOfDay, parseISO, startOfDay } from 'date-fns';
|
||||
|
||||
@Component({
|
||||
|
@ -7,8 +7,9 @@ import {
|
||||
OnInit,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { UNKNOWN_KEY, getCssVariable, getTextColor } from '@ghostfolio/helper';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { getCssVariable, getTextColor } from '@ghostfolio/common/helper';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Tooltip } from 'chart.js';
|
||||
import { LinearScale } from 'chart.js';
|
||||
|
@ -35,7 +35,9 @@
|
||||
<div class="h6 m-0 text-truncate">{{ position?.name }}</div>
|
||||
<div class="d-flex">
|
||||
<span>{{ position?.symbol | gfSymbol }}</span>
|
||||
<span *ngIf="position?.exchange" class="ml-2 text-muted"
|
||||
<span
|
||||
*ngIf="position?.exchange && position?.exchange !== unknownKey"
|
||||
class="ml-2 text-muted"
|
||||
>({{ position.exchange }})</span
|
||||
>
|
||||
</div>
|
||||
|
@ -7,8 +7,9 @@ import {
|
||||
} from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
import { PositionDetailDialog } from './position-detail-dialog/position-detail-dialog.component';
|
||||
@ -27,7 +28,7 @@ export class PositionComponent implements OnDestroy, OnInit {
|
||||
@Input() position: PortfolioPosition;
|
||||
@Input() range: string;
|
||||
|
||||
public routeQueryParams: Subscription;
|
||||
public unknownKey = UNKNOWN_KEY;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
@ -36,7 +37,7 @@ export class PositionComponent implements OnDestroy, OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private router: Router
|
||||
) {
|
||||
this.routeQueryParams = route.queryParams
|
||||
route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (
|
||||
|
@ -13,7 +13,7 @@ import { MatPaginator } from '@angular/material/paginator';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||
import { Order as OrderModel } from '@prisma/client';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
@ -5,8 +5,8 @@ import {
|
||||
OnChanges,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { MarketState } from '@ghostfolio/api/services/interfaces/interfaces';
|
||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces/portfolio-position.interface';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-positions',
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { PortfolioReportRule } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-report.interface';
|
||||
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-rule',
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
|
||||
import { PortfolioReportRule } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-report.interface';
|
||||
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-rules',
|
||||
|
@ -21,8 +21,9 @@ 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 { OrderWithAccount } from '@ghostfolio/api/app/order/interfaces/order-with-account.type';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { format } from 'date-fns';
|
||||
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -258,7 +259,21 @@ export class TransactionsTableComponent
|
||||
.filter((item) => {
|
||||
return item !== undefined;
|
||||
})
|
||||
.sort();
|
||||
.sort((a, b) => {
|
||||
const aFirstChar = a.charAt(0);
|
||||
const bFirstChar = b.charAt(0);
|
||||
const isANumber = aFirstChar >= '0' && aFirstChar <= '9';
|
||||
const isBNumber = bFirstChar >= '0' && bFirstChar <= '9';
|
||||
|
||||
// Sort priority: text, followed by numbers
|
||||
if (isANumber && !isBNumber) {
|
||||
return 1;
|
||||
} else if (!isANumber && isBNumber) {
|
||||
return -1;
|
||||
} else {
|
||||
return a.toLowerCase() < b.toLowerCase() ? -1 : 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private getFilterableValues(
|
||||
@ -270,6 +285,7 @@ export class TransactionsTableComponent
|
||||
fieldValues.add(transaction.type);
|
||||
fieldValues.add(transaction.Account?.name);
|
||||
fieldValues.add(transaction.Account?.Platform?.name);
|
||||
fieldValues.add(format(transaction.date, 'yyyy'));
|
||||
|
||||
return [...fieldValues].filter((item) => {
|
||||
return item !== undefined;
|
||||
|
@ -5,7 +5,7 @@ import {
|
||||
OnChanges,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { format, isDate } from 'date-fns';
|
||||
import { isNumber } from 'lodash';
|
||||
|
||||
|
@ -5,34 +5,69 @@ import {
|
||||
Router,
|
||||
RouterStateSnapshot
|
||||
} from '@angular/router';
|
||||
import { ViewMode } from '@prisma/client';
|
||||
import { EMPTY } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
import { TokenStorageService } from '../services/token-storage.service';
|
||||
import { DataService } from '../services/data.service';
|
||||
import { SettingsStorageService } from '../services/settings-storage.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class AuthGuard implements CanActivate {
|
||||
constructor(
|
||||
private dataService: DataService,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
private settingsStorageService: SettingsStorageService
|
||||
) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const isLoggedIn = !!this.tokenStorageService.getToken();
|
||||
|
||||
if (isLoggedIn) {
|
||||
if (state.url === '/start') {
|
||||
this.router.navigate(['/home']);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (route.queryParams?.utm_source) {
|
||||
this.settingsStorageService.setSetting(
|
||||
'utm_source',
|
||||
route.queryParams?.utm_source
|
||||
);
|
||||
}
|
||||
|
||||
// Not logged in
|
||||
if (state.url !== '/start') {
|
||||
this.router.navigate(['/start']);
|
||||
return false;
|
||||
}
|
||||
return new Promise<boolean>((resolve) => {
|
||||
this.dataService
|
||||
.fetchUser()
|
||||
.pipe(
|
||||
catchError(() => {
|
||||
if (state.url !== '/start') {
|
||||
this.router.navigate(['/start']);
|
||||
resolve(false);
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
return true;
|
||||
resolve(true);
|
||||
return EMPTY;
|
||||
})
|
||||
)
|
||||
.subscribe((user) => {
|
||||
if (
|
||||
state.url === '/home' &&
|
||||
user.settings.viewMode === ViewMode.ZEN
|
||||
) {
|
||||
this.router.navigate(['/zen']);
|
||||
resolve(false);
|
||||
} else if (state.url === '/start') {
|
||||
if (user.settings.viewMode === ViewMode.ZEN) {
|
||||
this.router.navigate(['/zen']);
|
||||
} else {
|
||||
this.router.navigate(['/home']);
|
||||
}
|
||||
|
||||
resolve(false);
|
||||
} else if (
|
||||
state.url === '/zen' &&
|
||||
user.settings.viewMode === ViewMode.DEFAULT
|
||||
) {
|
||||
this.router.navigate(['/home']);
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,4 @@
|
||||
import {
|
||||
HTTP_INTERCEPTORS,
|
||||
HttpErrorResponse,
|
||||
HttpEvent
|
||||
} from '@angular/common/http';
|
||||
import { HTTP_INTERCEPTORS, HttpEvent } from '@angular/common/http';
|
||||
import {
|
||||
HttpHandler,
|
||||
HttpInterceptor,
|
||||
@ -11,7 +7,6 @@ import {
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
import { ImpersonationStorageService } from '../services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '../services/token-storage.service';
|
||||
|
@ -79,7 +79,6 @@ export class HttpResponseInterceptor implements HttpInterceptor {
|
||||
}
|
||||
} else if (error.status === StatusCodes.UNAUTHORIZED) {
|
||||
this.tokenStorageService.signOut();
|
||||
this.router.navigate(['start']);
|
||||
}
|
||||
|
||||
return throwError('');
|
||||
|
@ -1,126 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import * as deDateFnsLocale from 'date-fns/locale/de/index';
|
||||
import * as frDateFnsLocale from 'date-fns/locale/fr/index';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
// TODO: Rename to language service
|
||||
|
||||
/**
|
||||
* Service that distributes onLanguageChanged events
|
||||
*/
|
||||
@Injectable()
|
||||
export class LanguageManager {
|
||||
private static readonly AVALABLE_LANGUAGES = ['de', 'fr'];
|
||||
private static readonly LANGUAGE_LABELS = {
|
||||
de: 'Deutsch',
|
||||
fr: 'Français'
|
||||
};
|
||||
private currentLanguage: string;
|
||||
private changeLoadLanguageStateSubject = new BehaviorSubject(false);
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {} // private translate: TranslateService // private dataLoaderManager: DataLoaderManager,
|
||||
|
||||
/**
|
||||
* Emits an event that the language has changed
|
||||
*/
|
||||
public changeLanguage(aLanguage: string) {
|
||||
if (aLanguage && aLanguage !== this.currentLanguage) {
|
||||
this.currentLanguage = aLanguage;
|
||||
|
||||
this.changeLoadLanguageStateSubject.next(true);
|
||||
|
||||
// this.translate.use(this.currentLanguage);
|
||||
|
||||
/*this.dataLoaderManager.changeLanguage(this.currentLanguage).then(() => {
|
||||
// Emit an event that loading has finished
|
||||
this.changeLoadLanguageStateSubject.next(false);
|
||||
});*/
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of available languages for admin
|
||||
*/
|
||||
public getAvailableLanguages() {
|
||||
return LanguageManager.AVALABLE_LANGUAGES;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current language
|
||||
*/
|
||||
public getCurrentLanguage(aReturnFullLocale = false) {
|
||||
// Check if the full locale is needed (e.g. for angular pipes like
|
||||
// '| percentage')
|
||||
if (aReturnFullLocale) {
|
||||
if (this.currentLanguage) {
|
||||
if (this.currentLanguage.match(/^de/)) {
|
||||
return 'de-CH';
|
||||
}
|
||||
|
||||
if (this.currentLanguage.match(/^fr/)) {
|
||||
return 'fr-CH';
|
||||
}
|
||||
}
|
||||
|
||||
// Default
|
||||
return 'de-CH';
|
||||
}
|
||||
|
||||
if (this.currentLanguage) {
|
||||
return this.currentLanguage;
|
||||
}
|
||||
|
||||
// Default
|
||||
return 'de';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the locale module of date-fns in the current language
|
||||
*/
|
||||
public getDateFnsLocale() {
|
||||
let currentDateFnsLocale = null;
|
||||
|
||||
switch (this.getCurrentLanguage()) {
|
||||
case 'de':
|
||||
currentDateFnsLocale = deDateFnsLocale;
|
||||
break;
|
||||
case 'fr':
|
||||
currentDateFnsLocale = frDateFnsLocale;
|
||||
break;
|
||||
default:
|
||||
currentDateFnsLocale = deDateFnsLocale;
|
||||
}
|
||||
|
||||
return currentDateFnsLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default language
|
||||
*/
|
||||
public getDefaultLanguage() {
|
||||
// return globals.defaultLanguage;
|
||||
return 'de';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a pretty label of the given language
|
||||
*/
|
||||
public getLanguageLabel(aLanguage: string) {
|
||||
if (LanguageManager.LANGUAGE_LABELS[aLanguage]) {
|
||||
return LanguageManager.LANGUAGE_LABELS[aLanguage];
|
||||
}
|
||||
|
||||
return aLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable that emits true when loading is in progress and false
|
||||
* when loading is finished
|
||||
*/
|
||||
public onChangeLoadLanguageState() {
|
||||
return this.changeLoadLanguageStateSubject.asObservable();
|
||||
}
|
||||
}
|
6
apps/client/src/app/core/language.service.ts
Normal file
6
apps/client/src/app/core/language.service.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
@Injectable()
|
||||
export class LanguageService {
|
||||
public constructor() {}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { baseCurrency } from '@ghostfolio/helper';
|
||||
import { baseCurrency } from '@ghostfolio/common/config';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
|
@ -1,13 +1,9 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import {
|
||||
DEFAULT_DATE_FORMAT,
|
||||
hasPermission,
|
||||
permissions
|
||||
} from '@ghostfolio/helper';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { Access, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Currency } from '@prisma/client';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -23,6 +19,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
public currencies: Currency[] = [];
|
||||
public defaultDateFormat = DEFAULT_DATE_FORMAT;
|
||||
public hasPermissionForSubscription: boolean;
|
||||
public hasPermissionToUpdateUserSettings: boolean;
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -54,6 +51,11 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.hasPermissionToUpdateUserSettings = hasPermission(
|
||||
this.user.permissions,
|
||||
permissions.updateUserSettings
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
});
|
||||
@ -66,9 +68,14 @@ export class AccountPageComponent implements OnDestroy, OnInit {
|
||||
this.update();
|
||||
}
|
||||
|
||||
public onChangeBaseCurrency({ value: currency }: { value: Currency }) {
|
||||
public onChangeUserSettings(aKey: string, aValue: string) {
|
||||
const settings = { ...this.user.settings, [aKey]: aValue };
|
||||
|
||||
this.dataService
|
||||
.putUserSettings({ currency })
|
||||
.putUserSettings({
|
||||
baseCurrency: settings?.baseCurrency,
|
||||
viewMode: settings?.viewMode
|
||||
})
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
|
@ -30,13 +30,14 @@
|
||||
<div class="d-flex mt-4 py-1">
|
||||
<div class="pt-4 w-50" i18n>Settings</div>
|
||||
<div class="w-50">
|
||||
<form #addTransactionForm="ngForm">
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<form #changeUserSettingsForm="ngForm">
|
||||
<mat-form-field appearance="outline" class="mb-3 w-100">
|
||||
<mat-label i18n>Base Currency</mat-label>
|
||||
<mat-select
|
||||
name="baseCurrency"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
[value]="user.settings.baseCurrency"
|
||||
(selectionChange)="onChangeBaseCurrency($event)"
|
||||
(selectionChange)="onChangeUserSettings('baseCurrency', $event.value)"
|
||||
>
|
||||
<mat-option
|
||||
*ngFor="let currency of currencies"
|
||||
@ -45,6 +46,18 @@
|
||||
>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label i18n>View Mode</mat-label>
|
||||
<mat-select
|
||||
name="viewMode"
|
||||
[disabled]="!hasPermissionToUpdateUserSettings"
|
||||
[value]="user.settings.viewMode"
|
||||
(selectionChange)="onChangeUserSettings('viewMode', $event.value)"
|
||||
>
|
||||
<mat-option value="DEFAULT">Default</mat-option>
|
||||
<mat-option value="ZEN">Zen</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,11 +3,11 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Account as AccountModel, AccountType } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { AdminService } from '@ghostfolio/client/services/admin.service';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
|
||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||
import { AdminData, User } from '@ghostfolio/common/interfaces';
|
||||
import { formatDistanceToNow, isValid, parseISO, sub } from 'date-fns';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
@ -73,26 +73,40 @@
|
||||
<table class="gf-table">
|
||||
<thead>
|
||||
<tr class="mat-header-row">
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>#</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>User</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Registration Date</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Accounts</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Transactions</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Engagement</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>
|
||||
Registration Date
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>
|
||||
Accounts
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>
|
||||
Transactions
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2 text-center" i18n>
|
||||
Engagement
|
||||
</th>
|
||||
<th class="mat-header-cell px-1 py-2" i18n>Last Activitiy</th>
|
||||
<th class="mat-header-cell px-1 py-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let userItem of users" class="mat-row">
|
||||
<tr *ngFor="let userItem of users; let i = index" class="mat-row">
|
||||
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
{{ userItem.alias || userItem.id }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
{{ userItem.createdAt | date: defaultDateFormat }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">{{ userItem._count?.Account }}</td>
|
||||
<td class="mat-cell px-1 py-2">{{ userItem._count?.Order }}</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
{{ userItem._count?.Account }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
{{ userItem._count?.Order }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2 text-right">
|
||||
{{ userItem.Analytics?.activityCount }}
|
||||
</td>
|
||||
<td class="mat-cell px-1 py-2">
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import {
|
||||
PortfolioItem,
|
||||
PortfolioPosition,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
@ -1,11 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { DateRange } from '@ghostfolio/api/app/portfolio/interfaces/date-range.type';
|
||||
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
|
||||
import { PortfolioPerformance } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||
import { PerformanceChartDialog } from '@ghostfolio/client/components/performance-chart-dialog/performance-chart-dialog.component';
|
||||
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
||||
@ -16,7 +11,14 @@ import {
|
||||
SettingsStorageService
|
||||
} from '@ghostfolio/client/services/settings-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import {
|
||||
PortfolioOverview,
|
||||
PortfolioPerformance,
|
||||
PortfolioPosition,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
@ -66,7 +68,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
private settingsStorageService: SettingsStorageService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {
|
||||
this.routeQueryParams = route.queryParams
|
||||
this.routeQueryParams = this.route.queryParams
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((params) => {
|
||||
if (params['performanceChartDialog']) {
|
||||
@ -130,6 +132,11 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
this.update();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private openDialog(): void {
|
||||
const dialogRef = this.dialog.open(PerformanceChartDialog, {
|
||||
autoFocus: false,
|
||||
@ -193,9 +200,4 @@ export class HomePageComponent implements OnDestroy, OnInit {
|
||||
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
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 { RouterModule } from '@angular/router';
|
||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||
@ -27,7 +26,6 @@ import { HomePageComponent } from './home-page.component';
|
||||
GfPositionsModule,
|
||||
GfToggleModule,
|
||||
HomePageRoutingModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
RouterModule
|
||||
],
|
||||
|
@ -37,9 +37,6 @@ export class LoginPageComponent implements OnDestroy, OnInit {
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
// Remove all tokens (e.g. impersonationId)
|
||||
window.localStorage.clear();
|
||||
|
||||
this.dataService.fetchInfo().subscribe(({ demoAuthToken }) => {
|
||||
this.demoAuthToken = demoAuthToken;
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { baseCurrency } from '@ghostfolio/helper';
|
||||
import { baseCurrency } from '@ghostfolio/common/config';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { PortfolioReportRule } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-report.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
|
@ -3,11 +3,11 @@ import { MatDialog } from '@angular/material/dialog';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { hasPermission, permissions } from '@ghostfolio/helper';
|
||||
import { User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { Order as OrderModel } from '@prisma/client';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
|
15
apps/client/src/app/pages/zen/zen-page-routing.module.ts
Normal file
15
apps/client/src/app/pages/zen/zen-page-routing.module.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||
|
||||
import { ZenPageComponent } from './zen-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: '', component: ZenPageComponent, canActivate: [AuthGuard] }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class ZenPageRoutingModule {}
|
104
apps/client/src/app/pages/zen/zen-page.component.ts
Normal file
104
apps/client/src/app/pages/zen/zen-page.component.ts
Normal file
@ -0,0 +1,104 @@
|
||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { LineChartItem } from '@ghostfolio/client/components/line-chart/interfaces/line-chart.interface';
|
||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
|
||||
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces';
|
||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'gf-zen-page',
|
||||
templateUrl: './zen-page.html',
|
||||
styleUrls: ['./zen-page.scss']
|
||||
})
|
||||
export class ZenPageComponent implements OnDestroy, OnInit {
|
||||
public dateRange: DateRange = 'max';
|
||||
public deviceType: string;
|
||||
public hasImpersonationId: boolean;
|
||||
public hasPermissionToReadForeignPortfolio: boolean;
|
||||
public historicalDataItems: LineChartItem[];
|
||||
public isLoadingPerformance = true;
|
||||
public performance: PortfolioPerformance;
|
||||
public user: User;
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private cd: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
private deviceService: DeviceDetectorService,
|
||||
private impersonationStorageService: ImpersonationStorageService,
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {
|
||||
this.tokenStorageService
|
||||
.onChangeHasToken()
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe(() => {
|
||||
this.dataService.fetchUser().subscribe((user) => {
|
||||
this.user = user;
|
||||
|
||||
this.hasPermissionToReadForeignPortfolio = hasPermission(
|
||||
user.permissions,
|
||||
permissions.readForeignPortfolio
|
||||
);
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
this.impersonationStorageService
|
||||
.onChangeHasImpersonation()
|
||||
.subscribe((aId) => {
|
||||
this.hasImpersonationId = !!aId;
|
||||
});
|
||||
|
||||
this.update();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private update() {
|
||||
this.isLoadingPerformance = true;
|
||||
|
||||
this.dataService
|
||||
.fetchChart({ range: this.dateRange })
|
||||
.subscribe((chartData) => {
|
||||
this.historicalDataItems = chartData.map((chartDataItem) => {
|
||||
return {
|
||||
date: chartDataItem.date,
|
||||
value: chartDataItem.value
|
||||
};
|
||||
});
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.dataService
|
||||
.fetchPortfolioPerformance({ range: this.dateRange })
|
||||
.subscribe((response) => {
|
||||
this.performance = response;
|
||||
this.isLoadingPerformance = false;
|
||||
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
|
||||
this.cd.markForCheck();
|
||||
}
|
||||
}
|
25
apps/client/src/app/pages/zen/zen-page.html
Normal file
25
apps/client/src/app/pages/zen/zen-page.html
Normal file
@ -0,0 +1,25 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="chart-container col mr-3">
|
||||
<gf-line-chart
|
||||
symbol="Performance"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[showLoader]="false"
|
||||
[showXAxis]="false"
|
||||
[showYAxis]="false"
|
||||
></gf-line-chart>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overview-container row mb-5 mt-1">
|
||||
<div class="col">
|
||||
<gf-portfolio-performance-summary
|
||||
class="pb-4"
|
||||
[baseCurrency]="user?.settings?.baseCurrency"
|
||||
[isLoading]="isLoadingPerformance"
|
||||
[locale]="user?.settings?.locale"
|
||||
[performance]="performance"
|
||||
[showDetails]="!hasImpersonationId || hasPermissionToReadForeignPortfolio"
|
||||
></gf-portfolio-performance-summary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
23
apps/client/src/app/pages/zen/zen-page.module.ts
Normal file
23
apps/client/src/app/pages/zen/zen-page.module.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { GfLineChartModule } from '@ghostfolio/client/components/line-chart/line-chart.module';
|
||||
import { GfPortfolioPerformanceSummaryModule } from '@ghostfolio/client/components/portfolio-performance-summary/portfolio-performance-summary.module';
|
||||
|
||||
import { ZenPageRoutingModule } from './zen-page-routing.module';
|
||||
import { ZenPageComponent } from './zen-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [ZenPageComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
CommonModule,
|
||||
GfLineChartModule,
|
||||
GfPortfolioPerformanceSummaryModule,
|
||||
MatCardModule,
|
||||
ZenPageRoutingModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class ZenPageModule {}
|
38
apps/client/src/app/pages/zen/zen-page.scss
Normal file
38
apps/client/src/app/pages/zen/zen-page.scss
Normal file
@ -0,0 +1,38 @@
|
||||
:host {
|
||||
color: rgb(var(--dark-primary-text));
|
||||
display: block;
|
||||
|
||||
.chart-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
margin-top: 3rem;
|
||||
max-height: 50vh;
|
||||
|
||||
// Fallback for aspect-ratio (using padding hack)
|
||||
@supports not (aspect-ratio: 16 / 9) {
|
||||
&::before {
|
||||
float: left;
|
||||
padding-top: 56.25%;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: block;
|
||||
content: '';
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
gf-line-chart {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:host-context(.is-dark-theme) {
|
||||
color: rgb(var(--light-primary-text));
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/helper';
|
||||
import { ghostfolioScraperApiSymbolPrefix } from '@ghostfolio/common/config';
|
||||
|
||||
@Pipe({ name: 'gfSymbol' })
|
||||
export class SymbolPipe implements PipeTransform {
|
||||
|
@ -1,28 +1,36 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Access } from '@ghostfolio/api/app/access/interfaces/access.interface';
|
||||
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||
import { AdminData } from '@ghostfolio/api/app/admin/interfaces/admin-data.interface';
|
||||
import { InfoItem } from '@ghostfolio/api/app/info/interfaces/info-item.interface';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||
import { PortfolioItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-item.interface';
|
||||
import { PortfolioOverview } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-overview.interface';
|
||||
import { PortfolioPerformance } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-performance.interface';
|
||||
import {
|
||||
HistoricalDataItem,
|
||||
PortfolioPositionDetail
|
||||
} from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
||||
import { PortfolioPosition } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position.interface';
|
||||
import { PortfolioReport } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-report.interface';
|
||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
||||
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
|
||||
import { User } from '@ghostfolio/api/app/user/interfaces/user.interface';
|
||||
import { UpdateUserSettingsDto } from '@ghostfolio/api/app/user/update-user-settings.dto';
|
||||
import {
|
||||
Access,
|
||||
AdminData,
|
||||
InfoItem,
|
||||
PortfolioItem,
|
||||
PortfolioOverview,
|
||||
PortfolioPerformance,
|
||||
PortfolioPosition,
|
||||
PortfolioReport,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { Order as OrderModel } from '@prisma/client';
|
||||
import { Account as AccountModel } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { SettingsStorageService } from './settings-storage.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -30,7 +38,10 @@ import { Account as AccountModel } from '@prisma/client';
|
||||
export class DataService {
|
||||
private info: InfoItem;
|
||||
|
||||
public constructor(private http: HttpClient) {}
|
||||
public constructor(
|
||||
private http: HttpClient,
|
||||
private settingsStorageService: SettingsStorageService
|
||||
) {}
|
||||
|
||||
public fetchAccounts() {
|
||||
return this.http.get<AccountModel[]>('/api/account');
|
||||
@ -70,7 +81,20 @@ export class DataService {
|
||||
}
|
||||
*/
|
||||
|
||||
return this.http.get<InfoItem>('/api/info');
|
||||
return this.http.get<InfoItem>('/api/info').pipe(
|
||||
map((data) => {
|
||||
if (
|
||||
this.settingsStorageService.getSetting('utm_source') ===
|
||||
'trusted-web-activity'
|
||||
) {
|
||||
data.globalPermissions = data.globalPermissions.filter(
|
||||
(permission) => permission !== permissions.enableSubscription
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public fetchSymbolItem(aSymbol: string) {
|
||||
@ -78,11 +102,25 @@ export class DataService {
|
||||
}
|
||||
|
||||
public fetchSymbols(aQuery: string) {
|
||||
return this.http.get<LookupItem[]>(`/api/symbol/lookup?query=${aQuery}`);
|
||||
return this.http
|
||||
.get<{ items: LookupItem[] }>(`/api/symbol/lookup?query=${aQuery}`)
|
||||
.pipe(
|
||||
map((respose) => {
|
||||
return respose.items;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public fetchOrders() {
|
||||
return this.http.get<OrderModel[]>('/api/order');
|
||||
public fetchOrders(): Observable<OrderModel[]> {
|
||||
return this.http.get<any[]>('/api/order').pipe(
|
||||
map((data) => {
|
||||
for (const item of data) {
|
||||
item.createdAt = parseISO(item.createdAt);
|
||||
item.date = parseISO(item.date);
|
||||
}
|
||||
return data;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public fetchPortfolio() {
|
||||
|
@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
const TOKEN_KEY = 'auth-token';
|
||||
// const USER_KEY = 'auth-user';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
@ -12,10 +11,12 @@ export class TokenStorageService {
|
||||
|
||||
public constructor() {}
|
||||
|
||||
public signOut(): void {
|
||||
window.localStorage.clear();
|
||||
public getToken(): string {
|
||||
return window.localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
this.hasTokenChangeSubject.next();
|
||||
public onChangeHasToken() {
|
||||
return this.hasTokenChangeSubject.asObservable();
|
||||
}
|
||||
|
||||
public saveToken(token: string): void {
|
||||
@ -25,20 +26,15 @@ export class TokenStorageService {
|
||||
this.hasTokenChangeSubject.next();
|
||||
}
|
||||
|
||||
public getToken(): string {
|
||||
return window.localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
public signOut(): void {
|
||||
const utmSource = window.localStorage.getItem('utm_source');
|
||||
|
||||
public onChangeHasToken() {
|
||||
return this.hasTokenChangeSubject.asObservable();
|
||||
}
|
||||
window.localStorage.clear();
|
||||
|
||||
/*public saveUser(user): void {
|
||||
window.localStorage.removeItem(USER_KEY);
|
||||
window.localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
}
|
||||
if (utmSource) {
|
||||
window.localStorage.setItem('utm_source', utmSource);
|
||||
}
|
||||
|
||||
public getUser(): any {
|
||||
return JSON.parse(localStorage.getItem(USER_KEY));
|
||||
}*/
|
||||
this.hasTokenChangeSubject.next();
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,6 @@ module.exports = {
|
||||
projects: [
|
||||
'<rootDir>/apps/api',
|
||||
'<rootDir>/apps/client',
|
||||
'<rootDir>/libs/helper'
|
||||
'<rootDir>/libs/common'
|
||||
]
|
||||
};
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user