Compare commits
9 Commits
Author | SHA1 | Date | |
---|---|---|---|
4a75c6d483 | |||
bbe9183fb0 | |||
1b03ddc586 | |||
beb12637ce | |||
20358d9105 | |||
0e4c39d145 | |||
83ebacbb06 | |||
7c58c5fb7f | |||
f3271ab1ff |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
custom: ['https://www.buymeacoffee.com/ghostfolio']
|
21
CHANGELOG.md
21
CHANGELOG.md
@ -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.140.1 - 22.04.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added support for sub-labels in the value component
|
||||||
|
- Added a symbol profile overrides model for manual adjustments
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Reused the value component in the _Ghostfolio in Numbers_ section of the about page
|
||||||
|
- Persisted the savings rate in the _FIRE_ calculator
|
||||||
|
- Upgraded `yahoo-finance2` from version `2.3.0` to `2.3.1`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the calculation of the total value for sell and dividend activities in the create or edit transaction dialog
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:migrate`)
|
||||||
|
|
||||||
## 1.139.0 - 18.04.2022
|
## 1.139.0 - 18.04.2022
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -246,6 +246,8 @@ Ghostfolio is **100% free** and **open source**. We encourage and support an act
|
|||||||
|
|
||||||
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack channel](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg), tweet to [@ghostfolio\_](https://twitter.com/ghostfolio_) or send an e-mail to hi@ghostfol.io. We would love to hear from you.
|
||||||
|
|
||||||
|
If you like to support this project, get **[Ghostfolio Premium](https://ghostfol.io/pricing)** or **[Buy me a coffee](https://www.buymeacoffee.com/ghostfolio)**.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
© 2022 [Ghostfolio](https://ghostfol.io)
|
© 2022 [Ghostfolio](https://ghostfol.io)
|
||||||
|
@ -12,4 +12,8 @@ export class UpdateUserSettingDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
locale?: string;
|
locale?: string;
|
||||||
|
|
||||||
|
@IsNumber()
|
||||||
|
@IsOptional()
|
||||||
|
savingsRate?: number;
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,12 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
|||||||
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
import { Country } from '@ghostfolio/common/interfaces/country.interface';
|
||||||
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, Prisma, SymbolProfile } from '@prisma/client';
|
import {
|
||||||
|
DataSource,
|
||||||
|
Prisma,
|
||||||
|
SymbolProfile,
|
||||||
|
SymbolProfileOverrides
|
||||||
|
} from '@prisma/client';
|
||||||
import { continents, countries } from 'countries-list';
|
import { continents, countries } from 'countries-list';
|
||||||
|
|
||||||
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
import { ScraperConfiguration } from './data-provider/ghostfolio-scraper-api/interfaces/scraper-configuration.interface';
|
||||||
@ -36,6 +41,7 @@ export class SymbolProfileService {
|
|||||||
): Promise<EnhancedSymbolProfile[]> {
|
): Promise<EnhancedSymbolProfile[]> {
|
||||||
return this.prismaService.symbolProfile
|
return this.prismaService.symbolProfile
|
||||||
.findMany({
|
.findMany({
|
||||||
|
include: { SymbolProfileOverrides: true },
|
||||||
where: {
|
where: {
|
||||||
symbol: {
|
symbol: {
|
||||||
in: symbols
|
in: symbols
|
||||||
@ -45,14 +51,38 @@ export class SymbolProfileService {
|
|||||||
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
.then((symbolProfiles) => this.getSymbols(symbolProfiles));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSymbols(symbolProfiles: SymbolProfile[]): EnhancedSymbolProfile[] {
|
private getSymbols(
|
||||||
return symbolProfiles.map((symbolProfile) => ({
|
symbolProfiles: (SymbolProfile & {
|
||||||
...symbolProfile,
|
SymbolProfileOverrides: SymbolProfileOverrides;
|
||||||
countries: this.getCountries(symbolProfile),
|
})[]
|
||||||
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
|
): EnhancedSymbolProfile[] {
|
||||||
sectors: this.getSectors(symbolProfile),
|
return symbolProfiles.map((symbolProfile) => {
|
||||||
symbolMapping: this.getSymbolMapping(symbolProfile)
|
const item = {
|
||||||
}));
|
...symbolProfile,
|
||||||
|
countries: this.getCountries(symbolProfile),
|
||||||
|
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
|
||||||
|
sectors: this.getSectors(symbolProfile),
|
||||||
|
symbolMapping: this.getSymbolMapping(symbolProfile)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item.SymbolProfileOverrides) {
|
||||||
|
item.assetClass =
|
||||||
|
item.SymbolProfileOverrides.assetClass ?? item.assetClass;
|
||||||
|
item.assetSubClass =
|
||||||
|
item.SymbolProfileOverrides.assetSubClass ?? item.assetSubClass;
|
||||||
|
item.countries =
|
||||||
|
(item.SymbolProfileOverrides.sectors as unknown as Country[]) ??
|
||||||
|
item.countries;
|
||||||
|
item.name = item.SymbolProfileOverrides?.name ?? item.name;
|
||||||
|
item.sectors =
|
||||||
|
(item.SymbolProfileOverrides.sectors as unknown as Sector[]) ??
|
||||||
|
item.sectors;
|
||||||
|
|
||||||
|
delete item.SymbolProfileOverrides;
|
||||||
|
}
|
||||||
|
|
||||||
|
return item;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCountries(symbolProfile: SymbolProfile): Country[] {
|
private getCountries(symbolProfile: SymbolProfile): Country[] {
|
||||||
|
@ -109,38 +109,39 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<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?.activeUsers1d || '-' }}</h3>
|
<gf-value
|
||||||
<div class="h6 mb-0">
|
label="Active Users"
|
||||||
<span i18n>Active Users</span> <small class="text-muted"
|
size="large"
|
||||||
>(Last 24 hours)</small
|
subLabel="(Last 24 hours)"
|
||||||
>
|
[value]="statistics?.activeUsers1d ?? '-'"
|
||||||
</div>
|
></gf-value>
|
||||||
</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?.newUsers30d ?? '-' }}</h3>
|
<gf-value
|
||||||
<div class="h6 mb-0">
|
label="New Users"
|
||||||
<span i18n>New Users</span> <small class="text-muted"
|
size="large"
|
||||||
>(Last 30 days)</small
|
subLabel="(Last 30 days)"
|
||||||
>
|
[value]="statistics?.newUsers30d ?? '-'"
|
||||||
</div>
|
></gf-value>
|
||||||
</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?.activeUsers30d ?? '-' }}</h3>
|
<gf-value
|
||||||
<div class="h6 mb-0">
|
label="Active Users"
|
||||||
<span i18n>Active Users</span> <small class="text-muted"
|
size="large"
|
||||||
>(Last 30 days)</small
|
subLabel="(Last 30 days)"
|
||||||
>
|
[value]="statistics?.activeUsers30d ?? '-'"
|
||||||
</div>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<a
|
<a
|
||||||
class="d-block"
|
class="d-block"
|
||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
>
|
>
|
||||||
<h3 class="mb-0">
|
<gf-value
|
||||||
{{ statistics?.slackCommunityUsers ?? '-' }}
|
label="Users in Slack community"
|
||||||
</h3>
|
size="large"
|
||||||
<div class="h6 mb-0" i18n>Users in Slack community</div>
|
[value]="statistics?.slackCommunityUsers ?? '-'"
|
||||||
|
></gf-value>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
@ -148,10 +149,11 @@
|
|||||||
class="d-block"
|
class="d-block"
|
||||||
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
|
||||||
>
|
>
|
||||||
<h3 class="mb-0">
|
<gf-value
|
||||||
{{ statistics?.gitHubContributors ?? '-' }}
|
label="Contributors on GitHub"
|
||||||
</h3>
|
size="large"
|
||||||
<div class="h6 mb-0" i18n>Contributors on GitHub</div>
|
[value]="statistics?.gitHubContributors ?? '-'"
|
||||||
|
></gf-value>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
@ -159,8 +161,11 @@
|
|||||||
class="d-block"
|
class="d-block"
|
||||||
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
href="https://github.com/ghostfolio/ghostfolio/stargazers"
|
||||||
>
|
>
|
||||||
<h3 class="mb-0">{{ statistics?.gitHubStargazers ?? '-' }}</h3>
|
<gf-value
|
||||||
<div class="h6 mb-0" i18n>Stars on GitHub</div>
|
label="Stars on GitHub"
|
||||||
|
size="large"
|
||||||
|
[value]="statistics?.gitHubStargazers ?? '-'"
|
||||||
|
></gf-value>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { AboutPageRoutingModule } from './about-page-routing.module';
|
import { AboutPageRoutingModule } from './about-page-routing.module';
|
||||||
import { AboutPageComponent } from './about-page.component';
|
import { AboutPageComponent } from './about-page.component';
|
||||||
@ -12,6 +13,7 @@ import { AboutPageComponent } from './about-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
AboutPageRoutingModule,
|
AboutPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule
|
MatCardModule
|
||||||
],
|
],
|
||||||
|
@ -68,6 +68,13 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onSavingsRateChange(savingsRate: number) {
|
||||||
|
this.dataService
|
||||||
|
.putUserSetting({ savingsRate })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
[value]="withdrawalRatePerMonth?.toNumber()"
|
[value]="withdrawalRatePerMonth?.toNumber()"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
per month</span
|
per month</span
|
||||||
>, based on your investment of
|
>, based on your total assets of
|
||||||
<gf-value
|
<gf-value
|
||||||
class="d-inline-block"
|
class="d-inline-block"
|
||||||
[currency]="user?.settings?.baseCurrency"
|
[currency]="user?.settings?.baseCurrency"
|
||||||
@ -60,6 +60,8 @@
|
|||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[fireWealth]="fireWealth?.toNumber()"
|
[fireWealth]="fireWealth?.toNumber()"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
|
[savingsRate]="user?.settings?.savingsRate"
|
||||||
|
(savingsRateChanged)="onSavingsRateChange($event)"
|
||||||
></gf-fire-calculator>
|
></gf-fire-calculator>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,6 +46,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
public filteredLookupItemsObservable: Observable<LookupItem[]>;
|
public filteredLookupItemsObservable: Observable<LookupItem[]>;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
|
public total = 0;
|
||||||
public Validators = Validators;
|
public Validators = Validators;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -89,6 +90,25 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
unitPrice: [this.data.activity?.unitPrice, Validators.required]
|
unitPrice: [this.data.activity?.unitPrice, Validators.required]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.activityForm.valueChanges
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
if (
|
||||||
|
this.activityForm.controls['type'].value === 'BUY' ||
|
||||||
|
this.activityForm.controls['type'].value === 'ITEM'
|
||||||
|
) {
|
||||||
|
this.total =
|
||||||
|
this.activityForm.controls['quantity'].value *
|
||||||
|
this.activityForm.controls['unitPrice'].value +
|
||||||
|
this.activityForm.controls['fee'].value ?? 0;
|
||||||
|
} else {
|
||||||
|
this.total =
|
||||||
|
this.activityForm.controls['quantity'].value *
|
||||||
|
this.activityForm.controls['unitPrice'].value -
|
||||||
|
this.activityForm.controls['fee'].value ?? 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.filteredLookupItemsObservable = this.activityForm.controls[
|
this.filteredLookupItemsObservable = this.activityForm.controls[
|
||||||
'searchSymbol'
|
'searchSymbol'
|
||||||
].valueChanges.pipe(
|
].valueChanges.pipe(
|
||||||
@ -100,9 +120,11 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
const filteredLookupItemsObservable =
|
const filteredLookupItemsObservable =
|
||||||
this.dataService.fetchSymbols(query);
|
this.dataService.fetchSymbols(query);
|
||||||
|
|
||||||
filteredLookupItemsObservable.subscribe((filteredLookupItems) => {
|
filteredLookupItemsObservable
|
||||||
this.filteredLookupItems = filteredLookupItems;
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
});
|
.subscribe((filteredLookupItems) => {
|
||||||
|
this.filteredLookupItems = filteredLookupItems;
|
||||||
|
});
|
||||||
|
|
||||||
return filteredLookupItemsObservable;
|
return filteredLookupItemsObservable;
|
||||||
}
|
}
|
||||||
@ -111,45 +133,47 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.activityForm.controls['type'].valueChanges.subscribe((type: Type) => {
|
this.activityForm.controls['type'].valueChanges
|
||||||
if (type === 'ITEM') {
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
this.activityForm.controls['accountId'].removeValidators(
|
.subscribe((type: Type) => {
|
||||||
Validators.required
|
if (type === 'ITEM') {
|
||||||
);
|
this.activityForm.controls['accountId'].removeValidators(
|
||||||
this.activityForm.controls['accountId'].updateValueAndValidity();
|
Validators.required
|
||||||
this.activityForm.controls['currency'].setValue(
|
);
|
||||||
this.data.user.settings.baseCurrency
|
this.activityForm.controls['accountId'].updateValueAndValidity();
|
||||||
);
|
this.activityForm.controls['currency'].setValue(
|
||||||
this.activityForm.controls['dataSource'].removeValidators(
|
this.data.user.settings.baseCurrency
|
||||||
Validators.required
|
);
|
||||||
);
|
this.activityForm.controls['dataSource'].removeValidators(
|
||||||
this.activityForm.controls['dataSource'].updateValueAndValidity();
|
Validators.required
|
||||||
this.activityForm.controls['name'].setValidators(Validators.required);
|
);
|
||||||
this.activityForm.controls['name'].updateValueAndValidity();
|
this.activityForm.controls['dataSource'].updateValueAndValidity();
|
||||||
this.activityForm.controls['quantity'].setValue(1);
|
this.activityForm.controls['name'].setValidators(Validators.required);
|
||||||
this.activityForm.controls['searchSymbol'].removeValidators(
|
this.activityForm.controls['name'].updateValueAndValidity();
|
||||||
Validators.required
|
this.activityForm.controls['quantity'].setValue(1);
|
||||||
);
|
this.activityForm.controls['searchSymbol'].removeValidators(
|
||||||
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
Validators.required
|
||||||
} else {
|
);
|
||||||
this.activityForm.controls['accountId'].setValidators(
|
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
||||||
Validators.required
|
} else {
|
||||||
);
|
this.activityForm.controls['accountId'].setValidators(
|
||||||
this.activityForm.controls['accountId'].updateValueAndValidity();
|
Validators.required
|
||||||
this.activityForm.controls['dataSource'].setValidators(
|
);
|
||||||
Validators.required
|
this.activityForm.controls['accountId'].updateValueAndValidity();
|
||||||
);
|
this.activityForm.controls['dataSource'].setValidators(
|
||||||
this.activityForm.controls['dataSource'].updateValueAndValidity();
|
Validators.required
|
||||||
this.activityForm.controls['name'].removeValidators(
|
);
|
||||||
Validators.required
|
this.activityForm.controls['dataSource'].updateValueAndValidity();
|
||||||
);
|
this.activityForm.controls['name'].removeValidators(
|
||||||
this.activityForm.controls['name'].updateValueAndValidity();
|
Validators.required
|
||||||
this.activityForm.controls['searchSymbol'].setValidators(
|
);
|
||||||
Validators.required
|
this.activityForm.controls['name'].updateValueAndValidity();
|
||||||
);
|
this.activityForm.controls['searchSymbol'].setValidators(
|
||||||
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
Validators.required
|
||||||
}
|
);
|
||||||
});
|
this.activityForm.controls['searchSymbol'].updateValueAndValidity();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.activityForm.controls['type'].setValue(this.data.activity?.type);
|
this.activityForm.controls['type'].setValue(this.data.activity?.type);
|
||||||
|
|
||||||
|
@ -138,9 +138,9 @@
|
|||||||
<div class="d-flex" mat-dialog-actions>
|
<div class="d-flex" mat-dialog-actions>
|
||||||
<gf-value
|
<gf-value
|
||||||
class="flex-grow-1"
|
class="flex-grow-1"
|
||||||
[currency]="activityForm.controls['currency'].value"
|
[currency]="activityForm.controls['currency']?.value ?? data.user?.settings?.baseCurrency"
|
||||||
[locale]="data.user?.settings?.locale"
|
[locale]="data.user?.settings?.locale"
|
||||||
[value]="activityForm.controls['fee'].value + (activityForm.controls['quantity'].value * activityForm.controls['unitPrice'].value) ?? 0"
|
[value]="total"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
<div>
|
<div>
|
||||||
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
<button i18n mat-button type="button" (click)="onCancel()">Cancel</button>
|
||||||
|
@ -5,9 +5,11 @@ import {
|
|||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
|
EventEmitter,
|
||||||
Input,
|
Input,
|
||||||
OnChanges,
|
OnChanges,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormBuilder, FormControl } from '@angular/forms';
|
import { FormBuilder, FormControl } from '@angular/forms';
|
||||||
@ -40,6 +42,9 @@ export class FireCalculatorComponent
|
|||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() fireWealth: number;
|
@Input() fireWealth: number;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
|
@Input() savingsRate = 0;
|
||||||
|
|
||||||
|
@Output() savingsRateChanged = new EventEmitter<number>();
|
||||||
|
|
||||||
@ViewChild('chartCanvas') chartCanvas;
|
@ViewChild('chartCanvas') chartCanvas;
|
||||||
|
|
||||||
@ -73,7 +78,7 @@ export class FireCalculatorComponent
|
|||||||
|
|
||||||
this.calculatorForm.setValue({
|
this.calculatorForm.setValue({
|
||||||
annualInterestRate: 5,
|
annualInterestRate: 5,
|
||||||
paymentPerPeriod: 500,
|
paymentPerPeriod: this.savingsRate,
|
||||||
principalInvestmentAmount: 0,
|
principalInvestmentAmount: 0,
|
||||||
time: 10
|
time: 10
|
||||||
});
|
});
|
||||||
@ -83,15 +88,28 @@ export class FireCalculatorComponent
|
|||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.calculatorForm
|
||||||
|
.get('paymentPerPeriod')
|
||||||
|
.valueChanges.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((savingsRate) => {
|
||||||
|
this.savingsRateChanged.emit(savingsRate);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngAfterViewInit() {
|
public ngAfterViewInit() {
|
||||||
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
|
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Wait for the chartCanvas
|
// Wait for the chartCanvas
|
||||||
this.calculatorForm.patchValue({
|
this.calculatorForm.patchValue(
|
||||||
principalInvestmentAmount: this.fireWealth
|
{
|
||||||
});
|
principalInvestmentAmount: this.fireWealth,
|
||||||
|
paymentPerPeriod: this.savingsRate ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitEvent: false
|
||||||
|
}
|
||||||
|
);
|
||||||
this.calculatorForm.get('principalInvestmentAmount').disable();
|
this.calculatorForm.get('principalInvestmentAmount').disable();
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
@ -103,9 +121,15 @@ export class FireCalculatorComponent
|
|||||||
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
|
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Wait for the chartCanvas
|
// Wait for the chartCanvas
|
||||||
this.calculatorForm.patchValue({
|
this.calculatorForm.patchValue(
|
||||||
principalInvestmentAmount: this.fireWealth
|
{
|
||||||
});
|
principalInvestmentAmount: this.fireWealth,
|
||||||
|
paymentPerPeriod: this.savingsRate ?? 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
emitEvent: false
|
||||||
|
}
|
||||||
|
);
|
||||||
this.calculatorForm.get('principalInvestmentAmount').disable();
|
this.calculatorForm.get('principalInvestmentAmount').disable();
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
@ -152,7 +176,7 @@ export class FireCalculatorComponent
|
|||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
return `Total Amount: ${new Intl.NumberFormat(this.locale, {
|
return `Total: ${new Intl.NumberFormat(this.locale, {
|
||||||
currency: this.currency,
|
currency: this.currency,
|
||||||
currencyDisplay: 'code',
|
currencyDisplay: 'code',
|
||||||
style: 'currency'
|
style: 'currency'
|
||||||
|
@ -10,14 +10,14 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<div
|
<div
|
||||||
*ngIf="isPercent"
|
*ngIf="isPercent"
|
||||||
class="mb-0"
|
class="mb-0 value"
|
||||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||||
>
|
>
|
||||||
{{ formattedValue }}%
|
{{ formattedValue }}%
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
*ngIf="!isPercent"
|
*ngIf="!isPercent"
|
||||||
class="mb-0"
|
class="mb-0 value"
|
||||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||||
>
|
>
|
||||||
<ng-container *ngIf="value === null">
|
<ng-container *ngIf="value === null">
|
||||||
@ -36,7 +36,7 @@
|
|||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container *ngIf="isString">
|
<ng-container *ngIf="isString">
|
||||||
<div
|
<div
|
||||||
class="mb-0 text-truncate"
|
class="mb-0 text-truncate value"
|
||||||
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
[ngClass]="{ h2: size === 'large', h4: size === 'medium' }"
|
||||||
>
|
>
|
||||||
{{ formattedValue | titlecase }}
|
{{ formattedValue | titlecase }}
|
||||||
@ -45,7 +45,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<ng-container *ngIf="label">
|
<ng-container *ngIf="label">
|
||||||
<div *ngIf="size === 'large'">
|
<div *ngIf="size === 'large'">
|
||||||
{{ label }}
|
<span class="h6">{{ label }}</span>
|
||||||
|
<span *ngIf="subLabel" class="text-muted"> {{ subLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
<small *ngIf="size !== 'large'">
|
<small *ngIf="size !== 'large'">
|
||||||
{{ label }}
|
{{ label }}
|
||||||
|
@ -2,4 +2,8 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
|
|
||||||
|
.h2 {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ export class ValueComponent implements OnChanges {
|
|||||||
@Input() position = '';
|
@Input() position = '';
|
||||||
@Input() precision: number | undefined;
|
@Input() precision: number | undefined;
|
||||||
@Input() size: 'large' | 'medium' | 'small' = 'small';
|
@Input() size: 'large' | 'medium' | 'small' = 'small';
|
||||||
|
@Input() subLabel = '';
|
||||||
@Input() value: number | string = '';
|
@Input() value: number | string = '';
|
||||||
|
|
||||||
public absoluteValue = 0;
|
public absoluteValue = 0;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.139.0",
|
"version": "1.140.1",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -118,7 +118,7 @@
|
|||||||
"tslib": "2.0.0",
|
"tslib": "2.0.0",
|
||||||
"twitter-api-v2": "1.10.3",
|
"twitter-api-v2": "1.10.3",
|
||||||
"uuid": "8.3.2",
|
"uuid": "8.3.2",
|
||||||
"yahoo-finance2": "2.3.0",
|
"yahoo-finance2": "2.3.1",
|
||||||
"zone.js": "0.11.4"
|
"zone.js": "0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SymbolProfileOverrides" (
|
||||||
|
"assetClass" "AssetClass",
|
||||||
|
"assetSubClass" "AssetSubClass",
|
||||||
|
"countries" JSONB,
|
||||||
|
"name" TEXT,
|
||||||
|
"sectors" JSONB,
|
||||||
|
"symbolProfileId" TEXT NOT NULL,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "SymbolProfileOverrides_pkey" PRIMARY KEY ("symbolProfileId")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "SymbolProfileOverrides" ADD CONSTRAINT "SymbolProfileOverrides_symbolProfileId_fkey" FOREIGN KEY ("symbolProfileId") REFERENCES "SymbolProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "AssetSubClass" ADD VALUE 'COMMODITY';
|
@ -112,25 +112,37 @@ model Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model SymbolProfile {
|
model SymbolProfile {
|
||||||
assetClass AssetClass?
|
assetClass AssetClass?
|
||||||
assetSubClass AssetSubClass?
|
assetSubClass AssetSubClass?
|
||||||
countries Json?
|
countries Json?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
currency String
|
currency String
|
||||||
dataSource DataSource
|
dataSource DataSource
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
name String?
|
name String?
|
||||||
Order Order[]
|
Order Order[]
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
scraperConfiguration Json?
|
scraperConfiguration Json?
|
||||||
sectors Json?
|
sectors Json?
|
||||||
symbol String
|
symbol String
|
||||||
symbolMapping Json?
|
symbolMapping Json?
|
||||||
url String?
|
SymbolProfileOverrides SymbolProfileOverrides?
|
||||||
|
url String?
|
||||||
|
|
||||||
@@unique([dataSource, symbol])
|
@@unique([dataSource, symbol])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SymbolProfileOverrides {
|
||||||
|
assetClass AssetClass?
|
||||||
|
assetSubClass AssetSubClass?
|
||||||
|
countries Json?
|
||||||
|
name String?
|
||||||
|
sectors Json?
|
||||||
|
SymbolProfile SymbolProfile @relation(fields: [symbolProfileId], references: [id])
|
||||||
|
symbolProfileId String @id
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
model Subscription {
|
model Subscription {
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
expiresAt DateTime
|
expiresAt DateTime
|
||||||
@ -176,6 +188,7 @@ enum AssetClass {
|
|||||||
|
|
||||||
enum AssetSubClass {
|
enum AssetSubClass {
|
||||||
BOND
|
BOND
|
||||||
|
COMMODITY
|
||||||
CRYPTOCURRENCY
|
CRYPTOCURRENCY
|
||||||
ETF
|
ETF
|
||||||
MUTUALFUND
|
MUTUALFUND
|
||||||
|
@ -18836,10 +18836,10 @@ y18n@^5.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
|
||||||
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
|
||||||
|
|
||||||
yahoo-finance2@2.3.0:
|
yahoo-finance2@2.3.1:
|
||||||
version "2.3.0"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.0.tgz#81bd76732dfd38aa5d7019a97caf0f938c0127c2"
|
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.3.1.tgz#d2cffbef78f6974e4e6a40487cc08ab133dc9fc5"
|
||||||
integrity sha512-7oj8n/WJH9MtX+q99WbHdjEVPdobTX8IyYjg7v4sDOh4f9ByT2Frxmp+Uj+rctrO0EiiD9QWTuwV4h8AemGuCg==
|
integrity sha512-QTXiiWgfrpVbSylchBgLqESZz+8+SyyDSqntjfZHxMIHa6d14xq+biNNDIeYd5SylcZ9Vt4zLmZXHN7EdLM1pA==
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv "8.10.0"
|
ajv "8.10.0"
|
||||||
ajv-formats "2.1.1"
|
ajv-formats "2.1.1"
|
||||||
|
Reference in New Issue
Block a user