Compare commits

...

9 Commits

Author SHA1 Message Date
bd4608e521 Release 1.138.0 (#838) 2022-04-16 21:03:28 +02:00
0d8362ca8f Feature/separate deposit and savings in fire calculator (#837)
* Separate deposit and savings

* Update changelog
2022-04-16 21:01:55 +02:00
638ae3f7fa Feature/add boringly getting rich guide to resources page (#836)
* Add "Boringly Getting Rich" guide

* Update changelog
2022-04-16 15:35:31 +02:00
6e7cf0380b Feature/export single draft (#835)
* Export single draft

* Update changelog
2022-04-16 11:33:01 +02:00
ec2ecab751 Clean up name (#834) 2022-04-16 10:21:32 +02:00
598fe41b8c Release 1.137.0 (#831) 2022-04-15 18:58:33 +02:00
ba7c98d325 Add test case for BUY and SELL (partially) (#826)
* Add test case for BUY and SELL (partially)

* Fix investment calculation for sell activities

* Do not show total value if sell activity

* Update changelog
2022-04-15 18:56:23 +02:00
65e062ad26 Simplify search (#828)
* Simplify search

* Update changelog
2022-04-15 12:39:33 +02:00
8526b5a027 Feature/export draft activities as ics (#830)
* Export draft activities as ICS

* Update changelog
2022-04-15 10:53:40 +02:00
21 changed files with 477 additions and 167 deletions

View File

@ -5,6 +5,31 @@ 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.138.0 - 16.04.2022
### Added
- Added support to export a single future activity (draft) as an `.ics` file
- Added the _Boringly Getting Rich_ guide to the resources section
### Changed
- Separated the deposit and savings in the chart of the the _FIRE_ calculator
## 1.137.0 - 15.04.2022
### Added
- Added support to export future activities (drafts) as an `.ics` file
### Changed
- Migrated the search functionality to `yahoo-finance2`
### Fixed
- Fixed an issue in the average price / investment calculation for sell activities
## 1.136.0 - 13.04.2022
### Changed

View File

@ -42,6 +42,7 @@ export class ExportService {
accountId,
date,
fee,
id,
quantity,
SymbolProfile,
type,
@ -49,13 +50,14 @@ export class ExportService {
}) => {
return {
accountId,
date,
fee,
id,
quantity,
type,
unitPrice,
currency: SymbolProfile.currency,
dataSource: SymbolProfile.dataSource,
date: date.toISOString(),
symbol: type === 'ITEM' ? SymbolProfile.name : SymbolProfile.symbol
};
}

View File

@ -20,6 +20,13 @@ function mockGetValue(symbol: string, date: Date) {
return { marketPrice: 0 };
case 'NOVN.SW':
if (isSameDay(parseDate('2022-04-11'), date)) {
return { marketPrice: 87.8 };
}
return { marketPrice: 0 };
default:
return { marketPrice: 0 };
}

View File

@ -0,0 +1,96 @@
import { CurrentRateService } from '@ghostfolio/api/app/portfolio/current-rate.service';
import { parseDate } from '@ghostfolio/common/helper';
import Big from 'big.js';
import { CurrentRateServiceMock } from './current-rate.service.mock';
import { PortfolioCalculator } from './portfolio-calculator';
jest.mock('@ghostfolio/api/app/portfolio/current-rate.service', () => {
return {
// eslint-disable-next-line @typescript-eslint/naming-convention
CurrentRateService: jest.fn().mockImplementation(() => {
return CurrentRateServiceMock;
})
};
});
describe('PortfolioCalculator', () => {
let currentRateService: CurrentRateService;
beforeEach(() => {
currentRateService = new CurrentRateService(null, null, null);
});
describe('get current positions', () => {
it.only('with BALN.SW buy and sell', async () => {
const portfolioCalculator = new PortfolioCalculator({
currentRateService,
currency: 'CHF',
orders: [
{
currency: 'CHF',
date: '2022-03-07',
dataSource: 'YAHOO',
fee: new Big(1.3),
name: 'Novartis AG',
quantity: new Big(2),
symbol: 'NOVN.SW',
type: 'BUY',
unitPrice: new Big(75.8)
},
{
currency: 'CHF',
date: '2022-04-08',
dataSource: 'YAHOO',
fee: new Big(2.95),
name: 'Novartis AG',
quantity: new Big(1),
symbol: 'NOVN.SW',
type: 'SELL',
unitPrice: new Big(85.73)
}
]
});
portfolioCalculator.computeTransactionPoints();
const spy = jest
.spyOn(Date, 'now')
.mockImplementation(() => parseDate('2022-04-11').getTime());
const currentPositions = await portfolioCalculator.getCurrentPositions(
parseDate('2022-03-07')
);
spy.mockRestore();
expect(currentPositions).toEqual({
currentValue: new Big('87.8'),
errors: [],
grossPerformance: new Big('21.93'),
grossPerformancePercentage: new Big('0.14465699208443271768'),
hasErrors: false,
netPerformance: new Big('17.68'),
netPerformancePercentage: new Big('0.11662269129287598945'),
positions: [
{
averagePrice: new Big('75.80'),
currency: 'CHF',
dataSource: 'YAHOO',
firstBuyDate: '2022-03-07',
grossPerformance: new Big('21.93'),
grossPerformancePercentage: new Big('0.14465699208443271768'),
investment: new Big('75.80'),
netPerformance: new Big('17.68'),
netPerformancePercentage: new Big('0.11662269129287598945'),
marketPrice: 87.8,
quantity: new Big('1'),
symbol: 'NOVN.SW',
transactionCount: 2
}
],
totalInvestment: new Big('75.80')
});
});
});
});

View File

@ -77,17 +77,30 @@ export class PortfolioCalculator {
const newQuantity = order.quantity
.mul(factor)
.plus(oldAccumulatedSymbol.quantity);
let investment = new Big(0);
if (newQuantity.gt(0)) {
if (order.type === 'BUY') {
investment = oldAccumulatedSymbol.investment.plus(
order.quantity.mul(unitPrice)
);
} else if (order.type === 'SELL') {
const averagePrice = oldAccumulatedSymbol.investment.div(
oldAccumulatedSymbol.quantity
);
investment = oldAccumulatedSymbol.investment.minus(
order.quantity.mul(averagePrice)
);
}
}
currentTransactionPointItem = {
investment,
currency: order.currency,
dataSource: order.dataSource,
fee: order.fee.plus(oldAccumulatedSymbol.fee),
firstBuyDate: oldAccumulatedSymbol.firstBuyDate,
investment: newQuantity.eq(0)
? new Big(0)
: unitPrice
.mul(order.quantity)
.mul(factor)
.plus(oldAccumulatedSymbol.investment),
quantity: newQuantity,
symbol: order.symbol,
transactionCount: oldAccumulatedSymbol.transactionCount + 1

View File

@ -17,8 +17,6 @@ import { format, subMonths, subWeeks, subYears } from 'date-fns';
@Injectable()
export class RakutenRapidApiService implements DataProviderInterface {
public static FEAR_AND_GREED_INDEX_NAME = 'Fear & Greed Index';
public constructor(
private readonly configurationService: ConfigurationService,
private readonly prismaService: PrismaService

View File

@ -16,7 +16,6 @@ import {
DataSource,
SymbolProfile
} from '@prisma/client';
import * as bent from 'bent';
import Big from 'big.js';
import { countries } from 'countries-list';
import { addDays, format, isSameDay } from 'date-fns';
@ -25,8 +24,6 @@ import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-ifa
@Injectable()
export class YahooFinanceService implements DataProviderInterface {
private readonly yahooFinanceHostname = 'https://query1.finance.yahoo.com';
public constructor(
private readonly cryptocurrencyService: CryptocurrencyService
) {}
@ -244,16 +241,7 @@ export class YahooFinanceService implements DataProviderInterface {
const items: LookupItem[] = [];
try {
const get = bent(
`${this.yahooFinanceHostname}/v1/finance/search?q=${encodeURIComponent(
aQuery
)}&lang=en-US&region=US&quotesCount=8&newsCount=0&enableFuzzyQuery=false&quotesQueryId=tss_match_phrase_query&multiQuoteQueryId=multi_quote_single_token_query&newsQueryId=news_cie_vespa&enableCb=true&enableNavLinks=false&enableEnhancedTrivialQuery=true`,
'GET',
'json',
200
);
const searchResult = await get();
const searchResult = await yahooFinance.search(aQuery);
const quotes = searchResult.quotes
.filter((quote) => {
@ -279,20 +267,24 @@ export class YahooFinanceService implements DataProviderInterface {
return true;
});
const marketData = await this.getQuotes(
const marketData = await yahooFinance.quote(
quotes.map(({ symbol }) => {
return symbol;
})
);
for (const [symbol, value] of Object.entries(marketData)) {
const quote = quotes.find((currentQuote: any) => {
return currentQuote.symbol === symbol;
for (const marketDataItem of marketData) {
const quote = quotes.find((currentQuote) => {
return currentQuote.symbol === marketDataItem.symbol;
});
const symbol = this.convertFromYahooFinanceSymbol(
marketDataItem.symbol
);
items.push({
symbol,
currency: value.currency,
currency: marketDataItem.currency,
dataSource: this.getName(),
name: quote?.longname || quote?.shortname || symbol
});

View File

@ -194,16 +194,17 @@
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #accountMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onUpdateAccount(element)">
Edit
<button mat-menu-item (click)="onUpdateAccount(element)">
<ion-icon class="mr-2" name="create-outline"></ion-icon>
<span i18n>Edit</span>
</button>
<button
i18n
mat-menu-item
[disabled]="element.isDefault || element.Order?.length > 0"
(click)="onDeleteAccount(element.id)"
>
Delete
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span>
</button>
</mat-menu>
</td>

View File

@ -68,12 +68,12 @@
</button>
<mat-menu #accountMenu="matMenu" xPosition="before">
<button
i18n
mat-menu-item
[disabled]="userItem.id === user?.id"
(click)="onDeleteUser(userItem.id)"
>
Delete
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span>
</button>
</mat-menu>
</td>

View File

@ -211,14 +211,14 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile(
data,
`ghostfolio-export-${this.SymbolProfile?.symbol}-${format(
downloadAsFile({
content: data,
fileName: `ghostfolio-export-${this.SymbolProfile?.symbol}-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.json`,
'text/plain'
);
format: 'json'
});
});
}

View File

@ -7,6 +7,7 @@ import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interf
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service';
import { IcsService } from '@ghostfolio/client/services/ics/ics.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -50,6 +51,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private icsService: IcsService,
private impersonationStorageService: ImpersonationStorageService,
private importTransactionsService: ImportTransactionsService,
private route: ActivatedRoute,
@ -152,14 +154,36 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.fetchExport(activityIds)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile(
data,
`ghostfolio-export-${format(
for (const activity of data.activities) {
delete activity.id;
}
downloadAsFile({
content: data,
fileName: `ghostfolio-export-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.json`,
'text/plain'
);
format: 'json'
});
});
}
public onExportDrafts(activityIds?: string[]) {
this.dataService
.fetchExport(activityIds)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile({
content: this.icsService.transformActivitiesToIcsContent(
data.activities
),
contentType: 'text/calendar',
fileName: `ghostfolio-draft${
data.activities.length > 1 ? 's' : ''
}-${format(parseISO(data.meta.date), 'yyyyMMddHHmm')}.ics`,
format: 'string'
});
});
}

View File

@ -15,6 +15,7 @@
(activityToClone)="onCloneTransaction($event)"
(activityToUpdate)="onUpdateTransaction($event)"
(export)="onExport($event)"
(exportDrafts)="onExportDrafts($event)"
(import)="onImport()"
></gf-activities-table>
</div>

View File

@ -1,105 +1,117 @@
<div class="container">
<div class="row">
<div class="col">
<h3 class="d-flex justify-content-center mb-3" i18n>Resources</h3>
<mat-card class="mb-3">
<mat-card-content>
<h4 class="mb-3">Market</h4>
<div class="mb-5">
<div class="mb-4 media">
<div class="media-body">
<h5 class="mt-0">Fear & Greed Index</h5>
<div class="mb-1">
The fear and greed index was developed by <i>CNNMoney</i> to
measure the primary emotions (fear and greed) that influence
how much investors are willing to pay for stocks.
</div>
<div>
<a
href="https://money.cnn.com/data/fear-and-greed/"
target="_blank"
>Fear & Greed Index →</a
>
</div>
</div>
<h1 class="d-flex h3 justify-content-center mb-3" i18n>Resources</h1>
<h2 class="h4 mb-3">Guides</h2>
<div class="mb-5">
<div class="mb-4 media">
<div class="media-body">
<h3 class="h5 mt-0">Boringly Getting Rich</h3>
<div class="mb-1">
The <i>Boringly Getting Rich</i> guide supports you to get started
with investing. It introduces a strategy utilizing a broadly
diversified, low-cost portfolio excluding the risks of individual
stocks.
</div>
<div class="media">
<div class="media-body">
<h5 class="mt-0">Inflation Chart</h5>
<div class="mb-1">
Inflation Chart helps you find the intrinsic value of stock
markets, stock prices, goods and services by adjusting them to
the amount of the money supply (M0, M1, M2) or price of other
goods (food or oil).
</div>
<div>
<a href="https://inflationchart.com" target="_blank"
>Inflation Chart →</a
>
</div>
</div>
<div>
<a href="https://herget.me/investing-guide" target="_blank"
>Boringly Getting Rich →</a
>
</div>
</div>
<h4 class="mb-3">Glossary</h4>
<div>
<div class="mb-4 media">
<!--<img src="" class="mr-3" />-->
<div class="media-body">
<h5 class="mt-0">Buy and Hold</h5>
<div class="mb-1">
Buy and hold is a passive investment strategy where you buy
assets and hold them for a long period regardless of
fluctuations in the market.
</div>
<div>
<a
href="https://www.investopedia.com/terms/b/buyandhold.asp"
target="_blank"
>Buy and Hold →</a
>
</div>
</div>
</div>
</div>
<h2 class="h4 mb-3">Market</h2>
<div class="mb-5">
<div class="mb-4 media">
<div class="media-body">
<h3 class="h5 mt-0">Fear & Greed Index</h3>
<div class="mb-1">
The fear and greed index was developed by <i>CNNMoney</i> to
measure the primary emotions (fear and greed) that influence how
much investors are willing to pay for stocks.
</div>
<div class="mb-4 media">
<!--<img src="" class="mr-3" />-->
<div class="media-body">
<h5 class="mt-0">Dollar-Cost Averaging (DCA)</h5>
<div class="mb-1">
Dollar-cost averaging is an investment strategy where you
split the total amount to be invested across periodic
purchases of a target asset to reduce the impact of volatility
on the overall purchase.
</div>
<div>
<a
href="https://www.investopedia.com/terms/d/dollarcostaveraging.asp"
target="_blank"
>Dollar-Cost Averaging →</a
>
</div>
</div>
</div>
<div class="media">
<!--<img src="" class="mr-3" />-->
<div class="media-body">
<h5 class="mt-0">Financial Independence</h5>
<div class="mb-1">
Financial independence is the status of having enough income,
for example with a passive income like dividends, to cover
your living expenses for the rest of your life.
</div>
<div>
<a
href="https://en.wikipedia.org/wiki/Financial_independence"
target="_blank"
>Financial Independence →</a
>
</div>
</div>
<div>
<a
href="https://money.cnn.com/data/fear-and-greed/"
target="_blank"
>Fear & Greed Index →</a
>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="media">
<div class="media-body">
<h3 class="h5 mt-0">Inflation Chart</h3>
<div class="mb-1">
Inflation Chart helps you find the intrinsic value of stock
markets, stock prices, goods and services by adjusting them to the
amount of the money supply (M0, M1, M2) or price of other goods
(food or oil).
</div>
<div>
<a href="https://inflationchart.com" target="_blank"
>Inflation Chart →</a
>
</div>
</div>
</div>
</div>
<h2 class="h4 mb-3">Glossary</h2>
<div>
<div class="mb-4 media">
<div class="media-body">
<h3 class="h5 mt-0">Buy and Hold</h3>
<div class="mb-1">
Buy and hold is a passive investment strategy where you buy assets
and hold them for a long period regardless of fluctuations in the
market.
</div>
<div>
<a
href="https://www.investopedia.com/terms/b/buyandhold.asp"
target="_blank"
>Buy and Hold →</a
>
</div>
</div>
</div>
<div class="mb-4 media">
<div class="media-body">
<h3 class="h5 mt-0">Dollar-Cost Averaging (DCA)</h3>
<div class="mb-1">
Dollar-cost averaging is an investment strategy where you split
the total amount to be invested across periodic purchases of a
target asset to reduce the impact of volatility on the overall
purchase.
</div>
<div>
<a
href="https://www.investopedia.com/terms/d/dollarcostaveraging.asp"
target="_blank"
>Dollar-Cost Averaging →</a
>
</div>
</div>
</div>
<div class="media">
<div class="media-body">
<h3 class="h5 mt-0">Financial Independence</h3>
<div class="mb-1">
Financial independence is the status of having enough income, for
example with a passive income like dividends, to cover your living
expenses for the rest of your life.
</div>
<div>
<a
href="https://en.wikipedia.org/wiki/Financial_independence"
target="_blank"
>Financial Independence →</a
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -2,14 +2,12 @@
color: rgb(var(--dark-primary-text));
display: block;
.mat-card {
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}

View File

@ -0,0 +1,60 @@
import { Injectable } from '@angular/core';
import { capitalize } from '@ghostfolio/common/helper';
import { Export } from '@ghostfolio/common/interfaces';
import { Type } from '@prisma/client';
import { format, parseISO } from 'date-fns';
@Injectable({
providedIn: 'root'
})
export class IcsService {
private readonly ICS_DATE_FORMAT = 'yyyyMMdd';
private readonly ICS_LINE_BREAK = '\r\n';
public constructor() {}
public transformActivitiesToIcsContent(
aActivities: Export['activities']
): string {
const header = [
'BEGIN:VCALENDAR',
'VERSION:2.0',
'PRODID:-//Ghostfolio//NONSGML v1.0//EN'
];
const events = aActivities.map((activity) => {
return this.getEvent({
date: parseISO(activity.date),
id: activity.id,
symbol: activity.symbol,
type: activity.type
});
});
const footer = ['END:VCALENDAR'];
return [...header, ...events, ...footer].join(this.ICS_LINE_BREAK);
}
private getEvent({
date,
id,
symbol,
type
}: {
date: Date;
id: string;
symbol: string;
type: Type;
}) {
const today = format(new Date(), this.ICS_DATE_FORMAT);
return [
'BEGIN:VEVENT',
`UID:${id}`,
`DTSTAMP:${today}T000000`,
`DTSTART;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`,
`DTEND;VALUE=DATE:${format(date, this.ICS_DATE_FORMAT)}`,
`SUMMARY:${capitalize(type)} ${symbol}`,
'END:VEVENT'
].join(this.ICS_LINE_BREAK);
}
}

View File

@ -12,17 +12,28 @@ export function decodeDataSource(encodedDataSource: string) {
return Buffer.from(encodedDataSource, 'hex').toString();
}
export function downloadAsFile(
aContent: unknown,
aFileName: string,
aContentType: string
) {
export function downloadAsFile({
content,
contentType = 'text/plain',
fileName,
format
}: {
content: unknown;
contentType?: string;
fileName: string;
format: 'json' | 'string';
}) {
const a = document.createElement('a');
const file = new Blob([JSON.stringify(aContent, undefined, ' ')], {
type: aContentType
if (format === 'json') {
content = JSON.stringify(content, undefined, ' ');
}
const file = new Blob([<string>content], {
type: contentType
});
a.href = URL.createObjectURL(file);
a.download = aFileName;
a.download = fileName;
a.click();
}

View File

@ -5,5 +5,14 @@ export interface Export {
date: string;
version: string;
};
activities: Partial<Order>[];
activities: (Omit<
Order,
| 'accountUserId'
| 'createdAt'
| 'date'
| 'isDraft'
| 'symbolProfileId'
| 'updatedAt'
| 'userId'
> & { date: string; symbol: string })[];
}

View File

@ -271,6 +271,7 @@
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
*ngIf="totalValue !== null"
[isAbsolute]="true"
[isCurrency]="true"
[locale]="locale"
@ -302,6 +303,7 @@
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
*ngIf="totalValue !== null"
[isAbsolute]="true"
[isCurrency]="true"
[locale]="locale"
@ -356,11 +358,22 @@
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="dataSource.data.length === 0"
(click)="onExport()"
>
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="!hasDrafts"
(click)="onExportDrafts()"
>
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Drafts as ICS</span>
</button>
</mat-menu>
</th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
@ -374,14 +387,25 @@
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #activityMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onUpdateActivity(element)">
Edit
<button mat-menu-item (click)="onUpdateActivity(element)">
<ion-icon class="mr-2" name="create-outline"></ion-icon>
<span i18n>Edit</span>
</button>
<button i18n mat-menu-item (click)="onCloneActivity(element)">
Clone
<button mat-menu-item (click)="onCloneActivity(element)">
<ion-icon class="mr-2" name="copy-outline"></ion-icon>
<span i18n>Clone</span>
</button>
<button i18n mat-menu-item (click)="onDeleteActivity(element.id)">
Delete
<button
mat-menu-item
[disabled]="!element.isDraft"
(click)="onExportDraft(element.id)"
>
<ion-icon class="mr-2" name="calendar-clear-outline"></ion-icon>
<span i18n>Export Draft as ICS</span>
</button>
<button mat-menu-item (click)="onDeleteActivity(element.id)">
<ion-icon class="mr-2" name="trash-outline"></ion-icon>
<span i18n>Delete</span>
</button>
</mat-menu>
</td>

View File

@ -56,6 +56,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@Output() export = new EventEmitter<string[]>();
@Output() exportDrafts = new EventEmitter<string[]>();
@Output() import = new EventEmitter<void>();
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
@ -68,6 +69,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
public endOfToday = endOfToday();
public filters$: Subject<string[]> = new BehaviorSubject([]);
public filters: Observable<string[]> = this.filters$.asObservable();
public hasDrafts = false;
public isAfter = isAfter;
public isLoading = true;
public isUUID = isUUID;
@ -198,6 +200,22 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
}
}
public onExportDraft(aActivityId: string) {
this.exportDrafts.emit([aActivityId]);
}
public onExportDrafts() {
this.exportDrafts.emit(
this.dataSource.filteredData
.filter((activity) => {
return activity.isDraft;
})
.map((activity) => {
return activity.id;
})
);
}
public onImport() {
this.import.emit();
}
@ -234,6 +252,9 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
this.filters$.next(this.allFilters);
this.hasDrafts = this.dataSource.data.some((activity) => {
return activity.isDraft === true;
});
this.totalFees = this.getTotalFees();
this.totalValue = this.getTotalValue();
}
@ -308,7 +329,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
if (activity.type === 'BUY' || activity.type === 'ITEM') {
totalValue = totalValue.plus(activity.valueInBaseCurrency);
} else if (activity.type === 'SELL') {
totalValue = totalValue.minus(activity.valueInBaseCurrency);
return null;
}
} else {
return null;

View File

@ -11,7 +11,7 @@ import {
ViewChild
} from '@angular/core';
import { FormBuilder, FormControl } from '@angular/forms';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import { primaryColorRgb } from '@ghostfolio/common/config';
import { transformTickToAbbreviation } from '@ghostfolio/common/helper';
import {
BarController,
@ -21,6 +21,7 @@ import {
LinearScale,
Tooltip
} from 'chart.js';
import * as Color from 'color';
import { isNumber } from 'lodash';
import { Subject, takeUntil } from 'rxjs';
@ -211,16 +212,30 @@ export class FireCalculatorComponent
labels.push(year);
}
const datasetDeposit = {
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
data: [],
label: 'Deposit'
};
const datasetInterest = {
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
backgroundColor: Color(
`rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`
)
.lighten(0.5)
.hex(),
data: [],
label: 'Interest'
};
const datasetPrincipal = {
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
const datasetSavings = {
backgroundColor: Color(
`rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`
)
.lighten(0.25)
.hex(),
data: [],
label: 'Principal'
label: 'Savings'
};
for (let period = 1; period <= t; period++) {
@ -232,8 +247,9 @@ export class FireCalculatorComponent
r
});
datasetPrincipal.data.push(principal.toNumber());
datasetDeposit.data.push(this.fireWealth);
datasetInterest.data.push(interest.toNumber());
datasetSavings.data.push(principal.minus(this.fireWealth).toNumber());
if (period === t) {
this.projectedTotalAmount = totalAmount.toNumber();
@ -242,7 +258,7 @@ export class FireCalculatorComponent
return {
labels,
datasets: [datasetPrincipal, datasetInterest]
datasets: [datasetDeposit, datasetSavings, datasetInterest]
};
}
}

View File

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