Compare commits

..

23 Commits

Author SHA1 Message Date
de2255f9ba Release 2.15.0 (#2548) 2023-10-26 19:38:44 +02:00
e4ec5f213e Feature/extend personal finance tools pages 20231026 (#2547)
* Improve wording (vs)

* Improve breadcrumb (vs)

* Add Beanvest

* Add Wealthica

* Update locales
2023-10-26 19:36:34 +02:00
f3c2fb853d Upgrade to Nx 17 (#2545)
* Upgrade to Nx 17

* Update changelog
2023-10-26 19:35:56 +02:00
f5ad1d2d24 Feature/set validation rule to positive number in cash balance transfer (#2544)
* Add validation rule (positive number)

* Update changelog
2023-10-26 19:19:43 +02:00
0af37ca1d7 Extend asset profile dialog form (#2535)
* Extend asset profile dialog form

* Update changelog
2023-10-25 20:28:51 +02:00
2992a0da4c Verify current benchmark before loading it (#2541)
* Verify current benchmark before loading it

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-25 20:27:41 +02:00
2dcc7e161c Improve validation of activities import (#2496)
* Improve validation of activities import: expects positive values for fee, quantity and unitPrice

* Update changelog

---------

Co-authored-by: Rafael Claudio <rafacla@github.com>
Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-10-24 20:51:48 +02:00
fa627f686f Bugfix/fix chart for account excluded from analysis (#2534)
* Fix chart for account excluded from analysis

* Update changelog
2023-10-24 20:13:57 +02:00
0567083fc1 Feature/upgrade UUID to version 9.0.1 (#2537)
* Upgrade uuid to version 9.0.1

* Update changelog
2023-10-24 20:12:00 +02:00
3212efef17 Feature/upgrade yahoo finance2 to version 2.8.1 (#2536)
* Upgrade yahoo-finance2 to version 2.8.1

* Update changelog
2023-10-24 19:40:53 +02:00
6077e7c2f9 Feature/improve position detail dialog (#2532)
* Improve style and wording

* Update locales

* Update changelog
2023-10-23 20:50:40 +02:00
96b5dcfaf8 Create reusable currency selector component using mat-autocomplete (#2487)
* Create reusable currency selector component using mat-autocomplete

* Update changelog
2023-10-23 20:30:05 +02:00
c4e8e37884 Release 2.14.0 (#2530) 2023-10-21 20:07:40 +02:00
281d33f825 Feature/update oss friends 20231021 (#2529)
* Update OSS friends

* Improve style of sub title
2023-10-21 20:05:20 +02:00
5822e4d186 Bugfix/fix style of active page in header navigation (#2528)
* Fix style of active page

* Update changelog
2023-10-21 19:34:44 +02:00
cb166dcc78 Redirect to membership page (#2527) 2023-10-21 18:43:33 +02:00
4e7b7375a9 Feature/setup open figi (#2526)
* Setup OpenFIGI

* Update changelog
2023-10-21 18:12:50 +02:00
b8626c2086 Feature/change fees interest and search to general availability (#2525)
* Change features to general availability

* Fees on account level
* Interest on account level
* Search for a holding

* Update changelog

* Add documentation for experimental features
2023-10-21 10:25:05 +02:00
a59f9fa037 Feature/remove version from client (#2522)
* Remove version

* Update changelog
2023-10-21 09:41:07 +02:00
1666486940 Bugfix/trim text in i18n service (#2520)
* Trim text

* Update changelog
2023-10-20 22:36:03 +02:00
ac0ad48a65 Display invalid activity in csv import (#2460)
* Display invalid activity in csv import

* Update changelog
2023-10-20 22:07:57 +02:00
6a19eab425 Feature/improve membership card (#2517)
* Improve style

* Add animated border
2023-10-20 22:05:27 +02:00
750c627613 Feature/allow to edit market data of today (#2515)
* Allow to edit today's market data

* Update changelog
2023-10-20 17:37:55 +02:00
84 changed files with 16526 additions and 11936 deletions

1
.gitignore vendored
View File

@ -27,6 +27,7 @@
/.angular/cache
.env
.env.prod
.nx/cache
/.sass-cache
/connect.lock
/coverage

View File

@ -1,2 +1,3 @@
/.nx/cache
/dist
/test/import

View File

@ -5,6 +5,48 @@ 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).
## 2.15.0 - 2023-10-26
### Added
- Added support to edit the name, asset class and asset sub class of asset profiles with `MANUAL` data source in the asset profile details dialog of the admin control panel
### Changed
- Improved the style and wording of the position detail dialog
- Improved the validation in the activities import (expects positive values for `fee`, `quantity` and `unitPrice`)
- Improved the validation in the cash balance transfer from one to another account (expects a positive value)
- Changed the currency selector in the create or update account dialog to `@angular/material/autocomplete`
- Upgraded `Nx` from version `16.7.4` to `17.0.2`
- Upgraded `uuid` from version `9.0.0` to `9.0.1`
- Upgraded `yahoo-finance2` from version `2.8.0` to `2.8.1`
### Fixed
- Fixed the chart in the account detail dialog for accounts excluded from analysis
- Verified the current benchmark before loading it on the analysis page
## 2.14.0 - 2023-10-21
### Added
- Added the _OpenFIGI_ data enhancer for _Financial Instrument Global Identifier_ (FIGI)
- Added `figi`, `figiComposite` and `figiShareClass` to the asset profile model
### Changed
- Moved the fees on account level feature from experimental to general availability
- Moved the interest on account level feature from experimental to general availability
- Moved the search for a holding from experimental to general availability
- Improved the error message in the activities import for `csv` files
- Removed the application version from the client
- Allowed to edit todays historical market data in the asset profile details dialog of the admin control panel
### Fixed
- Fixed the style of the active page in the header navigation
- Trimmed text in `i18n` service to query `messages.*.xlf` files on the server
## 2.13.0 - 2023-10-20
### Added

View File

@ -1,5 +1,17 @@
# Ghostfolio Development Guide
## Experimental Features
New functionality can be enabled using a feature flag switch from the user settings.
### Backend
Remove permission in `UserService` using `without()`
### Frontend
Use `*ngIf="user?.settings?.isExperimentalFeatures"` in HTML template
## Git
### Rebase

View File

@ -1,4 +1,4 @@
import { IsNumber, IsString } from 'class-validator';
import { IsNumber, IsPositive, IsString } from 'class-validator';
export class TransferBalanceDto {
@IsString()
@ -8,5 +8,6 @@ export class TransferBalanceDto {
accountIdTo: string;
@IsNumber()
@IsPositive()
balance: number;
}

View File

@ -303,15 +303,21 @@ export class AdminService {
}
public async patchAssetProfileData({
assetClass,
assetSubClass,
comment,
dataSource,
name,
scraperConfiguration,
symbol,
symbolMapping
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
await this.symbolProfileService.updateSymbolProfile({
assetClass,
assetSubClass,
comment,
dataSource,
name,
scraperConfiguration,
symbol,
symbolMapping

View File

@ -1,11 +1,23 @@
import { Prisma } from '@prisma/client';
import { IsObject, IsOptional, IsString } from 'class-validator';
import { AssetClass, AssetSubClass, Prisma } from '@prisma/client';
import { IsEnum, IsObject, IsOptional, IsString } from 'class-validator';
export class UpdateAssetProfileDto {
@IsEnum(AssetClass, { each: true })
@IsOptional()
assetClass?: AssetClass;
@IsEnum(AssetSubClass, { each: true })
@IsOptional()
assetSubClass?: AssetSubClass;
@IsString()
@IsOptional()
comment?: string;
@IsString()
@IsOptional()
name?: string;
@IsObject()
@IsOptional()
scraperConfiguration?: Prisma.InputJsonObject;

View File

@ -280,6 +280,9 @@ export class ImportService {
createdAt,
currency,
dataSource,
figi,
figiComposite,
figiShareClass,
id,
isin,
name,
@ -350,6 +353,9 @@ export class ImportService {
createdAt,
currency,
dataSource,
figi,
figiComposite,
figiShareClass,
id,
isin,
name,
@ -509,6 +515,9 @@ export class ImportService {
comment: null,
countries: null,
createdAt: undefined,
figi: null,
figiComposite: null,
figiShareClass: null,
id: undefined,
isin: null,
name: null,

View File

@ -13,7 +13,9 @@ import {
IsISO8601,
IsNumber,
IsOptional,
IsString
IsPositive,
IsString,
Min
} from 'class-validator';
import { isString } from 'lodash';
@ -48,9 +50,11 @@ export class CreateOrderDto {
date: string;
@IsNumber()
@Min(0)
fee: number;
@IsNumber()
@IsPositive()
quantity: number;
@IsString()
@ -64,6 +68,7 @@ export class CreateOrderDto {
type: Type;
@IsNumber()
@IsPositive()
unitPrice: number;
@IsBoolean()

View File

@ -13,7 +13,9 @@ import {
IsISO8601,
IsNumber,
IsOptional,
IsString
IsPositive,
IsString,
Min
} from 'class-validator';
import { isString } from 'lodash';
@ -47,12 +49,14 @@ export class UpdateOrderDto {
date: string;
@IsNumber()
@Min(0)
fee: number;
@IsString()
id: string;
@IsNumber()
@IsPositive()
quantity: number;
@IsString()
@ -66,5 +70,6 @@ export class UpdateOrderDto {
type: Type;
@IsNumber()
@IsPositive()
unitPrice: number;
}

View File

@ -323,7 +323,8 @@ export class PortfolioController {
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
@Query('tags') filterByTags?: string,
@Query('withExcludedAccounts') withExcludedAccounts = false
): Promise<PortfolioPerformanceResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
@ -335,6 +336,7 @@ export class PortfolioController {
dateRange,
filters,
impersonationId,
withExcludedAccounts,
userId: this.request.user.id
});

View File

@ -372,20 +372,23 @@ export class PortfolioService {
filters,
impersonationId,
userCurrency,
userId
userId,
withExcludedAccounts = false
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userCurrency: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<HistoricalDataContainer> {
userId = await this.getUserId(impersonationId, userId);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
filters,
userId
userId,
withExcludedAccounts
});
const portfolioCalculator = new PortfolioCalculator({
@ -1110,12 +1113,14 @@ export class PortfolioService {
dateRange = 'max',
filters,
impersonationId,
userId
userId,
withExcludedAccounts = false
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<PortfolioPerformanceResponse> {
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId });
@ -1124,7 +1129,8 @@ export class PortfolioService {
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
filters,
userId
userId,
withExcludedAccounts
});
const portfolioCalculator = new PortfolioCalculator({
@ -1174,7 +1180,8 @@ export class PortfolioService {
filters,
impersonationId,
userCurrency,
userId
userId,
withExcludedAccounts
});
const itemOfToday = historicalDataContainer.items.find((item) => {
@ -1763,7 +1770,7 @@ export class PortfolioService {
filters,
includeDrafts = false,
userId,
withExcludedAccounts
withExcludedAccounts = false
}: {
filters?: Filter[];
includeDrafts?: boolean;
@ -1851,7 +1858,7 @@ export class PortfolioService {
portfolioItemsNow,
userCurrency,
userId,
withExcludedAccounts
withExcludedAccounts = false
}: {
filters?: Filter[];
orders: OrderWithAccount[];

View File

@ -104,7 +104,7 @@ export class SubscriptionController {
response.redirect(
`${this.configurationService.get(
'ROOT_URL'
)}/${DEFAULT_LANGUAGE_CODE}/account`
)}/${DEFAULT_LANGUAGE_CODE}/account/membership`
);
}

View File

@ -164,10 +164,10 @@ export class UserService {
let currentPermissions = getPermissions(user.role);
if (!(user.Settings.settings as UserSettings).isExperimentalFeatures) {
currentPermissions = without(
currentPermissions,
permissions.accessAssistant
);
// currentPermissions = without(
// currentPermissions,
// permissions.xyz
// );
}
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {

View File

@ -58,6 +58,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-capmon</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -166,6 +170,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-utluna</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -312,6 +320,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-capmon</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -420,6 +432,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-utluna</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -594,6 +610,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-campmon</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -702,6 +722,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-utluna</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -722,6 +746,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-capmon</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -830,6 +858,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-utluna</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-wealthica</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -38,6 +38,7 @@ export class ConfigurationService {
JWT_SECRET_KEY: str({}),
MAX_ACTIVITIES_TO_IMPORT: num({ default: Number.MAX_SAFE_INTEGER }),
MAX_ITEM_IN_CACHE: num({ default: 9999 }),
OPEN_FIGI_API_KEY: str({ default: '' }),
PORT: port({ default: 3333 }),
RAPID_API_API_KEY: str({ default: '' }),
REDIS_HOST: str({ default: 'localhost' }),

View File

@ -164,6 +164,9 @@ export class DataGatheringService {
countries,
currency,
dataSource,
figi,
figiComposite,
figiShareClass,
isin,
name,
sectors,
@ -178,6 +181,9 @@ export class DataGatheringService {
countries,
currency,
dataSource,
figi,
figiComposite,
figiShareClass,
isin,
name,
sectors,
@ -189,6 +195,9 @@ export class DataGatheringService {
assetSubClass,
countries,
currency,
figi,
figiComposite,
figiShareClass,
isin,
name,
sectors,

View File

@ -1,5 +1,6 @@
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module';
import { OpenFigiDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/openfigi/openfigi.service';
import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service';
import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service';
import { Module } from '@nestjs/common';
@ -9,6 +10,7 @@ import { DataEnhancerService } from './data-enhancer.service';
@Module({
exports: [
DataEnhancerService,
OpenFigiDataEnhancerService,
TrackinsightDataEnhancerService,
YahooFinanceDataEnhancerService,
'DataEnhancers'
@ -16,15 +18,21 @@ import { DataEnhancerService } from './data-enhancer.service';
imports: [ConfigurationModule, CryptocurrencyModule],
providers: [
DataEnhancerService,
OpenFigiDataEnhancerService,
TrackinsightDataEnhancerService,
YahooFinanceDataEnhancerService,
{
inject: [
OpenFigiDataEnhancerService,
TrackinsightDataEnhancerService,
YahooFinanceDataEnhancerService
],
provide: 'DataEnhancers',
useFactory: (trackinsight, yahooFinance) => [trackinsight, yahooFinance]
useFactory: (openfigi, trackinsight, yahooFinance) => [
openfigi,
trackinsight,
yahooFinance
]
}
]
})

View File

@ -0,0 +1,85 @@
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { parseSymbol } from '@ghostfolio/common/helper';
import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client';
import got, { Headers } from 'got';
@Injectable()
export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
private static baseUrl = 'https://api.openfigi.com';
public constructor(
private readonly configurationService: ConfigurationService
) {}
public async enhance({
response,
symbol
}: {
response: Partial<SymbolProfile>;
symbol: string;
}): Promise<Partial<SymbolProfile>> {
if (
!(
response.assetClass === 'EQUITY' &&
(response.assetSubClass === 'ETF' || response.assetSubClass === 'STOCK')
)
) {
return response;
}
const headers: Headers = {};
const { exchange, ticker } = parseSymbol({
symbol,
dataSource: response.dataSource
});
if (this.configurationService.get('OPEN_FIGI_API_KEY')) {
headers['X-OPENFIGI-APIKEY'] =
this.configurationService.get('OPEN_FIGI_API_KEY');
}
let abortController = new AbortController();
setTimeout(() => {
abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT);
const mappings = await got
.post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, {
headers,
json: [{ exchCode: exchange, idType: 'TICKER', idValue: ticker }],
// @ts-ignore
signal: abortController.signal
})
.json<any[]>();
if (mappings?.length === 1 && mappings[0].data?.length === 1) {
const { compositeFIGI, figi, shareClassFIGI } = mappings[0].data[0];
if (figi) {
response.figi = figi;
}
if (compositeFIGI) {
response.figiComposite = compositeFIGI;
}
if (shareClassFIGI) {
response.figiShareClass = shareClassFIGI;
}
}
return response;
}
public getName() {
return 'OPENFIGI';
}
public getTestSymbol() {
return undefined;
}
}

View File

@ -38,7 +38,7 @@ export class I18nService {
);
}
return translatedText;
return translatedText.trim();
}
private loadFiles() {

View File

@ -26,6 +26,7 @@ export interface Environment extends CleanedEnvAccessors {
JWT_SECRET_KEY: string;
MAX_ACTIVITIES_TO_IMPORT: number;
MAX_ITEM_IN_CACHE: number;
OPEN_FIGI_API_KEY: string;
PORT: number;
RAPID_API_API_KEY: string;
REDIS_HOST: string;

View File

@ -86,14 +86,24 @@ export class SymbolProfileService {
}
public updateSymbolProfile({
assetClass,
assetSubClass,
comment,
dataSource,
name,
scraperConfiguration,
symbol,
symbolMapping
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
return this.prismaService.symbolProfile.update({
data: { comment, scraperConfiguration, symbolMapping },
data: {
assetClass,
assetSubClass,
comment,
name,
scraperConfiguration,
symbolMapping
},
where: { dataSource_symbol: { dataSource, symbol } }
});
}

View File

@ -165,7 +165,6 @@
<div class="row text-center">
<div class="col">
© 2021 - {{ currentYear }} <a href="https://ghostfol.io">Ghostfolio</a>
{{ version }}
</div>
</div>

View File

@ -17,7 +17,6 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { environment } from '../environments/environment';
import { DataService } from './services/data.service';
import { TokenStorageService } from './services/token-storage.service';
import { UserService } from './services/user/user.service';
@ -60,7 +59,6 @@ export class AppComponent implements OnDestroy, OnInit {
public routerLinkResources = ['/' + $localize`resources`];
public showFooter = false;
public user: User;
public version = environment.version;
private unsubscribeSubject = new Subject<void>();

View File

@ -116,7 +116,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
type: 'ACCOUNT'
}
],
range: 'max'
range: 'max',
withExcludedAccounts: true
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ chart }) => {

View File

@ -9,7 +9,11 @@
[showYAxis]="true"
[symbol]="symbol"
></gf-line-chart>
<div *ngFor="let itemByMonth of marketDataByMonth | keyvalue" class="d-flex">
<div
*ngFor="let itemByMonth of marketDataByMonth | keyvalue"
class="d-flex"
[hidden]="!marketData.length > 0"
>
<div class="date px-1 text-nowrap">{{ itemByMonth.key }}</div>
<div class="align-items-center d-flex flex-grow-1 px-1">
<div

View File

@ -28,7 +28,6 @@
&.today {
background-color: rgba(var(--palette-accent-500), 1);
cursor: default;
}
}
}

View File

@ -83,10 +83,10 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
public ngOnChanges() {
this.defaultDateFormat = getDateFormatString(this.locale);
this.historicalDataItems = this.marketData.map((marketDataItem) => {
this.historicalDataItems = this.marketData.map(({ date, marketPrice }) => {
return {
date: format(marketDataItem.date, DATE_FORMAT),
value: marketDataItem.marketPrice
date: format(date, DATE_FORMAT),
value: marketPrice
};
});
@ -157,10 +157,6 @@ export class AdminMarketDataDetailComponent implements OnChanges, OnInit {
const date = parseISO(`${yearMonth}-${day}`);
const marketPrice = this.marketDataByMonth[yearMonth]?.[day]?.marketPrice;
if (isSameDay(date, new Date())) {
return;
}
const dialogRef = this.dialog.open(MarketDataDetailDialog, {
data: <MarketDataDetailDialogParams>{
date,

View File

@ -6,7 +6,7 @@ import {
OnDestroy,
OnInit
} from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service';
@ -17,7 +17,12 @@ import {
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { translate } from '@ghostfolio/ui/i18n';
import { MarketData, SymbolProfile } from '@prisma/client';
import {
AssetClass,
AssetSubClass,
MarketData,
SymbolProfile
} from '@prisma/client';
import { format, parseISO } from 'date-fns';
import { parse as csvToJson } from 'papaparse';
import { Subject } from 'rxjs';
@ -33,14 +38,23 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
styleUrls: ['./asset-profile-dialog.component.scss']
})
export class AssetProfileDialog implements OnDestroy, OnInit {
public assetClass: string;
public assetProfileClass: string;
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { id: assetClass, label: translate(assetClass) };
});
public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => {
return { id: assetSubClass, label: translate(assetSubClass) };
});
public assetProfile: AdminMarketDataDetails['assetProfile'];
public assetProfileForm = this.formBuilder.group({
assetClass: new FormControl<AssetClass>(undefined),
assetSubClass: new FormControl<AssetSubClass>(undefined),
comment: '',
name: ['', Validators.required],
scraperConfiguration: '',
symbolMapping: ''
});
public assetSubClass: string;
public assetProfileSubClass: string;
public benchmarks: Partial<SymbolProfile>[];
public countries: {
[code: string]: { name: string; value: number };
@ -86,8 +100,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
.subscribe(({ assetProfile, marketData }) => {
this.assetProfile = assetProfile;
this.assetClass = translate(this.assetProfile?.assetClass);
this.assetSubClass = translate(this.assetProfile?.assetSubClass);
this.assetProfileClass = translate(this.assetProfile?.assetClass);
this.assetProfileSubClass = translate(this.assetProfile?.assetSubClass);
this.countries = {};
this.isBenchmark = this.benchmarks.some(({ id }) => {
return id === this.assetProfile.id;
@ -114,6 +128,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}
this.assetProfileForm.setValue({
name: this.assetProfile.name,
assetClass: this.assetProfile.assetClass,
assetSubClass: this.assetProfile.assetSubClass,
comment: this.assetProfile?.comment ?? '',
scraperConfiguration: JSON.stringify(
this.assetProfile?.scraperConfiguration ?? {}
@ -204,9 +221,12 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} catch {}
const assetProfileData: UpdateAssetProfileDto = {
assetClass: this.assetProfileForm.controls['assetClass'].value,
assetSubClass: this.assetProfileForm.controls['assetSubClass'].value,
comment: this.assetProfileForm.controls['comment'].value ?? null,
name: this.assetProfileForm.controls['name'].value,
scraperConfiguration,
symbolMapping,
comment: this.assetProfileForm.controls['comment'].value ?? null
symbolMapping
};
this.adminService

View File

@ -112,7 +112,11 @@
>
</div>
<div class="col-6 mb-3">
<gf-value i18n size="medium" [hidden]="!assetClass" [value]="assetClass"
<gf-value
i18n
size="medium"
[hidden]="!assetProfileClass"
[value]="assetProfileClass"
>Asset Class</gf-value
>
</div>
@ -120,8 +124,8 @@
<gf-value
i18n
size="medium"
[hidden]="!assetSubClass"
[value]="assetSubClass"
[hidden]="!assetProfileSubClass"
[value]="assetProfileSubClass"
>Asset Sub Class</gf-value
>
</div>
@ -174,6 +178,38 @@
</ng-template>
</ng-container>
</div>
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name</mat-label>
<input formControlName="name" matInput type="text" />
</mat-form-field>
</div>
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass">
<mat-option [value]="null"></mat-option>
<mat-option
*ngFor="let assetClass of assetClasses"
[value]="assetClass.id"
>{{ assetClass.label }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Asset Sub Class</mat-label>
<mat-select formControlName="assetSubClass">
<mat-option [value]="null"></mat-option>
<mat-option
*ngFor="let assetSubClass of assetSubClasses"
[value]="assetSubClass.id"
>{{ assetSubClass.label }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
<div class="d-flex my-3">
<div class="w-50">
<mat-checkbox

View File

@ -7,6 +7,7 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select';
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value';
@ -26,6 +27,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
MatDialogModule,
MatInputModule,
MatMenuModule,
MatSelectModule,
ReactiveFormsModule,
TextFieldModule
],

View File

@ -1,6 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatCheckboxChange } from '@angular/material/checkbox';
import { environment } from '@ghostfolio/client/../environments/environment';
import { AdminService } from '@ghostfolio/client/services/admin.service';
import { CacheService } from '@ghostfolio/client/services/cache.service';
import { DataService } from '@ghostfolio/client/services/data.service';

View File

@ -1,6 +1,5 @@
<button
*ngIf="deviceType === 'mobile'"
class="mt-2"
mat-button
(click)="onClickCloseButton()"
>

View File

@ -1,7 +1,9 @@
:host {
display: flex;
flex: 0 0 auto;
margin-bottom: 0;
min-height: 0;
padding: 0 !important;
@media (min-width: 576px) {
padding: 0 !important;
}
}

View File

@ -155,16 +155,18 @@
[isDate]="true"
[locale]="data.locale"
[value]="firstBuyDate"
>First Buy Date</gf-value
>First Activity</gf-value
>
</div>
<div class="col-6 mb-3">
<gf-value
i18n
size="medium"
[locale]="data.locale"
[value]="transactionCount"
>Transactions</gf-value
><ng-container *ngIf="transactionCount === 1">Activity</ng-container
><ng-container *ngIf="transactionCount !== 1"
>Activities</ng-container
></gf-value
>
</div>
<div class="col-6 mb-3">

View File

@ -2,40 +2,42 @@
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Membership</h1>
<div class="row">
<div class="col">
<gf-membership-card
class="mb-5 mx-auto"
[expiresAt]="(user?.subscription?.expiresAt | date: defaultDateFormat) ?? ''"
[name]="user?.subscription?.type"
></gf-membership-card>
<div class="d-flex">
<div class="mx-auto">
<div *ngIf="user?.subscription?.type === 'Basic'">
<ng-container
*ngIf="hasPermissionForSubscription && hasPermissionToUpdateUserSettings"
>
<button color="primary" mat-flat-button (click)="onCheckout()">
<ng-container *ngIf="user.subscription.offer === 'default'" i18n
>Upgrade</ng-container
>
<ng-container *ngIf="user.subscription.offer === 'renewal'" i18n
>Renew</ng-container
>
</button>
<div *ngIf="price" class="mt-1">
<ng-container *ngIf="coupon"
><del class="text-muted"
>{{ baseCurrency }}&nbsp;{{ price }}</del
>&nbsp;{{ baseCurrency }}&nbsp;{{ price - coupon
}}</ng-container
>
<ng-container *ngIf="!coupon"
>{{ baseCurrency }}&nbsp;{{ price }}</ng-container
>&nbsp;<span i18n>per year</span>
</div>
</ng-container>
<div class="align-items-center d-flex flex-column">
<gf-membership-card
[expiresAt]="user?.subscription?.expiresAt | date: defaultDateFormat"
[name]="user?.subscription?.type"
></gf-membership-card>
<div
*ngIf="user?.subscription?.type === 'Basic'"
class="d-flex flex-column mt-5"
>
<ng-container
*ngIf="hasPermissionForSubscription && hasPermissionToUpdateUserSettings"
>
<button color="primary" mat-flat-button (click)="onCheckout()">
<ng-container *ngIf="user.subscription.offer === 'default'" i18n
>Upgrade</ng-container
>
<ng-container *ngIf="user.subscription.offer === 'renewal'" i18n
>Renew</ng-container
>
</button>
<div *ngIf="price" class="mt-1 text-center">
<ng-container *ngIf="coupon"
><del class="text-muted"
>{{ baseCurrency }}&nbsp;{{ price }}</del
>&nbsp;{{ baseCurrency }}&nbsp;{{ price - coupon
}}</ng-container
>
<ng-container *ngIf="!coupon"
>{{ baseCurrency }}&nbsp;{{ price }}</ng-container
>&nbsp;<span i18n>per year</span>
</div>
</ng-container>
<div class="align-items-center d-flex justfiy-content-center mt-4">
<a
*ngIf="!user?.subscription?.expiresAt"
class="mr-2 my-2"
class="mx-1"
mat-stroked-button
[href]="trySubscriptionMail"
><span i18n>Try Premium</span>
@ -46,7 +48,7 @@
></a>
<a
*ngIf="hasPermissionToUpdateUserSettings"
class="mr-2 my-2"
class="mx-1"
i18n
mat-stroked-button
[routerLink]=""

View File

@ -1,7 +1,7 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h1 class="h3 mb-4 text-center">
<h1 class="h3 line-height-1 mb-4 text-center">
<span class="d-none d-sm-block"
><ng-container i18n>Our</ng-container> OSS Friends</span
>

View File

@ -1,5 +1,4 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { environment } from '@ghostfolio/client/../environments/environment';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces';
@ -20,7 +19,6 @@ export class AboutOverviewPageComponent implements OnDestroy, OnInit {
public routerLinkFaq = ['/' + $localize`faq`];
public routerLinkFeatures = ['/' + $localize`features`];
public user: User;
public version = environment.version;
private unsubscribeSubject = new Subject<void>();

View File

@ -35,9 +35,6 @@
title="Contributors to Ghostfolio"
>contributors</a
>.
<ng-container *ngIf="version">
This instance is running Ghostfolio {{ version }}.
</ng-container>
<ng-container *ngIf="hasPermissionForSubscription"
>Check the system status at
<a href="https://status.ghostfol.io" title="Ghostfolio Status"

View File

@ -15,6 +15,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { Currency } from '@ghostfolio/common/interfaces/currency.interface';
import { Platform } from '@prisma/client';
import { Observable, Subject } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
@ -30,7 +31,7 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
})
export class CreateOrUpdateAccountDialog implements OnDestroy {
public accountForm: FormGroup;
public currencies: string[] = [];
public currencies: Currency[] = [];
public filteredPlatforms: Observable<Platform[]>;
public platforms: Platform[];
@ -46,7 +47,10 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
public ngOnInit() {
const { currencies, platforms } = this.dataService.fetchInfo();
this.currencies = currencies;
this.currencies = currencies.map((currency) => ({
label: currency,
value: currency
}));
this.platforms = platforms;
this.accountForm = this.formBuilder.group({
@ -101,7 +105,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
const account: CreateAccountDto | UpdateAccountDto = {
balance: this.accountForm.controls['balance'].value,
comment: this.accountForm.controls['comment'].value,
currency: this.accountForm.controls['currency'].value,
currency: this.accountForm.controls['currency'].value?.value,
id: this.accountForm.controls['accountId'].value,
isExcluded: this.accountForm.controls['isExcluded'].value,
name: this.accountForm.controls['name'].value,

View File

@ -20,11 +20,10 @@
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label>
<mat-select formControlName="currency">
<mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option
>
</mat-select>
<gf-currency-selector
formControlName="currency"
[currencies]="currencies"
/>
</mat-form-field>
</div>
<div>
@ -37,7 +36,7 @@
(keydown.enter)="$event.stopPropagation()"
/>
<span class="ml-2" matTextSuffix
>{{ accountForm.controls['currency'].value }}</span
>{{ accountForm.controls['currency']?.value?.value }}</span
>
</mat-form-field>
</div>

View File

@ -7,8 +7,8 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { GfCurrencySelectorModule } from '@ghostfolio/ui/currency-selector/currency-selector.module';
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
@ -17,6 +17,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
imports: [
CommonModule,
FormsModule,
GfCurrencySelectorModule,
GfSymbolIconModule,
MatAutocompleteModule,
MatButtonModule,
@ -24,7 +25,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
ReactiveFormsModule
]
})

View File

@ -21,10 +21,7 @@
>Stocks, ETFs, bonds, cryptocurrencies, commodities</small
>
</mat-option>
<mat-option
*ngIf="data.user?.settings?.isExperimentalFeatures"
value="FEE"
>
<mat-option value="FEE">
<span><b>{{ typesTranslationMap['FEE'] }}</b></span>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>One-time fee, annual account fees</small
@ -36,10 +33,7 @@
>Distribution of corporate earnings</small
>
</mat-option>
<mat-option
*ngIf="data.user?.settings?.isExperimentalFeatures"
value="INTEREST"
>
<mat-option value="INTEREST">
<span><b>{{ typesTranslationMap['INTEREST'] }}</b></span>
<small class="d-block line-height-1 text-muted text-nowrap" i18n
>Revenue for lending out money</small

View File

@ -267,6 +267,8 @@ export class ImportActivitiesDialog implements OnDestroy {
return;
} else if (file.name.endsWith('.csv')) {
const content = fileContent.split('\n').slice(1);
try {
const data = await this.importActivitiesService.importCsv({
fileContent,
@ -277,7 +279,7 @@ export class ImportActivitiesDialog implements OnDestroy {
} catch (error) {
console.error(error);
this.handleImportError({
activities: error?.activities ?? [],
activities: error?.activities ?? content,
error: {
error: { message: error?.error?.message ?? [error?.message] }
}

View File

@ -309,7 +309,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
}
private update() {
this.isLoadingBenchmarkComparator = true;
this.isLoadingInvestmentChart = true;
this.dataService
@ -385,35 +384,37 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
}
private updateBenchmarkDataItems() {
this.benchmarkDataItems = [];
if (this.user.settings.benchmark) {
const { dataSource, symbol } =
this.benchmarks.find(({ id }) => {
return id === this.user.settings.benchmark;
}) ?? {};
this.dataService
.fetchBenchmarkBySymbol({
dataSource,
symbol,
startDate: this.firstOrderDate
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketData }) => {
this.benchmarkDataItems = marketData.map(({ date, value }) => {
return {
date,
value
};
if (dataSource && symbol) {
this.isLoadingBenchmarkComparator = true;
this.dataService
.fetchBenchmarkBySymbol({
dataSource,
symbol,
startDate: this.firstOrderDate
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ marketData }) => {
this.benchmarkDataItems = marketData.map(({ date, value }) => {
return {
date,
value
};
});
this.isLoadingBenchmarkComparator = false;
this.changeDetectorRef.markForCheck();
});
this.isLoadingBenchmarkComparator = false;
this.changeDetectorRef.markForCheck();
});
} else {
this.benchmarkDataItems = [];
this.isLoadingBenchmarkComparator = false;
}
}
}
}

View File

@ -3,7 +3,7 @@
<div *ngIf="user?.settings?.viewMode !== 'ZEN'" class="my-4 text-center">
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoadingBenchmarkComparator"
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
></gf-toggle>
@ -23,7 +23,7 @@
[benchmarks]="benchmarks"
[colorScheme]="user?.settings?.colorScheme"
[daysInMarket]="daysInMarket"
[isLoading]="isLoadingBenchmarkComparator"
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[locale]="user?.settings?.locale"
[performanceDataItems]="performanceDataItemsInPercentage"
[user]="user"
@ -149,7 +149,7 @@
[daysInMarket]="daysInMarket"
[historicalDataItems]="performanceDataItems"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[isLoading]="isLoadingBenchmarkComparator"
[isLoading]="isLoadingInvestmentChart"
[locale]="user?.settings?.locale"
[range]="user?.settings?.dateRange"
></gf-investment-chart>

View File

@ -35,15 +35,19 @@
its capabilities, security, and user experience.
</p>
<p i18n>
Lets dive deeper into the detailed comparison table below to gain a
thorough understanding of how Ghostfolio positions itself relative
to {{ product2.name }}. We will explore various aspects such as
features, data privacy, pricing, and more, allowing you to make a
well-informed choice for your personal requirements.
Lets dive deeper into the detailed Ghostfolio vs {{ product2.name
}} comparison table below to gain a thorough understanding of how
Ghostfolio positions itself relative to {{ product2.name }}. We will
explore various aspects such as features, data privacy, pricing, and
more, allowing you to make a well-informed choice for your personal
requirements.
</p>
</section>
<section class="mb-4">
<table class="gf-table w-100">
<caption class="text-center" i18n>
Ghostfolio vs {{ product2.name }} comparison table
</caption>
<thead>
<tr class="mat-mdc-header-row">
<th class="mat-mdc-header-cell px-1 py-2"></th>
@ -197,12 +201,13 @@
</section>
<section class="mb-4">
<p i18n>
Please note that the information provided is based on our
independent research and analysis. This website is not affiliated
with {{ product2.name }} or any other product mentioned in the
comparison. As the landscape of personal finance tools evolves, it
is essential to verify any specific details or changes directly from
the respective product page. Data needs a refresh? Help us maintain
Please note that the information provided in the Ghostfolio vs {{
product2.name }} comparison table is based on our independent
research and analysis. This website is not affiliated with {{
product2.name }} or any other product mentioned in the comparison.
As the landscape of personal finance tools evolves, it is essential
to verify any specific details or changes directly from the
respective product page. Data needs a refresh? Help us maintain
accurate data on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</p>
@ -291,7 +296,7 @@
aria-current="page"
class="active breadcrumb-item text-truncate"
>
{{ product2.name }}
Ghostfolio vs {{ product2.name }}
</li>
</ol>
</nav>

View File

@ -1,6 +1,7 @@
import { Product } from '@ghostfolio/common/interfaces';
import { AltooPageComponent } from './products/altoo-page.component';
import { BeanvestPageComponent } from './products/beanvest-page.component';
import { CapMonPageComponent } from './products/capmon-page.component';
import { CopilotMoneyPageComponent } from './products/copilot-money-page.component';
import { DeltaPageComponent } from './products/delta-page.component';
@ -28,6 +29,7 @@ import { StocklePageComponent } from './products/stockle-page.component';
import { StockMarketEyePageComponent } from './products/stockmarketeye-page.component';
import { SumioPageComponent } from './products/sumio-page.component';
import { UtlunaPageComponent } from './products/utluna-page.component';
import { WealthicaPageComponent } from './products/wealthica-page.component';
import { YeekateePageComponent } from './products/yeekatee-page.component';
export const products: Product[] = [
@ -63,6 +65,17 @@ export const products: Product[] = [
origin: $localize`Switzerland`,
slogan: 'Simplicity for Complex Wealth'
},
{
component: BeanvestPageComponent,
founded: 2020,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'beanvest',
name: 'Beanvest',
origin: $localize`France`,
pricingPerYear: '$100',
slogan: 'Stock Portfolio Tracker for Smart Investors'
},
{
component: CapMonPageComponent,
founded: 2022,
@ -353,6 +366,18 @@ export const products: Product[] = [
slogan: 'Your Portfolio. Revealed.',
useAnonymously: true
},
{
component: WealthicaPageComponent,
founded: 2015,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'wealthica',
languages: ['English', 'Français'],
name: 'Wealthica',
origin: $localize`Canada`,
pricingPerYear: '$50',
slogan: 'See all your investments in one place'
},
{
component: YeekateePageComponent,
founded: 2021,

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-beanvest-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class BeanvestPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'beanvest';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-wealthica-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class WealthicaPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'wealthica';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

View File

@ -203,15 +203,25 @@ export class AdminService {
}
public patchAssetProfile({
assetClass,
assetSubClass,
comment,
dataSource,
name,
scraperConfiguration,
symbol,
symbolMapping
}: UniqueAsset & UpdateAssetProfileDto) {
return this.http.patch<EnhancedSymbolProfile>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}`,
{ comment, scraperConfiguration, symbolMapping }
{
assetClass,
assetSubClass,
comment,
name,
scraperConfiguration,
symbolMapping
}
);
}

View File

@ -386,14 +386,20 @@ export class DataService {
public fetchPortfolioPerformance({
filters,
range
range,
withExcludedAccounts = false
}: {
filters?: Filter[];
range: DateRange;
withExcludedAccounts?: boolean;
}): Observable<PortfolioPerformanceResponse> {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range);
if (withExcludedAccounts) {
params = params.append('withExcludedAccounts', withExcludedAccounts);
}
return this.http
.get<any>(`/api/v2/portfolio/performance`, {
params

View File

@ -286,7 +286,7 @@ export class ImportActivitiesService {
for (const key of ImportActivitiesService.QUANTITY_KEYS) {
if (isFinite(item[key])) {
return item[key];
return Math.abs(item[key]);
}
}
@ -372,7 +372,7 @@ export class ImportActivitiesService {
for (const key of ImportActivitiesService.UNIT_PRICE_KEYS) {
if (isFinite(item[key])) {
return item[key];
return Math.abs(item[key]);
}
}

View File

@ -1,5 +1,5 @@
{
"createdAt": "2023-10-05T00:00:00.000Z",
"createdAt": "2023-10-21T00:00:00.000Z",
"data": [
{
"name": "BoxyHQ",
@ -21,11 +21,21 @@
"description": "The Open-Source DocuSign Alternative. We aim to earn your trust by enabling you to self-host the platform and examine its inner workings.",
"href": "https://documenso.com"
},
{
"name": "dyrector.io",
"description": "dyrector.io is an open-source continuous delivery & deployment platform with version management.",
"href": "https://dyrector.io"
},
{
"name": "Erxes",
"description": "The Open-Source HubSpot Alternative. A single XOS enables to create unique and life-changing experiences that work for all types of business.",
"href": "https://erxes.io"
},
{
"name": "Firecamp",
"description": "vscode for apis, open-source postman/insomnia alternative",
"href": "https://firecamp.io"
},
{
"name": "Formbricks",
"description": "Survey granular user segments at any point in the user journey. Gather up to 6x more insights with targeted micro-surveys. All open-source.",
@ -46,6 +56,11 @@
"description": "Open-source authentication and user management for the passkey era. Integrated in minutes, for web and mobile apps.",
"href": "https://www.hanko.io"
},
{
"name": "Hook0",
"description": "Open-Source Webhooks-as-a-service (WaaS) that makes it easy for developers to send webhooks.",
"href": "https://www.hook0.com/"
},
{
"name": "HTMX",
"description": "HTMX is a dependency-free JavaScript library that allows you to access AJAX, CSS Transitions, WebSockets, and Server Sent Events directly in HTML.",
@ -86,11 +101,21 @@
"description": "Open-source solution to deploy, scale, and operate your multiplayer game.",
"href": "https://rivet.gg"
},
{
"name": "Shelf.nu",
"description": "Open Source Asset and Equipment tracking software that lets you create QR asset labels, manage and overview your assets across locations.",
"href": "https://www.shelf.nu/"
},
{
"name": "Sniffnet",
"description": "Sniffnet is a network monitoring tool to help you easily keep track of your Internet traffic.",
"href": "https://www.sniffnet.net"
},
{
"name": "Spark.NET",
"description": "The .NET Web Framework for Makers. Build production ready, full-stack web applications fast without sweating the small stuff.",
"href": "https://spark-framework.net"
},
{
"name": "Tolgee",
"description": "Software localization from A to Z made really easy.",
@ -101,16 +126,16 @@
"description": "Create long-running Jobs directly in your codebase with features like API integrations, webhooks, scheduling and delays.",
"href": "https://trigger.dev"
},
{
"name": "Typebot",
"description": "Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.",
"href": "https://typebot.io"
},
{
"name": "Twenty",
"description": "A modern CRM offering the flexibility of open-source, advanced features and sleek design.",
"href": "https://twenty.com"
},
{
"name": "Typebot",
"description": "Typebot gives you powerful blocks to create unique chat experiences. Embed them anywhere on your apps and start collecting results like magic.",
"href": "https://typebot.io"
},
{
"name": "Webiny",
"description": "Open-source enterprise-grade serverless CMS. Own your data. Scale effortlessly. Customize everything.",
@ -120,11 +145,6 @@
"name": "Webstudio",
"description": "Webstudio is an open source alternative to Webflow",
"href": "https://webstudio.is"
},
{
"name": "Spark.NET",
"description": "The .NET Web Framework for Makers. Build production ready, full-stack web applications fast without sweating the small stuff.",
"href": "https://spark-framework.net"
}
],
"source": "https://formbricks.com/api/oss-friends"

View File

@ -1,6 +1,5 @@
export const environment = {
lastPublish: '{BUILD_TIMESTAMP}',
production: true,
stripePublicKey: '',
version: `v${require('../../../../package.json').version}`
stripePublicKey: ''
};

View File

@ -5,8 +5,7 @@
export const environment = {
lastPublish: null,
production: false,
stripePublicKey: '',
version: 'dev'
stripePublicKey: ''
};
/*

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -446,6 +446,12 @@ ngx-skeleton-loader {
.mat-mdc-menu-panel {
.mat-mdc-menu-item {
&.font-weight-bold {
.mat-mdc-menu-item-text {
--mat-menu-item-label-text-weight: 700;
}
}
.mdc-list-item__primary-text {
align-items: center;
display: flex;

View File

@ -322,6 +322,15 @@ export function parseDate(date: string): Date | null {
return parseISO(date);
}
export function parseSymbol({ dataSource, symbol }: UniqueAsset) {
const [ticker, exchange] = symbol.split('.');
return {
ticker,
exchange: exchange ?? (dataSource === 'YAHOO' ? 'US' : undefined)
};
}
export function prettifySymbol(aSymbol: string): string {
return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, '');
}

View File

@ -0,0 +1,4 @@
export interface Currency {
label: string;
value: string;
}

View File

@ -0,0 +1,21 @@
<input
autocapitalize="off"
autocomplete="off"
matInput
[formControl]="control"
[matAutocomplete]="currencyAutocomplete"
/>
<mat-autocomplete
#currencyAutocomplete="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="onUpdateCurrency($event)"
>
<mat-option
*ngFor="let currencyItem of filteredCurrencies"
class="line-height-1"
[value]="currencyItem"
>
{{ currencyItem.label }}
</mat-option>
</mat-autocomplete>

View File

@ -0,0 +1,3 @@
:host {
display: block;
}

View File

@ -0,0 +1,167 @@
import { FocusMonitor } from '@angular/cdk/a11y';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { FormControl, FormGroupDirective, NgControl } from '@angular/forms';
import {
MatAutocomplete,
MatAutocompleteSelectedEvent
} from '@angular/material/autocomplete';
import { MatFormFieldControl } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { Currency } from '@ghostfolio/common/interfaces/currency.interface';
import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field';
import { Subject } from 'rxjs';
import { map, startWith, takeUntil } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[attr.aria-describedBy]': 'describedBy',
'[id]': 'id'
},
providers: [
{
provide: MatFormFieldControl,
useExisting: CurrencySelectorComponent
}
],
selector: 'gf-currency-selector',
styleUrls: ['./currency-selector.component.scss'],
templateUrl: 'currency-selector.component.html'
})
export class CurrencySelectorComponent
extends AbstractMatFormField<Currency>
implements OnInit, OnDestroy
{
@Input() private currencies: Currency[] = [];
@Input() private formControlName: string;
@ViewChild(MatInput) private input: MatInput;
@ViewChild('currencyAutocomplete')
public currencyAutocomplete: MatAutocomplete;
public control = new FormControl();
public filteredCurrencies: Currency[] = [];
private unsubscribeSubject = new Subject<void>();
public constructor(
public readonly _elementRef: ElementRef,
public readonly _focusMonitor: FocusMonitor,
public readonly changeDetectorRef: ChangeDetectorRef,
private readonly formGroupDirective: FormGroupDirective,
public readonly ngControl: NgControl
) {
super(_elementRef, _focusMonitor, ngControl);
this.controlType = 'currency-selector';
}
public ngOnInit() {
if (this.disabled) {
this.control.disable();
}
const formGroup = this.formGroupDirective.form;
if (formGroup) {
const control = formGroup.get(this.formControlName);
if (control) {
this.value = this.currencies.find(({ value }) => {
return value === control.value;
});
}
}
this.control.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
if (super.value?.value) {
super.value.value = null;
}
});
this.control.valueChanges
.pipe(
takeUntil(this.unsubscribeSubject),
startWith(''),
map((value) => {
return value ? this.filter(value) : this.currencies.slice();
})
)
.subscribe((values) => {
this.filteredCurrencies = values;
});
}
public displayFn(currency: Currency) {
return currency?.label ?? '';
}
public get empty() {
return this.input?.empty;
}
public focus() {
this.input.focus();
}
public ngDoCheck() {
if (this.ngControl) {
this.validateRequired();
this.errorState = this.ngControl.invalid && this.ngControl.touched;
this.stateChanges.next();
}
}
public onUpdateCurrency(event: MatAutocompleteSelectedEvent) {
super.value = {
label: event.option.value.label,
value: event.option.value.value
} as Currency;
}
public set value(value: Currency) {
const newValue =
typeof value === 'object' && value !== null ? { ...value } : value;
this.control.setValue(newValue);
super.value = newValue;
}
public ngOnDestroy() {
super.ngOnDestroy();
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private filter(value: Currency | string) {
const filterValue =
typeof value === 'string'
? value?.toLowerCase()
: value?.value.toLowerCase();
return this.currencies.filter((currency) => {
return currency.value.toLowerCase().startsWith(filterValue);
});
}
private validateRequired() {
const requiredCheck = super.required
? !super.value.label || !super.value.value
: false;
if (requiredCheck) {
this.ngControl.control.setErrors({ invalidData: true });
}
}
}

View File

@ -0,0 +1,23 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { CurrencySelectorComponent } from './currency-selector.component';
@NgModule({
declarations: [CurrencySelectorComponent],
exports: [CurrencySelectorComponent],
imports: [
CommonModule,
FormsModule,
MatAutocompleteModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfCurrencySelectorModule {}

View File

@ -1,8 +1,4 @@
<span class="align-items-center d-flex"
><span
class="d-inline-block logo"
[ngClass]="{ 'mr-1': showLabel }"
[ngStyle]="{ 'background-color': color }"
></span>
><span class="d-inline-block logo" [ngClass]="{ 'mr-1': showLabel }"></span>
<span *ngIf="showLabel" class="label">{{ label ?? 'Ghostfolio' }}</span></span
>

View File

@ -4,18 +4,12 @@
}
.logo {
background-color: rgba(var(--dark-primary-text));
background-color: currentColor;
margin-top: -2px;
mask: url('/assets/ghost.svg') no-repeat center;
}
}
:host-context(.is-dark-theme) {
.logo {
background-color: rgba(var(--light-primary-text));
}
}
:host-context(.large) {
.label {
font-size: 3rem;

View File

@ -13,7 +13,6 @@ import {
})
export class LogoComponent {
@HostBinding('class') @Input() size: 'large' | 'medium' = 'medium';
@Input() color: string;
@Input() label: string;
@Input() showLabel = true;

View File

@ -1,25 +1,29 @@
<a
class="card-item d-flex flex-column justify-content-between p-4"
<div
class="card-container position-relative"
[ngClass]="{ premium: name === 'Premium' }"
[routerLink]="routerLinkPricing"
>
<div class="d-flex justify-content-end">
<gf-logo
color="rgba(255, 255, 255, 1)"
size="large"
[showLabel]="false"
></gf-logo>
</div>
<div class="d-flex justify-content-between">
<div>
<div class="card-item-heading mb-1 text-muted" i18n>Membership</div>
<div class="card-item-name line-height-1 text-truncate">{{ name }}</div>
<a
class="card-item d-flex flex-column justify-content-between p-4"
[routerLink]="routerLinkPricing"
>
<div class="d-flex justify-content-end">
<gf-logo
size="large"
[ngClass]="{ 'text-muted': name === 'Basic' }"
[showLabel]="false"
/>
</div>
<div>
<div class="card-item-heading mb-1 text-muted" i18n>Valid until</div>
<div class="card-item-name line-height-1 text-truncate">
{{ expiresAt }}
<div class="d-flex justify-content-between">
<div>
<div class="heading text-muted" i18n>Membership</div>
<div class="text-truncate value">{{ name }}</div>
</div>
<div *ngIf="expiresAt">
<div class="heading text-muted" i18n>Valid until</div>
<div class="text-truncate value">
{{ expiresAt }}
</div>
</div>
</div>
</div>
</a>
</a>
</div>

View File

@ -1,24 +1,66 @@
:host {
--borderRadius: 1rem;
--borderWidth: 2px;
display: block;
max-width: 25rem;
padding-top: calc(1 * var(--borderWidth));
width: 100%;
.card-item {
aspect-ratio: 1.586;
background-color: #343a40 !important;
border-radius: 1rem;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.3);
.card-container {
border-radius: var(--borderRadius);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15);
&.premium {
background-color: #1d2124 !important;
&:after {
animation: animatedborder 7s ease alternate infinite;
background: linear-gradient(60deg, #5073b8, #1098ad, #07b39b, #6fba82);
background-size: 300% 300%;
border-radius: var(--borderRadius);
content: '';
height: calc(100% + var(--borderWidth) * 2);
left: calc(-1 * var(--borderWidth));
top: calc(-1 * var(--borderWidth));
position: absolute;
width: calc(100% + var(--borderWidth) * 2);
z-index: -1;
@keyframes animatedborder {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
}
.card-item-heading {
font-size: 13px;
}
.card-item-name {
.card-item {
aspect-ratio: 1.586;
background-color: #1d2124;
border-radius: calc(var(--borderRadius) - var(--borderWidth));
color: rgba(var(--light-primary-text));
font-size: 18px;
.heading {
font-size: 13px;
}
.value {
font-size: 18px;
}
}
&:not(.premium) {
&:after {
opacity: 0;
}
.card-item {
background-color: #ffffff;
color: rgba(var(--dark-primary-text));
}
}
}
}

View File

@ -19,6 +19,7 @@ import { MatInput } from '@angular/material/input';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { translate } from '@ghostfolio/ui/i18n';
import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field';
import { isString } from 'lodash';
import { Subject, tap } from 'rxjs';
import {
@ -29,8 +30,6 @@ import {
takeUntil
} from 'rxjs/operators';
import { AbstractMatFormField } from './abstract-mat-form-field';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
@ -54,7 +53,7 @@ export class SymbolAutocompleteComponent
@Input() private includeIndices = false;
@Input() public isLoading = false;
@ViewChild(MatInput, { static: false }) private input: MatInput;
@ViewChild(MatInput) private input: MatInput;
@ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;

36
nx.json
View File

@ -2,23 +2,6 @@
"affected": {
"defaultBase": "origin/main"
},
"npmScope": "ghostfolio",
"tasksRunnerOptions": {
"default": {
"runner": "nx-cloud",
"options": {
"accessToken": "Mjg0ZGQ2YjAtNGI4NS00NmYwLThhOWEtMWZmNmQzODM4YzU4fHJlYWQ=",
"cacheableOperations": [
"build",
"lint",
"test",
"e2e",
"build-storybook"
],
"parallel": 1
}
}
},
"defaultProject": "api",
"generators": {
"@nx/angular:application": {
@ -37,13 +20,16 @@
"targetDefaults": {
"build": {
"dependsOn": ["^build"],
"inputs": ["production", "^production"]
"inputs": ["production", "^production"],
"cache": true
},
"e2e": {
"inputs": ["default", "^production"]
"inputs": ["default", "^production"],
"cache": true
},
"test": {
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"]
"inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"],
"cache": true
},
"build-storybook": {
"inputs": [
@ -52,7 +38,11 @@
"{workspaceRoot}/.storybook/**/*",
"{projectRoot}/.storybook/**/*",
"{projectRoot}/tsconfig.storybook.json"
]
],
"cache": true
},
"lint": {
"cache": true
}
},
"namedInputs": {
@ -72,5 +62,7 @@
"!{projectRoot}/tsconfig.storybook.json",
"!{projectRoot}/src/test-setup.[jt]s"
]
}
},
"nxCloudAccessToken": "Mjg0ZGQ2YjAtNGI4NS00NmYwLThhOWEtMWZmNmQzODM4YzU4fHJlYWQ=",
"parallel": 1
}

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.13.0",
"version": "2.15.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -128,8 +128,8 @@
"stripe": "11.12.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.14.2",
"uuid": "9.0.0",
"yahoo-finance2": "2.8.0",
"uuid": "9.0.1",
"yahoo-finance2": "2.8.1",
"zone.js": "0.13.1"
},
"devDependencies": {
@ -146,21 +146,21 @@
"@angular/pwa": "16.2.0",
"@nestjs/schematics": "10.0.1",
"@nestjs/testing": "10.1.3",
"@nx/angular": "16.7.4",
"@nx/cypress": "16.7.4",
"@nx/eslint-plugin": "16.7.4",
"@nx/jest": "16.7.4",
"@nx/js": "16.7.4",
"@nx/nest": "16.7.4",
"@nx/node": "16.7.4",
"@nx/storybook": "16.7.4",
"@nx/web": "16.7.4",
"@nx/workspace": "16.7.4",
"@nx/angular": "17.0.2",
"@nx/cypress": "17.0.2",
"@nx/eslint-plugin": "17.0.2",
"@nx/jest": "17.0.2",
"@nx/js": "17.0.2",
"@nx/nest": "17.0.2",
"@nx/node": "17.0.2",
"@nx/storybook": "17.0.2",
"@nx/web": "17.0.2",
"@nx/workspace": "17.0.2",
"@schematics/angular": "16.2.0",
"@simplewebauthn/typescript-types": "8.0.0",
"@storybook/addon-essentials": "7.3.2",
"@storybook/angular": "7.3.2",
"@storybook/core-server": "7.3.2",
"@storybook/addon-essentials": "7.5.1",
"@storybook/angular": "7.5.1",
"@storybook/core-server": "7.5.1",
"@types/big.js": "6.1.6",
"@types/body-parser": "1.19.2",
"@types/cache-manager": "3.4.2",
@ -187,8 +187,7 @@
"jest": "29.4.3",
"jest-environment-jsdom": "29.4.3",
"jest-preset-angular": "13.1.1",
"nx": "16.7.4",
"nx-cloud": "16.3.0",
"nx": "17.0.2",
"prettier": "3.0.3",
"prettier-plugin-organize-attributes": "1.0.0",
"react": "18.2.0",

View File

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "SymbolProfile"
ADD COLUMN "figi" TEXT,
ADD COLUMN "figiComposite" TEXT,
ADD COLUMN "figiShareClass" TEXT;

View File

@ -132,6 +132,9 @@ model SymbolProfile {
createdAt DateTime @default(now())
currency String
dataSource DataSource
figi String?
figiComposite String?
figiShareClass String?
id String @id @default(uuid())
isin String?
name String?

View File

@ -0,0 +1,5 @@
Date,Code,Currency,Price,Quantity,Action,Fee,Note
16-09-2021,MSFT,USD,298.580,5,buy,19.00,My first order 🤓
17/11/2021,MSFT,USD,0.62,5,dividend,0.00
01.01.2022,Penthouse Apartment,USD,500000.0,1,<invalid>,0.00
20500606,MSFT,USD,0.00,0,buy,0.00
1 Date,Code,Currency,Price,Quantity,Action,Fee,Note
2 16-09-2021,MSFT,USD,298.580,5,buy,19.00,My first order 🤓
3 17/11/2021,MSFT,USD,0.62,5,dividend,0.00
4 01.01.2022,Penthouse Apartment,USD,500000.0,1,<invalid>,0.00
5 20500606,MSFT,USD,0.00,0,buy,0.00

5201
yarn.lock

File diff suppressed because it is too large Load Diff