Compare commits

..

10 Commits

Author SHA1 Message Date
d9ea255c17 Release 1.24.0 (#201) 2021-07-07 21:45:57 +02:00
2c19d8c8e7 Feature/add balance to account (#193)
* Add balance attribute and calculate total balance

* Update changelog
2021-07-07 21:23:36 +02:00
db090229ce Feature/add total value in the create or edit transaction dialog (#192)
* Display total value

* Update changelog
2021-07-07 21:14:01 +02:00
fbe590ddb9 Feature/upgrade angular material css vars to 2.0.0 (#200)
* Upgrade angular-material-css-vars

* Update changelog
2021-07-05 21:53:30 +02:00
0d65136a9e Revert "Remove unneeded dependencies (#197)" (#199)
This reverts commit a062a3cee4.
2021-07-05 20:35:10 +02:00
dea87cc3cf Improve README.md (#198) 2021-07-04 22:19:09 +02:00
a062a3cee4 Remove unneeded dependencies (#197) 2021-07-04 22:11:47 +02:00
5b1b207a6f Feature/upgrade angular dependencies to version 12.0.x (#196)
* Update angular dependencies to version 12.0.X

* Update changelog
2021-07-04 21:55:25 +02:00
63cc7b2871 Feature/upgrade nestjs dependencies (#195)
* Upgrade @nestjs dependencies

* Update changelog
2021-07-04 21:45:53 +02:00
3986e8f879 Upgrade Nx to version 12.5.4 (#194) 2021-07-04 21:31:15 +02:00
40 changed files with 1130 additions and 889 deletions

View File

@ -5,6 +5,21 @@ 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.24.0 - 07.07.2021
### Added
- Added the total value in the create or edit transaction dialog
- Added a balance attribute to the account model
- Calculated the total balance (cash)
### Changed
- Upgraded `@angular/cdk` and `@angular/material` from version `11.0.4` to `12.0.6`
- Upgraded `@nestjs` dependencies
- Upgraded `angular-material-css-vars` from version `1.2.0` to `2.0.0`
- Upgraded `Nx` from version `12.3.6` to `12.5.4`
## 1.23.1 - 03.07.2021 ## 1.23.1 - 03.07.2021
### Fixed ### Fixed

View File

@ -15,7 +15,7 @@
</p> </p>
</div> </div>
**Ghostfolio** is an open source portfolio tracker based on web technology. The software empowers busy folks to have a sharp look of their financial assets and to make solid, data-driven investment decisions by evaluating automated static portfolio analysis rules. **Ghostfolio** is an open source portfolio tracker built with web technology. The software empowers busy people to have a sharp look of their financial assets and to make solid, data-driven investment decisions.
## Why Ghostfolio? ## Why Ghostfolio?
@ -79,8 +79,8 @@ The frontend is built with [Angular](https://angular.io) and uses [Angular Mater
1. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data 1. Run `yarn setup:database` to initialize the database schema and populate your database with (example) data
1. Start server and client (see [_Development_](#Development)) 1. Start server and client (see [_Development_](#Development))
1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9` 1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9`
1. Go to the _Admin Control Panel_ and press _Gather All Data_ to fetch historical data 1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data
1. Press _Sign out_ and check out the _Live Demo_ 1. Click _Sign out_ and check out the _Live Demo_
## Development ## Development

View File

@ -11,5 +11,6 @@ module.exports = {
}, },
moduleFileExtensions: ['ts', 'js', 'html'], moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/api', coverageDirectory: '../../coverage/apps/api',
testTimeout: 10000 testTimeout: 10000,
testEnvironment: 'node'
}; };

View File

@ -4,6 +4,7 @@ import { AlphaVantageService } from '@ghostfolio/api/services/data-provider/alph
import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service'; import { GhostfolioScraperApiService } from '@ghostfolio/api/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service';
import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service'; import { RakutenRapidApiService } from '@ghostfolio/api/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service';
import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@ -20,6 +21,7 @@ import { AccountService } from './account.service';
AlphaVantageService, AlphaVantageService,
ConfigurationService, ConfigurationService,
DataProviderService, DataProviderService,
ExchangeRateDataService,
GhostfolioScraperApiService, GhostfolioScraperApiService,
ImpersonationService, ImpersonationService,
PrismaService, PrismaService,

View File

@ -1,12 +1,14 @@
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Account, Order, Prisma } from '@prisma/client'; import { Account, Currency, Order, Prisma } from '@prisma/client';
import { RedisCacheService } from '../redis-cache/redis-cache.service'; import { RedisCacheService } from '../redis-cache/redis-cache.service';
@Injectable() @Injectable()
export class AccountService { export class AccountService {
public constructor( public constructor(
private exchangeRateDataService: ExchangeRateDataService,
private readonly redisCacheService: RedisCacheService, private readonly redisCacheService: RedisCacheService,
private prisma: PrismaService private prisma: PrismaService
) {} ) {}
@ -53,6 +55,24 @@ export class AccountService {
}); });
} }
public async calculateCashBalance(aUserId: string, aCurrency: Currency) {
let totalCashBalance = 0;
const accounts = await this.accounts({
where: { userId: aUserId }
});
accounts.forEach((account) => {
totalCashBalance += this.exchangeRateDataService.toCurrency(
account.balance,
account.currency,
aCurrency
);
});
return totalCashBalance;
}
public async createAccount( public async createAccount(
data: Prisma.AccountCreateInput, data: Prisma.AccountCreateInput,
aUserId: string aUserId: string

View File

@ -1,10 +1,16 @@
import { AccountType } from '@prisma/client'; import { AccountType, Currency } from '@prisma/client';
import { IsString, ValidateIf } from 'class-validator'; import { IsNumber, IsString, ValidateIf } from 'class-validator';
export class CreateAccountDto { export class CreateAccountDto {
@IsString() @IsString()
accountType: AccountType; accountType: AccountType;
@IsNumber()
balance: number;
@IsString()
currency: Currency;
@IsString() @IsString()
name: string; name: string;

View File

@ -1,10 +1,16 @@
import { AccountType } from '@prisma/client'; import { AccountType, Currency } from '@prisma/client';
import { IsString, ValidateIf } from 'class-validator'; import { IsNumber, IsString, ValidateIf } from 'class-validator';
export class UpdateAccountDto { export class UpdateAccountDto {
@IsString() @IsString()
accountType: AccountType; accountType: AccountType;
@IsNumber()
balance: number;
@IsString()
currency: Currency;
@IsString() @IsString()
id: string; id: string;

View File

@ -1,5 +1,5 @@
import { Currency, DataSource, Type } from '@prisma/client'; import { Currency, DataSource, Type } from '@prisma/client';
import { IsISO8601, IsNumber, IsString, ValidateIf } from 'class-validator'; import { IsISO8601, IsNumber, IsString } from 'class-validator';
export class CreateOrderDto { export class CreateOrderDto {
@IsString() @IsString()

View File

@ -142,7 +142,8 @@ export class PortfolioController {
): Promise<{ [symbol: string]: PortfolioPosition }> { ): Promise<{ [symbol: string]: PortfolioPosition }> {
let details: { [symbol: string]: PortfolioPosition } = {}; let details: { [symbol: string]: PortfolioPosition } = {};
const impersonationUserId = await this.impersonationService.validateImpersonationId( const impersonationUserId =
await this.impersonationService.validateImpersonationId(
impersonationId, impersonationId,
this.request.user.id this.request.user.id
); );
@ -221,6 +222,7 @@ export class PortfolioController {
) )
) { ) {
overview = nullifyValuesInObject(overview, [ overview = nullifyValuesInObject(overview, [
'cash',
'committedFunds', 'committedFunds',
'fees', 'fees',
'totalBuy', 'totalBuy',
@ -238,7 +240,8 @@ export class PortfolioController {
@Query('range') range, @Query('range') range,
@Res() res: Response @Res() res: Response
): Promise<PortfolioPerformance> { ): Promise<PortfolioPerformance> {
const impersonationUserId = await this.impersonationService.validateImpersonationId( const impersonationUserId =
await this.impersonationService.validateImpersonationId(
impersonationId, impersonationId,
this.request.user.id this.request.user.id
); );
@ -306,7 +309,8 @@ export class PortfolioController {
public async getReport( public async getReport(
@Headers('impersonation-id') impersonationId @Headers('impersonation-id') impersonationId
): Promise<PortfolioReport> { ): Promise<PortfolioReport> {
const impersonationUserId = await this.impersonationService.validateImpersonationId( const impersonationUserId =
await this.impersonationService.validateImpersonationId(
impersonationId, impersonationId,
this.request.user.id this.request.user.id
); );

View File

@ -1,3 +1,8 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheService } from '@ghostfolio/api/app/cache/cache.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { RedisCacheModule } from '@ghostfolio/api/app/redis-cache/redis-cache.module';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
@ -11,10 +16,6 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { RulesService } from '@ghostfolio/api/services/rules.service'; import { RulesService } from '@ghostfolio/api/services/rules.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CacheService } from '../cache/cache.service';
import { OrderService } from '../order/order.service';
import { RedisCacheModule } from '../redis-cache/redis-cache.module';
import { UserService } from '../user/user.service';
import { PortfolioController } from './portfolio.controller'; import { PortfolioController } from './portfolio.controller';
import { PortfolioService } from './portfolio.service'; import { PortfolioService } from './portfolio.service';
@ -22,6 +23,7 @@ import { PortfolioService } from './portfolio.service';
imports: [RedisCacheModule], imports: [RedisCacheModule],
controllers: [PortfolioController], controllers: [PortfolioController],
providers: [ providers: [
AccountService,
AlphaVantageService, AlphaVantageService,
CacheService, CacheService,
ConfigurationService, ConfigurationService,

View File

@ -1,3 +1,7 @@
import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service';
import { UserService } from '@ghostfolio/api/app/user/user.service';
import { Portfolio } from '@ghostfolio/api/models/portfolio'; import { Portfolio } from '@ghostfolio/api/models/portfolio';
import { DataProviderService } from '@ghostfolio/api/services/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
@ -30,9 +34,6 @@ import {
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import * as roundTo from 'round-to'; import * as roundTo from 'round-to';
import { OrderService } from '../order/order.service';
import { RedisCacheService } from '../redis-cache/redis-cache.service';
import { UserService } from '../user/user.service';
import { import {
HistoricalDataItem, HistoricalDataItem,
PortfolioPositionDetail PortfolioPositionDetail
@ -41,6 +42,7 @@ import {
@Injectable() @Injectable()
export class PortfolioService { export class PortfolioService {
public constructor( public constructor(
private readonly accountService: AccountService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly impersonationService: ImpersonationService, private readonly impersonationService: ImpersonationService,
@ -192,10 +194,15 @@ export class PortfolioService {
impersonationUserId || this.request.user.id impersonationUserId || this.request.user.id
); );
const cash = await this.accountService.calculateCashBalance(
impersonationUserId || this.request.user.id,
this.request.user.Settings.currency
);
const committedFunds = portfolio.getCommittedFunds(); const committedFunds = portfolio.getCommittedFunds();
const fees = portfolio.getFees(); const fees = portfolio.getFees();
return { return {
cash,
committedFunds, committedFunds,
fees, fees,
ordersCount: portfolio.getOrders().length, ordersCount: portfolio.getOrders().length,

View File

@ -110,7 +110,9 @@ describe('Portfolio', () => {
Account: [ Account: [
{ {
accountType: AccountType.SECURITIES, accountType: AccountType.SECURITIES,
balance: 0,
createdAt: new Date(), createdAt: new Date(),
currency: Currency.USD,
id: DEFAULT_ACCOUNT_ID, id: DEFAULT_ACCOUNT_ID,
isDefault: true, isDefault: true,
name: 'Default Account', name: 'Default Account',

View File

@ -5,13 +5,7 @@ module.exports = {
globals: { globals: {
'ts-jest': { 'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json', tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$', stringifyContentPathRegex: '\\.(html|svg)$'
astTransformers: {
before: [
'jest-preset-angular/build/InlineFilesTransformer',
'jest-preset-angular/build/StripStylesTransformer'
]
}
} }
}, },
coverageDirectory: '../../coverage/apps/client', coverageDirectory: '../../coverage/apps/client',
@ -19,5 +13,6 @@ module.exports = {
'jest-preset-angular/build/serializers/no-ng-attributes', 'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot', 'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment' 'jest-preset-angular/build/serializers/html-comment'
] ],
transform: { '^.+\\.(ts|js|html)$': 'jest-preset-angular' }
}; };

View File

@ -7,10 +7,6 @@
.create-account-box { .create-account-box {
cursor: pointer; cursor: pointer;
font-size: 90%; font-size: 90%;
.link {
color: rgba(var(--palette-primary-500), 1);
}
} }
} }

View File

@ -26,6 +26,27 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="transactions">
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>
Transactions
</th>
<td *matCellDef="let element" class="text-right" mat-cell>
{{ element.Order?.length }}
</td>
</ng-container>
<ng-container matColumnDef="balance">
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>Balance</th>
<td *matCellDef="let element" class="text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[currency]="element.currency"
[locale]="locale"
[value]="element.balance"
></gf-value>
</td>
</ng-container>
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th> <th *matHeaderCellDef class="px-1 text-center" i18n mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell> <td *matCellDef="let element" class="px-1 text-center" mat-cell>
@ -53,15 +74,6 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="transactions">
<th *matHeaderCellDef class="text-right" i18n mat-header-cell>
Transactions
</th>
<td *matCellDef="let element" class="text-right" mat-cell>
{{ element.Order?.length }}
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr> <tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
</table> </table>

View File

@ -28,7 +28,8 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
@Output() accountDeleted = new EventEmitter<string>(); @Output() accountDeleted = new EventEmitter<string>();
@Output() accountToUpdate = new EventEmitter<AccountModel>(); @Output() accountToUpdate = new EventEmitter<AccountModel>();
public dataSource: MatTableDataSource<AccountModel> = new MatTableDataSource(); public dataSource: MatTableDataSource<AccountModel> =
new MatTableDataSource();
public displayedColumns = []; public displayedColumns = [];
public isLoading = true; public isLoading = true;
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
@ -40,7 +41,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
public ngOnInit() {} public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
this.displayedColumns = ['account', 'platform', 'transactions']; this.displayedColumns = ['account', 'platform', 'transactions', 'balance'];
if (this.showActions) { if (this.showActions) {
this.displayedColumns.push('actions'); this.displayedColumns.push('actions');

View File

@ -5,10 +5,7 @@
z-index: 999; z-index: 999;
.mat-toolbar { .mat-toolbar {
background-color: rgba( background-color: rgba(var(--light-disabled-text));
var(--light-primary-text),
var(--palette-foreground-disabled-alpha)
);
.spacer { .spacer {
flex: 1 1 auto; flex: 1 1 auto;
@ -28,11 +25,6 @@
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {
.mat-toolbar { .mat-toolbar {
background-color: rgba( background-color: rgba(39, 39, 39, $alpha-disabled-text);
39,
39,
39,
var(--palette-foreground-disabled-alpha)
);
} }
} }

View File

@ -1,4 +1,18 @@
<div class="container p-0"> <div class="container p-0">
<div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Cash</div>
<div class="d-flex justify-content-end">
<gf-value
class="justify-content-end"
[currency]="baseCurrency"
[locale]="locale"
[value]="isLoading ? undefined : overview?.cash"
></gf-value>
</div>
</div>
<div class="row">
<div class="col"><hr /></div>
</div>
<div class="row px-3 py-1"> <div class="row px-3 py-1">
<div class="d-flex flex-grow-1" i18n>Buy</div> <div class="d-flex flex-grow-1" i18n>Buy</div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">

View File

@ -8,7 +8,7 @@ import {
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getCssVariable, getTextColor } from '@ghostfolio/common/helper'; import { getTextColor } from '@ghostfolio/common/helper';
import { PortfolioPosition } from '@ghostfolio/common/interfaces'; import { PortfolioPosition } from '@ghostfolio/common/interfaces';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';
import { Tooltip } from 'chart.js'; import { Tooltip } from 'chart.js';
@ -43,9 +43,7 @@ export class PortfolioProportionChartComponent
private colorMap: { private colorMap: {
[symbol: string]: string; [symbol: string]: string;
} = { } = {
[UNKNOWN_KEY]: `rgba(${getTextColor()}, ${getCssVariable( [UNKNOWN_KEY]: `rgba(${getTextColor()}, 0.12)`
'--palette-foreground-divider-alpha'
)})`
}; };
public constructor() { public constructor() {

View File

@ -1,12 +1,11 @@
@import '~apps/client/src/styles/ghostfolio-style';
:host { :host {
display: block; display: block;
gf-position { gf-position {
&:nth-child(even) { &:nth-child(even) {
background-color: rgba( background-color: rgba(0, 0, 0, $alpha-hover);
var(--dark-primary-text),
var(--palette-background-hover-alpha)
);
} }
} }
} }
@ -14,10 +13,7 @@
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {
gf-position { gf-position {
&:nth-child(even) { &:nth-child(even) {
background-color: rgba( background-color: rgba(255, 255, 255, $alpha-hover);
var(--light-primary-text),
var(--palette-background-hover-alpha)
);
} }
} }
} }

View File

@ -7,10 +7,7 @@
padding: 0.15rem 0.75rem; padding: 0.15rem 0.75rem;
&.mat-radio-checked { &.mat-radio-checked {
background-color: rgba( background-color: rgba(var(--dark-dividers));
var(--dark-primary-text),
var(--palette-foreground-divider-alpha)
);
} }
::ng-deep { ::ng-deep {
@ -33,15 +30,8 @@
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {
.mat-radio-button { .mat-radio-button {
&.mat-radio-checked { &.mat-radio-checked {
background-color: rgba( background-color: rgba(var(--light-dividers));
var(--light-primary-text), border: 1px solid rgba(var(--light-disabled-text));
var(--palette-foreground-divider-alpha)
);
border: 1px solid
rgba(
var(--light-primary-text),
var(--palette-foreground-disabled-button-alpha)
);
} }
::ng-deep { ::ng-deep {

View File

@ -12,7 +12,7 @@ import {
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { baseCurrency, DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Currency } from '@prisma/client'; import { Currency } from '@prisma/client';

View File

@ -125,6 +125,8 @@ export class AccountsPageComponent implements OnInit {
public openUpdateAccountDialog({ public openUpdateAccountDialog({
accountType, accountType,
balance,
currency,
id, id,
name, name,
platformId platformId
@ -133,6 +135,8 @@ export class AccountsPageComponent implements OnInit {
data: { data: {
account: { account: {
accountType, accountType,
balance,
currency,
id, id,
name, name,
platformId platformId
@ -167,6 +171,8 @@ export class AccountsPageComponent implements OnInit {
data: { data: {
account: { account: {
accountType: AccountType.SECURITIES, accountType: AccountType.SECURITIES,
balance: 0,
currency: this.user?.settings?.baseCurrency,
name: null, name: null,
platformId: null platformId: null
} }

View File

@ -8,14 +8,37 @@
<input matInput name="name" required [(ngModel)]="data.account.name" /> <input matInput name="name" required [(ngModel)]="data.account.name" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="d-none"> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Type</mat-label> <mat-label i18n>Type</mat-label>
<mat-select name="type" required [(value)]="data.account.accountType"> <mat-select name="type" required [(value)]="data.account.accountType">
<mat-option value="SECURITIES" i18n> SECURITIES </mat-option> <mat-option value="CASH" i18n>Cash</mat-option>
<mat-option value="SECURITIES" i18n>Securities</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label>
<mat-select name="currency" required [(value)]="data.account.currency">
<mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Balance</mat-label>
<input
matInput
name="balance"
required
type="number"
[(ngModel)]="data.account.balance"
/>
</mat-form-field>
</div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Platform</mat-label> <mat-label i18n>Platform</mat-label>

View File

@ -23,7 +23,7 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces';
@Component({ @Component({
host: { class: 'h-100' }, host: { class: 'h-100' },
selector: 'create-or-update-transaction-dialog', selector: 'gf-create-or-update-transaction-dialog',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./create-or-update-transaction-dialog.scss'], styleUrls: ['./create-or-update-transaction-dialog.scss'],
templateUrl: 'create-or-update-transaction-dialog.html' templateUrl: 'create-or-update-transaction-dialog.html'

View File

@ -2,6 +2,22 @@
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1> <h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1> <h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
<div class="flex-grow-1" mat-dialog-content> <div class="flex-grow-1" mat-dialog-content>
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account</mat-label>
<mat-select
name="accountId"
required
[(value)]="data.transaction.accountId"
>
<mat-option
*ngFor="let account of data.user?.accounts"
[value]="account.id"
>{{ account.name }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
<div> <div>
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol or ISIN</mat-label> <mat-label i18n>Symbol or ISIN</mat-label>
@ -42,7 +58,7 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div> <div class="d-none">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label> <mat-label i18n>Currency</mat-label>
<mat-select <mat-select
@ -136,22 +152,15 @@
</button> </button>
</mat-form-field> </mat-form-field>
</div> </div>
</div>
<div class="d-flex" mat-dialog-actions>
<gf-value
class="flex-grow-1"
[currency]="data.transaction.currency"
[locale]="data.user?.settings?.locale"
[value]="data.transaction.fee + (data.transaction.quantity * data.transaction.unitPrice)"
></gf-value>
<div> <div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account</mat-label>
<mat-select
name="accountId"
required
[(value)]="data.transaction.accountId"
>
<mat-option *ngFor="let account of data.accounts" [value]="account.id"
>{{ account.name }}</mat-option
>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button> <button i18n mat-button (click)="onCancel()">Cancel</button>
<button <button
color="primary" color="primary"
@ -163,4 +172,5 @@
Save Save
</button> </button>
</div> </div>
</div>
</form> </form>

View File

@ -9,6 +9,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { GfValueModule } from '@ghostfolio/client/components/value/value.module';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component'; import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-dialog.component';
@ -19,6 +20,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
imports: [ imports: [
CommonModule, CommonModule,
GfSymbolModule, GfSymbolModule,
GfValueModule,
FormsModule, FormsModule,
MatAutocompleteModule, MatAutocompleteModule,
MatButtonModule, MatButtonModule,

View File

@ -1,6 +1,12 @@
:host { :host {
display: block; display: block;
.mat-dialog-actions {
gf-value {
font-size: 0.9rem;
}
}
.mat-dialog-content { .mat-dialog-content {
max-height: unset; max-height: unset;

View File

@ -1,7 +1,8 @@
import { User } from '@ghostfolio/common/interfaces';
import { Account, Order } from '@prisma/client'; import { Account, Order } from '@prisma/client';
export interface CreateOrUpdateTransactionDialogParams { export interface CreateOrUpdateTransactionDialogParams {
accountId: string; accountId: string;
accounts: Account[];
transaction: Order; transaction: Order;
user: User;
} }

View File

@ -141,7 +141,6 @@ export class TransactionsPageComponent implements OnInit {
}: OrderModel): void { }: OrderModel): void {
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
data: { data: {
accounts: this.user.accounts,
transaction: { transaction: {
accountId, accountId,
currency, currency,
@ -153,7 +152,8 @@ export class TransactionsPageComponent implements OnInit {
symbol, symbol,
type, type,
unitPrice unitPrice
} },
user: this.user
}, },
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'
@ -182,7 +182,6 @@ export class TransactionsPageComponent implements OnInit {
private openCreateTransactionDialog(aTransaction?: OrderModel): void { private openCreateTransactionDialog(aTransaction?: OrderModel): void {
const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, { const dialogRef = this.dialog.open(CreateOrUpdateTransactionDialog, {
data: { data: {
accounts: this.user?.accounts,
transaction: { transaction: {
accountId: accountId:
aTransaction?.accountId ?? aTransaction?.accountId ??
@ -197,7 +196,8 @@ export class TransactionsPageComponent implements OnInit {
symbol: aTransaction?.symbol ?? null, symbol: aTransaction?.symbol ?? null,
type: aTransaction?.type ?? 'BUY', type: aTransaction?.type ?? 'BUY',
unitPrice: null unitPrice: null
} },
user: this.user
}, },
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType === 'mobile' ? '100vw' : '50rem'

View File

@ -43,10 +43,7 @@ body {
} }
hr { hr {
border-top-color: rgba( border-top-color: rgba(var(--light-dividers));
var(--light-primary-text),
var(--palette-foreground-divider-alpha)
);
} }
ngx-skeleton-loader { ngx-skeleton-loader {
@ -63,10 +60,7 @@ body {
background: var(--dark-background); background: var(--dark-background);
&:not([class*='mat-elevation-z']) { &:not([class*='mat-elevation-z']) {
border-color: rgba( border-color: rgba(var(--light-dividers));
var(--light-primary-text),
var(--palette-foreground-divider-alpha)
);
box-shadow: none; box-shadow: none;
} }
} }
@ -102,8 +96,7 @@ button:focus {
} }
hr { hr {
border-top: 1px solid border-top: 1px solid rgba(var(--dark-dividers));
rgba(var(--dark-primary-text), var(--palette-foreground-divider-alpha));
} }
ion-icon { ion-icon {
@ -138,8 +131,7 @@ ngx-skeleton-loader {
.mat-card { .mat-card {
&:not([class*='mat-elevation-z']) { &:not([class*='mat-elevation-z']) {
border: 1px solid border: 1px solid rgba(var(--dark-dividers));
rgba(var(--dark-primary-text), var(--palette-foreground-divider-alpha));
box-shadow: none; box-shadow: none;
} }
} }

View File

@ -2,6 +2,9 @@ $mat-css-dark-theme-selector: '.is-dark-theme';
@import '~angular-material-css-vars/public-util'; @import '~angular-material-css-vars/public-util';
$alpha-disabled-text: 0.38;
$alpha-hover: 0.04;
.gf-table { .gf-table {
td { td {
border: 0; border: 0;

View File

@ -26,11 +26,15 @@ export function getCssVariable(aCssVariable: string) {
} }
export function getTextColor() { export function getTextColor() {
return getCssVariable( const cssVariable = getCssVariable(
window.matchMedia('(prefers-color-scheme: dark)').matches window.matchMedia('(prefers-color-scheme: dark)').matches
? '--light-primary-text' ? '--light-primary-text'
: '--dark-primary-text' : '--dark-primary-text'
); );
const [r, g, b] = cssVariable.split(',');
return `${r}, ${g}, ${b}`;
} }
export function getToday() { export function getToday() {

View File

@ -1,4 +1,5 @@
export interface PortfolioOverview { export interface PortfolioOverview {
cash: number;
committedFunds: number; committedFunds: number;
fees: number; fees: number;
ordersCount: number; ordersCount: number;

View File

@ -35,5 +35,13 @@
"common": { "common": {
"tags": [] "tags": []
} }
},
"targetDependencies": {
"build": [
{
"target": "build",
"projects": "dependencies"
}
]
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "1.23.1", "version": "1.24.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"scripts": { "scripts": {
@ -46,32 +46,32 @@
}, },
"dependencies": { "dependencies": {
"@angular/animations": "12.0.4", "@angular/animations": "12.0.4",
"@angular/cdk": "11.0.4", "@angular/cdk": "12.0.6",
"@angular/common": "12.0.4", "@angular/common": "12.0.4",
"@angular/compiler": "12.0.4", "@angular/compiler": "12.0.4",
"@angular/core": "12.0.4", "@angular/core": "12.0.4",
"@angular/forms": "12.0.4", "@angular/forms": "12.0.4",
"@angular/material": "11.0.4", "@angular/material": "12.0.6",
"@angular/platform-browser": "12.0.4", "@angular/platform-browser": "12.0.4",
"@angular/platform-browser-dynamic": "12.0.4", "@angular/platform-browser-dynamic": "12.0.4",
"@angular/router": "12.0.4", "@angular/router": "12.0.4",
"@codewithdan/observable-store": "2.2.11", "@codewithdan/observable-store": "2.2.11",
"@nestjs/common": "7.6.5", "@nestjs/common": "7.6.18",
"@nestjs/config": "0.6.1", "@nestjs/config": "0.6.3",
"@nestjs/core": "7.6.5", "@nestjs/core": "7.6.18",
"@nestjs/jwt": "7.2.0", "@nestjs/jwt": "7.2.0",
"@nestjs/passport": "7.1.5", "@nestjs/passport": "7.1.6",
"@nestjs/platform-express": "7.6.5", "@nestjs/platform-express": "7.6.18",
"@nestjs/schedule": "0.4.1", "@nestjs/schedule": "0.4.3",
"@nestjs/serve-static": "2.1.4", "@nestjs/serve-static": "2.1.4",
"@nrwl/angular": "12.3.6", "@nrwl/angular": "12.5.4",
"@prisma/client": "2.24.1", "@prisma/client": "2.24.1",
"@simplewebauthn/browser": "3.0.0", "@simplewebauthn/browser": "3.0.0",
"@simplewebauthn/server": "3.0.0", "@simplewebauthn/server": "3.0.0",
"@simplewebauthn/typescript-types": "3.0.0", "@simplewebauthn/typescript-types": "3.0.0",
"@stripe/stripe-js": "1.15.0", "@stripe/stripe-js": "1.15.0",
"alphavantage": "2.2.0", "alphavantage": "2.2.0",
"angular-material-css-vars": "1.2.0", "angular-material-css-vars": "2.0.0",
"bent": "7.3.12", "bent": "7.3.12",
"bootstrap": "4.6.0", "bootstrap": "4.6.0",
"cache-manager": "3.4.3", "cache-manager": "3.4.3",
@ -104,7 +104,7 @@
"svgmap": "2.1.1", "svgmap": "2.1.1",
"uuid": "8.3.2", "uuid": "8.3.2",
"yahoo-finance": "0.3.6", "yahoo-finance": "0.3.6",
"zone.js": "~0.11.4" "zone.js": "0.11.4"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "12.0.4", "@angular-devkit/build-angular": "12.0.4",
@ -112,17 +112,17 @@
"@angular/cli": "12.0.4", "@angular/cli": "12.0.4",
"@angular/compiler-cli": "12.0.4", "@angular/compiler-cli": "12.0.4",
"@angular/language-service": "12.0.4", "@angular/language-service": "12.0.4",
"@angular/localize": "11.0.9", "@angular/localize": "12.0.5",
"@nestjs/schematics": "7.2.6", "@nestjs/schematics": "7.3.1",
"@nestjs/testing": "7.6.5", "@nestjs/testing": "7.6.18",
"@nrwl/cli": "12.3.6", "@nrwl/cli": "12.5.4",
"@nrwl/cypress": "12.3.6", "@nrwl/cypress": "12.5.4",
"@nrwl/eslint-plugin-nx": "12.3.6", "@nrwl/eslint-plugin-nx": "12.5.4",
"@nrwl/jest": "12.3.6", "@nrwl/jest": "12.5.4",
"@nrwl/nest": "12.3.6", "@nrwl/nest": "12.5.4",
"@nrwl/node": "12.3.6", "@nrwl/node": "12.5.4",
"@nrwl/tao": "12.3.6", "@nrwl/tao": "12.5.4",
"@nrwl/workspace": "12.3.6", "@nrwl/workspace": "12.5.4",
"@types/cache-manager": "3.4.0", "@types/cache-manager": "3.4.0",
"@types/jest": "26.0.20", "@types/jest": "26.0.20",
"@types/lodash": "4.14.168", "@types/lodash": "4.14.168",
@ -139,12 +139,12 @@
"import-sort-cli": "6.0.0", "import-sort-cli": "6.0.0",
"import-sort-parser-typescript": "6.0.0", "import-sort-parser-typescript": "6.0.0",
"import-sort-style-module": "6.0.0", "import-sort-style-module": "6.0.0",
"jest": "26.6.3", "jest": "27.0.3",
"jest-preset-angular": "8.4.0", "jest-preset-angular": "9.0.3",
"prettier": "2.3.1", "prettier": "2.3.2",
"replace-in-file": "6.2.0", "replace-in-file": "6.2.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"ts-jest": "26.5.5", "ts-jest": "27.0.3",
"ts-node": "9.1.1", "ts-node": "9.1.1",
"typescript": "4.2.4" "typescript": "4.2.4"
}, },

View File

@ -0,0 +1,6 @@
-- AlterEnum
ALTER TYPE "AccountType" ADD VALUE 'CASH';
-- AlterTable
ALTER TABLE "Account" ADD COLUMN "balance" DOUBLE PRECISION NOT NULL DEFAULT 0,
ADD COLUMN "currency" "Currency" NOT NULL DEFAULT E'USD';

View File

@ -26,7 +26,9 @@ model Access {
model Account { model Account {
accountType AccountType @default(SECURITIES) accountType AccountType @default(SECURITIES)
balance Float @default(0)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
currency Currency @default(USD)
id String @default(uuid()) id String @default(uuid())
isDefault Boolean @default(false) isDefault Boolean @default(false)
name String? name String?
@ -158,6 +160,7 @@ model User {
} }
enum AccountType { enum AccountType {
CASH
SECURITIES SECURITIES
} }

View File

@ -87,6 +87,8 @@ async function main() {
create: [ create: [
{ {
accountType: AccountType.SECURITIES, accountType: AccountType.SECURITIES,
balance: 0,
currency: Currency.USD,
id: 'f4425b66-9ba9-4ac4-93d7-fdf9a145e8cb', id: 'f4425b66-9ba9-4ac4-93d7-fdf9a145e8cb',
isDefault: true, isDefault: true,
name: 'Default Account' name: 'Default Account'
@ -109,18 +111,24 @@ async function main() {
create: [ create: [
{ {
accountType: AccountType.SECURITIES, accountType: AccountType.SECURITIES,
balance: 0,
currency: Currency.USD,
id: 'd804de69-0429-42dc-b6ca-b308fd7dd926', id: 'd804de69-0429-42dc-b6ca-b308fd7dd926',
name: 'Coinbase Account', name: 'Coinbase Account',
platformId: platformCoinbase.id platformId: platformCoinbase.id
}, },
{ {
accountType: AccountType.SECURITIES, accountType: AccountType.SECURITIES,
balance: 0,
currency: Currency.EUR,
id: '65cfb79d-b6c7-4591-9d46-73426bc62094', id: '65cfb79d-b6c7-4591-9d46-73426bc62094',
name: 'DEGIRO Account', name: 'DEGIRO Account',
platformId: platformDegiro.id platformId: platformDegiro.id
}, },
{ {
accountType: AccountType.SECURITIES, accountType: AccountType.SECURITIES,
balance: 0,
currency: Currency.USD,
id: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c', id: '480269ce-e12a-4fd1-ac88-c4b0ff3f899c',
isDefault: true, isDefault: true,
name: 'Interactive Brokers Account', name: 'Interactive Brokers Account',

1565
yarn.lock

File diff suppressed because it is too large Load Diff