Compare commits

..

7 Commits

Author SHA1 Message Date
92e502e1c2 Release 1.103.0 (#628) 2022-01-13 20:33:31 +01:00
e344c43a5a Bugfix/fix currency of value in position detail dialog (#627)
* Fix currency

* Update changelog
2022-01-13 20:25:21 +01:00
d6b78f3457 Feature/add links to statistics section (#626)
* Add links and clean up style

* Update changelog
2022-01-13 19:07:23 +01:00
9bbb856f66 Release 1.102.0 (#625) 2022-01-11 19:53:23 +01:00
d3707bbb87 Bugfix/fix preselected default account in create activity dialog (#624)
* Fix preselected default account

* Update changelog
2022-01-11 19:50:22 +01:00
7df53896f3 Feature/start eliminating data source from order (#622)
* Start eliminating dataSource from order

* Update changelog
2022-01-11 19:49:45 +01:00
b2b3fde80e Bugfix/support multiple accounts with the same name (#623)
* Support multiple accounts with the same name

* Update changelog
2022-01-10 21:23:47 +01:00
13 changed files with 134 additions and 177 deletions

View File

@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.103.0 - 13.01.2022
### Changed
- Added links to the statistics section on the about page
### Fixed
- Fixed the currency of the value in the position detail dialog
## 1.102.0 - 11.01.2022
### Changed
- Start eliminating `dataSource` from activity
### Fixed
- Fixed the support for multiple accounts with the same name
- Fixed the preselected default account of the create activity dialog
## 1.101.0 - 08.01.2022 ## 1.101.0 - 08.01.2022
### Added ### Added

View File

@ -85,19 +85,6 @@ describe('CurrentRateService', () => {
); );
}); });
it('getValue', async () => {
expect(
await currentRateService.getValue({
currency: 'USD',
date: new Date(Date.UTC(2020, 0, 1, 0, 0, 0)),
symbol: 'AMZN',
userCurrency: 'CHF'
})
).toMatchObject({
marketPrice: 1847.839966
});
});
it('getValues', async () => { it('getValues', async () => {
expect( expect(
await currentRateService.getValues({ await currentRateService.getValues({

View File

@ -7,7 +7,6 @@ import { isBefore, isToday } from 'date-fns';
import { flatten } from 'lodash'; import { flatten } from 'lodash';
import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValueObject } from './interfaces/get-value-object.interface';
import { GetValueParams } from './interfaces/get-value-params.interface';
import { GetValuesParams } from './interfaces/get-values-params.interface'; import { GetValuesParams } from './interfaces/get-values-params.interface';
@Injectable() @Injectable()
@ -18,46 +17,6 @@ export class CurrentRateService {
private readonly marketDataService: MarketDataService private readonly marketDataService: MarketDataService
) {} ) {}
public async getValue({
currency,
date,
symbol,
userCurrency
}: GetValueParams): Promise<GetValueObject> {
if (isToday(date)) {
const dataProviderResult = await this.dataProviderService.get([
{
symbol,
dataSource: this.dataProviderService.getPrimaryDataSource()
}
]);
return {
symbol,
date: resetHours(date),
marketPrice: dataProviderResult?.[symbol]?.marketPrice ?? 0
};
}
const marketData = await this.marketDataService.get({
date,
symbol
});
if (marketData) {
return {
date: marketData.date,
marketPrice: this.exchangeRateDataService.toCurrency(
marketData.marketPrice,
currency,
userCurrency
),
symbol: marketData.symbol
};
}
throw new Error(`Value not found for ${symbol} at ${resetHours(date)}`);
}
public async getValues({ public async getValues({
currencies, currencies,
dataGatheringItems, dataGatheringItems,

View File

@ -1,6 +0,0 @@
export interface GetValueParams {
currency: string;
date: Date;
symbol: string;
userCurrency: string;
}

View File

@ -1,17 +1,9 @@
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import { addDays, endOfDay, format, isBefore, isSameDay } from 'date-fns';
addDays,
differenceInCalendarDays,
endOfDay,
format,
isBefore,
isSameDay
} from 'date-fns';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
import { GetValueParams } from './interfaces/get-value-params.interface';
import { GetValuesParams } from './interfaces/get-values-params.interface'; import { GetValuesParams } from './interfaces/get-values-params.interface';
import { PortfolioOrder } from './interfaces/portfolio-order.interface'; import { PortfolioOrder } from './interfaces/portfolio-order.interface';
import { TimelinePeriod } from './interfaces/timeline-period.interface'; import { TimelinePeriod } from './interfaces/timeline-period.interface';
@ -275,9 +267,6 @@ jest.mock('./current-rate.service', () => {
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => { CurrentRateService: jest.fn().mockImplementation(() => {
return { return {
getValue: ({ date, symbol }: GetValueParams) => {
return Promise.resolve(mockGetValue(symbol, date));
},
getValues: ({ dataGatheringItems, dateQuery }: GetValuesParams) => { getValues: ({ dataGatheringItems, dateQuery }: GetValuesParams) => {
const result = []; const result = [];
if (dateQuery.lt) { if (dateQuery.lt) {

View File

@ -107,7 +107,7 @@ export class PortfolioService {
account.currency, account.currency,
userCurrency userCurrency
), ),
value: details.accounts[account.name]?.current ?? 0 value: details.accounts[account.id]?.current ?? 0
}; };
delete result.Order; delete result.Order;
@ -428,7 +428,7 @@ export class PortfolioService {
}) })
.map((order) => ({ .map((order) => ({
currency: order.currency, currency: order.currency,
dataSource: order.dataSource, dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big(order.fee), fee: new Big(order.fee),
name: order.SymbolProfile?.name, name: order.SymbolProfile?.name,
@ -1038,7 +1038,7 @@ export class PortfolioService {
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({ const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
currency: order.currency, currency: order.currency,
dataSource: order.dataSource, dataSource: order.SymbolProfile?.dataSource ?? order.dataSource,
date: format(order.date, DATE_FORMAT), date: format(order.date, DATE_FORMAT),
fee: new Big( fee: new Big(
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
@ -1091,10 +1091,11 @@ export class PortfolioService {
account.currency, account.currency,
userCurrency userCurrency
); );
accounts[account.name] = { accounts[account.id] = {
balance: convertedBalance, balance: convertedBalance,
currency: account.currency, currency: account.currency,
current: convertedBalance, current: convertedBalance,
name: account.name,
original: convertedBalance original: convertedBalance
}; };
@ -1108,16 +1109,17 @@ export class PortfolioService {
originalValueOfSymbol *= -1; originalValueOfSymbol *= -1;
} }
if (accounts[order.Account?.name || UNKNOWN_KEY]?.current) { if (accounts[order.Account?.id || UNKNOWN_KEY]?.current) {
accounts[order.Account?.name || UNKNOWN_KEY].current += accounts[order.Account?.id || UNKNOWN_KEY].current +=
currentValueOfSymbol; currentValueOfSymbol;
accounts[order.Account?.name || UNKNOWN_KEY].original += accounts[order.Account?.id || UNKNOWN_KEY].original +=
originalValueOfSymbol; originalValueOfSymbol;
} else { } else {
accounts[order.Account?.name || UNKNOWN_KEY] = { accounts[order.Account?.id || UNKNOWN_KEY] = {
balance: 0, balance: 0,
currency: order.Account?.currency, currency: order.Account?.currency,
current: currentValueOfSymbol, current: currentValueOfSymbol,
name: account.name,
original: originalValueOfSymbol original: originalValueOfSymbol
}; };
} }

View File

@ -12,7 +12,7 @@
<div class="col-12 d-flex justify-content-center mb-3"> <div class="col-12 d-flex justify-content-center mb-3">
<gf-value <gf-value
size="large" size="large"
[currency]="currency" [currency]="data.baseCurrency"
[locale]="data.locale" [locale]="data.locale"
[value]="value" [value]="value"
></gf-value> ></gf-value>

View File

@ -132,16 +132,35 @@
</div> </div>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<h3 class="mb-0">{{ statistics?.slackCommunityUsers ?? '-' }}</h3> <a
class="d-block"
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
>
<h3 class="mb-0">
{{ statistics?.slackCommunityUsers ?? '-' }}
</h3>
<div class="h6 mb-0" i18n>Users in Slack community</div> <div class="h6 mb-0" i18n>Users in Slack community</div>
</a>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<h3 class="mb-0">{{ statistics?.gitHubContributors ?? '-' }}</h3> <a
class="d-block"
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
>
<h3 class="mb-0">
{{ statistics?.gitHubContributors ?? '-' }}
</h3>
<div class="h6 mb-0" i18n>Contributors on GitHub</div> <div class="h6 mb-0" i18n>Contributors on GitHub</div>
</a>
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<a
class="d-block"
href="https://github.com/ghostfolio/ghostfolio/stargazers"
>
<h3 class="mb-0">{{ statistics?.gitHubStargazers ?? '-' }}</h3> <h3 class="mb-0">{{ statistics?.gitHubStargazers ?? '-' }}</h3>
<div class="h6 mb-0" i18n>Stars on GitHub</div> <div class="h6 mb-0" i18n>Stars on GitHub</div>
</a>
</div> </div>
</div> </div>
</mat-card-content> </mat-card-content>
@ -150,22 +169,28 @@
</div> </div>
<div class="row"> <div class="row">
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
<a class="py-2 w-100" i18n mat-stroked-button [routerLink]="['/blog']"
>Blog</a
>
</div>
<div <div
class="col-md-6 col-xs-12 my-2" class="col-md-6 col-xs-12 my-2"
[ngClass]="{ 'offset-md-3': !hasPermissionForBlog }" [ngClass]="{ 'offset-md-3': !hasPermissionForBlog }"
> >
<a <a
class="py-2 w-100" class="py-2 w-100"
color="primary"
i18n i18n
mat-stroked-button mat-stroked-button
[routerLink]="['/about', 'changelog']" [routerLink]="['/about', 'changelog']"
>Changelog & License</a >Changelog & License</a
> >
</div> </div>
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
<a
class="py-2 w-100"
color="primary"
i18n
mat-flat-button
[routerLink]="['/blog']"
>Blog</a
>
</div>
</div> </div>
</div> </div>

View File

@ -2,13 +2,8 @@
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
display: block; display: block;
a {
color: rgb(var(--dark-primary-text));
}
.mat-card { .mat-card {
&.about-container, &.about-container {
&.changelog {
a { a {
color: rgba(var(--palette-primary-500), 1); color: rgba(var(--palette-primary-500), 1);
font-weight: 500; font-weight: 500;
@ -19,29 +14,6 @@
} }
} }
&.changelog {
::ng-deep {
markdown {
h1,
p {
display: none;
}
h2 {
font-size: 18px;
&:not(:first-of-type) {
margin-top: 2rem;
}
}
h3 {
font-size: 15px;
}
}
}
}
.independent-and-bootstrapped-logo { .independent-and-bootstrapped-logo {
background-image: url('/assets/bootstrapped-dark.svg'); background-image: url('/assets/bootstrapped-dark.svg');
background-position: center; background-position: center;
@ -57,10 +29,6 @@
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text)); color: rgb(var(--light-primary-text));
a {
color: rgb(var(--light-primary-text));
}
.mat-card { .mat-card {
.independent-and-bootstrapped-logo { .independent-and-bootstrapped-logo {
background-image: url('/assets/bootstrapped-light.svg'); background-image: url('/assets/bootstrapped-light.svg');

View File

@ -162,10 +162,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
} }
}; };
for (const [name, { current, original }] of Object.entries( for (const [id, { current, name, original }] of Object.entries(
this.portfolioDetails.accounts this.portfolioDetails.accounts
)) { )) {
this.accounts[name] = { this.accounts[id] = {
name, name,
value: aPeriod === 'original' ? original : current value: aPeriod === 'original' ? original : current
}; };

View File

@ -106,20 +106,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.updateUser(state.user);
this.defaultAccountId = this.user?.accounts.find((account) => {
return account.isDefault;
})?.id;
this.hasPermissionToCreateOrder = hasPermission(
this.user.permissions,
permissions.createOrder
);
this.hasPermissionToDeleteOrder = hasPermission(
this.user.permissions,
permissions.deleteOrder
);
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
@ -352,6 +339,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
} }
private openCreateTransactionDialog(aTransaction?: OrderModel): void { private openCreateTransactionDialog(aTransaction?: OrderModel): void {
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.updateUser(user);
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
data: { data: {
accounts: this.user?.accounts?.filter((account) => { accounts: this.user?.accounts?.filter((account) => {
@ -390,6 +383,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
this.router.navigate(['.'], { relativeTo: this.route }); this.router.navigate(['.'], { relativeTo: this.route });
}); });
});
} }
private openPositionDialog({ symbol }: { symbol: string }) { private openPositionDialog({ symbol }: { symbol: string }) {
@ -397,7 +391,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.get() .get()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.updateUser(user);
const dialogRef = this.dialog.open(PositionDetailDialog, { const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false, autoFocus: false,
@ -419,4 +413,21 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
}); });
}); });
} }
private updateUser(aUser: User) {
this.user = aUser;
this.defaultAccountId = this.user?.accounts.find((account) => {
return account.isDefault;
})?.id;
this.hasPermissionToCreateOrder = hasPermission(
this.user.permissions,
permissions.createOrder
);
this.hasPermissionToDeleteOrder = hasPermission(
this.user.permissions,
permissions.deleteOrder
);
}
} }

View File

@ -2,10 +2,11 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces';
export interface PortfolioDetails { export interface PortfolioDetails {
accounts: { accounts: {
[name: string]: { [id: string]: {
balance: number; balance: number;
currency: string; currency: string;
current: number; current: number;
name: string;
original: number; original: number;
}; };
}; };

View File

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