Compare commits

...

17 Commits

Author SHA1 Message Date
7b5454e7de Release 1.167.0 (#1065) 2022-07-07 21:08:32 +02:00
30835ced88 Bugfix/fix holdings for basic users (#1064)
* Fix holdings for basic users

* Update changelog
2022-07-07 21:06:12 +02:00
8897f32bc5 Clean up modules (#1063) 2022-07-07 07:04:23 +02:00
abaa6b5f27 Feature/improve create account link in live demo (#1061)
* Improve router link

* Update changelog
2022-07-06 17:49:19 +02:00
2060fcaf0b Feature/add markets to public pages (#1062)
* Add Markets to public pages

* Update changelog
2022-07-05 21:45:27 +02:00
fd2408dd62 fix: add git when building docker image (#1052) 2022-07-03 19:57:04 +02:00
31cca024f1 Feature/upgrade ngx markdown to version 14.0.1 (#1055)
* Upgrade ngx-markdown

* Update changelog
2022-07-02 10:58:46 +02:00
b535122945 Make use of demo route (#1060) 2022-07-01 20:07:49 +02:00
5113e4e3ad Release 1.166.0 (#1059) 2022-06-30 21:09:58 +02:00
35e039748f Feature/refactor demo account as route (#1058)
* Refactor demo account as route

* Update changelog
2022-06-30 21:07:35 +02:00
c6b9e0aa5b Feature/upgrade zone.js to version 0.11.6 (#1054)
* Upgrade zone.js

* Update changelog
2022-06-30 19:38:33 +02:00
b250491ca5 Feature/upgrade yahoo finance2 to version 2.3.3 (#1053)
* Upgrade yahoo-finance2 to version 2.3.3

* Update changelog
2022-06-30 08:04:39 +02:00
61e501c659 Feature/fix version of @angular/cli (#1056)
* Fix version

* Update yarn.lock
2022-06-29 21:07:55 +02:00
c0f19d56ec Feature/add account detail dialog (#1047)
* Add account detail dialog

* Update changelog
2022-06-28 21:08:34 +02:00
8e2b235b1f Feature/improve search label (#1048)
* Improve search label

* Update changelog
2022-06-28 13:33:59 +02:00
c3407e9b34 Feature/upgrade prisma to version 3.15.2 (#1046)
* Upgrade prisma to version 3.15.2

* Update changelog
2022-06-25 19:04:01 +02:00
74193e4ee2 Feature/upgrade nestjs dependencies to version 8.4.7 (#1045)
* Upgrade nestjs dependencies to version 8.4.7

* Update changelog
2022-06-25 19:03:25 +02:00
101 changed files with 1704 additions and 707 deletions

View File

@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.167.0 - 07.07.2022
### Added
- Added _Markets_ to the public pages
### Changed
- Improved the _Create Account_ link in the _Live Demo_
- Upgraded `ngx-markdown` from version `13.0.0` to `14.0.1`
### Fixed
- Fixed an issue in the _Holdings_ section for users without a subscription
## 1.166.0 - 30.06.2022
### Added
- Added an account detail dialog
### Changed
- Improved the label of the (symbol) search
- Refactored the demo account as a route (`/demo`)
- Upgraded `nestjs` from version `8.2.3` to `8.4.7`
- Upgraded `prisma` from version `3.14.0` to `3.15.2`
- Upgraded `yahoo-finance2` from version `2.3.2` to `2.3.3`
- Upgraded `zone.js` from version `0.11.4` to `0.11.6`
## 1.165.0 - 25.06.2022
### Added

View File

@ -12,7 +12,7 @@ COPY ./package.json package.json
COPY ./yarn.lock yarn.lock
COPY ./prisma/schema.prisma prisma/schema.prisma
RUN apk add --no-cache python3 g++ make openssl
RUN apk add --no-cache python3 g++ make openssl git
RUN yarn install
# See https://github.com/nrwl/nx/issues/6586 for further details

View File

@ -12,7 +12,7 @@
<strong>Open Source Wealth Management Software</strong>
</p>
<p>
<a href="https://ghostfol.io"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
<a href="https://ghostfol.io"><strong>Ghostfol.io</strong></a> | <a href="https://ghostfol.io/demo"><strong>Live Demo</strong></a> | <a href="https://ghostfol.io/pricing"><strong>Ghostfolio Premium</strong></a> | <a href="https://ghostfol.io/blog"><strong>Blog</strong></a> | <a href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"><strong>Slack</strong></a> | <a href="https://twitter.com/ghostfolio_"><strong>Twitter</strong></a>
</p>
<p>
<a href="#contributing">

View File

@ -115,7 +115,7 @@
}
],
"styles": ["apps/client/src/styles.scss"],
"scripts": ["node_modules/marked/lib/marked.js"],
"scripts": ["node_modules/marked/marked.min.js"],
"vendorChunk": true,
"extractLicenses": false,
"buildOptimizer": false,

View File

@ -7,7 +7,10 @@ import {
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Accounts } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import type {
AccountWithValue,
RequestWithUser
} from '@ghostfolio/common/types';
import {
Body,
Controller,
@ -123,13 +126,45 @@ export class AccountController {
@Get(':id')
@UseGuards(AuthGuard('jwt'))
public async getAccountById(@Param('id') id: string): Promise<AccountModel> {
return this.accountService.account({
id_userId: {
id,
userId: this.request.user.id
}
});
public async getAccountById(
@Headers('impersonation-id') impersonationId,
@Param('id') id: string
): Promise<AccountWithValue> {
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
impersonationId,
this.request.user.id
);
let accountsWithAggregations =
await this.portfolioService.getAccountsWithAggregations(
impersonationUserId || this.request.user.id,
[{ id, type: 'ACCOUNT' }]
);
if (
impersonationUserId ||
this.userService.isRestrictedView(this.request.user)
) {
accountsWithAggregations = {
...nullifyValuesInObject(accountsWithAggregations, [
'totalBalanceInBaseCurrency',
'totalValueInBaseCurrency'
]),
accounts: nullifyValuesInObjects(accountsWithAggregations.accounts, [
'balance',
'balanceInBaseCurrency',
'convertedBalance',
'fee',
'quantity',
'unitPrice',
'value',
'valueInBaseCurrency'
])
};
}
return accountsWithAggregations.accounts[0];
}
@Post()

View File

@ -3,8 +3,7 @@ import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interc
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { PROPERTY_BENCHMARKS } from '@ghostfolio/common/config';
import { BenchmarkResponse, UniqueAsset } from '@ghostfolio/common/interfaces';
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Controller, Get, UseInterceptors } from '@nestjs/common';
import { BenchmarkService } from './benchmark.service';
@ -16,7 +15,6 @@ export class BenchmarkController {
) {}
@Get()
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getBenchmark(): Promise<BenchmarkResponse> {

View File

@ -63,6 +63,8 @@ export class InfoService {
} else {
info.fearAndGreedDataSource = ghostfolioFearAndGreedIndexDataSource;
}
globalPermissions.push(permissions.enableFearAndGreedIndex);
}
if (this.configurationService.get('ENABLE_FEATURE_IMPORT')) {

View File

@ -4,6 +4,7 @@ import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation.service';
import { Filter } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import type { RequestWithUser } from '@ghostfolio/common/types';
import {
@ -17,6 +18,7 @@ import {
Param,
Post,
Put,
Query,
UseGuards,
UseInterceptors
} from '@nestjs/common';
@ -66,8 +68,36 @@ export class OrderController {
@UseInterceptors(RedactValuesInResponseInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getAllOrders(
@Headers('impersonation-id') impersonationId
@Headers('impersonation-id') impersonationId,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('tags') filterByTags?: string
): Promise<Activities> {
const accountIds = filterByAccounts?.split(',') ?? [];
const assetClasses = filterByAssetClasses?.split(',') ?? [];
const tagIds = filterByTags?.split(',') ?? [];
const filters: Filter[] = [
...accountIds.map((accountId) => {
return <Filter>{
id: accountId,
type: 'ACCOUNT'
};
}),
...assetClasses.map((assetClass) => {
return <Filter>{
id: assetClass,
type: 'ASSET_CLASS'
};
}),
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,
type: 'TAG'
};
})
];
const impersonationUserId =
await this.impersonationService.validateImpersonationId(
impersonationId,
@ -76,6 +106,7 @@ export class OrderController {
const userCurrency = this.request.user.Settings.currency;
let activities = await this.orderService.getOrders({
filters,
userCurrency,
includeDrafts: true,
userId: impersonationUserId || this.request.user.id

View File

@ -190,14 +190,27 @@ export class PortfolioController {
}
}
const isBasicUser =
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
this.request.user.subscription.type === 'Basic';
let hasDetails = true;
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
hasDetails = this.request.user.subscription.type === 'Premium';
}
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
holdings[symbol] = {
...portfolioPosition,
assetClass: hasDetails ? portfolioPosition.assetClass : undefined,
assetSubClass: hasDetails ? portfolioPosition.assetSubClass : undefined,
countries: hasDetails ? portfolioPosition.countries : [],
currency: hasDetails ? portfolioPosition.currency : undefined,
markets: hasDetails ? portfolioPosition.markets : undefined,
sectors: hasDetails ? portfolioPosition.sectors : []
};
}
return {
accounts,
hasError,
holdings: isBasicUser ? {} : holdings
holdings
};
}
@ -340,8 +353,8 @@ export class PortfolioController {
portfolioPublicDetails.holdings[symbol] = {
allocationCurrent: portfolioPosition.value / totalValue,
countries: hasDetails ? portfolioPosition.countries : [],
currency: portfolioPosition.currency,
markets: portfolioPosition.markets,
currency: hasDetails ? portfolioPosition.currency : undefined,
markets: hasDetails ? portfolioPosition.markets : undefined,
name: portfolioPosition.name,
netPerformancePercent: portfolioPosition.netPerformancePercent,
sectors: hasDetails ? portfolioPosition.sectors : [],

View File

@ -50,6 +50,7 @@ import { REQUEST } from '@nestjs/core';
import {
AssetClass,
DataSource,
Prisma,
Tag,
Type as TypeOfOrder
} from '@prisma/client';
@ -100,14 +101,23 @@ export class PortfolioService {
this.baseCurrency = this.configurationService.get('BASE_CURRENCY');
}
public async getAccounts(aUserId: string): Promise<AccountWithValue[]> {
public async getAccounts(
aUserId: string,
aFilters?: Filter[]
): Promise<AccountWithValue[]> {
const where: Prisma.AccountWhereInput = { userId: aUserId };
if (aFilters?.[0].id && aFilters?.[0].type === 'ACCOUNT') {
where.id = aFilters[0].id;
}
const [accounts, details] = await Promise.all([
this.accountService.accounts({
where,
include: { Order: true, Platform: true },
orderBy: { name: 'asc' },
where: { userId: aUserId }
orderBy: { name: 'asc' }
}),
this.getDetails(aUserId, aUserId)
this.getDetails(aUserId, aUserId, undefined, aFilters)
]);
const userCurrency = this.request.user.Settings.currency;
@ -145,8 +155,11 @@ export class PortfolioService {
});
}
public async getAccountsWithAggregations(aUserId: string): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId);
public async getAccountsWithAggregations(
aUserId: string,
aFilters?: Filter[]
): Promise<Accounts> {
const accounts = await this.getAccounts(aUserId, aFilters);
let totalBalanceInBaseCurrency = new Big(0);
let totalValueInBaseCurrency = new Big(0);
let transactionCount = 0;
@ -1290,6 +1303,10 @@ export class PortfolioService {
if (filters.length === 0) {
currentAccounts = await this.accountService.getAccounts(userId);
} else if (filters.length === 1 && filters[0].type === 'ACCOUNT') {
currentAccounts = await this.accountService.accounts({
where: { id: filters[0].id }
});
} else {
const accountIds = uniq(
orders.map(({ accountId }) => {

View File

@ -46,7 +46,6 @@ export class SymbolController {
* Must be after /lookup
*/
@Get(':dataSource/:symbol')
@UseGuards(AuthGuard('jwt'))
@UseInterceptors(TransformDataSourceInRequestInterceptor)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getSymbolData(

View File

@ -158,10 +158,6 @@ export class UserService {
let currentPermissions = getPermissions(user.role);
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
currentPermissions.push(permissions.accessFearAndGreedIndex);
}
if (user.subscription?.type === 'Premium') {
currentPermissions.push(permissions.reportDataGlitch);
}

View File

@ -59,6 +59,11 @@ const routes: Routes = [
'./pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.module'
).then((m) => m.HalloGhostfolioPageModule)
},
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
},
{
path: 'en/blog/2021/07/hello-ghostfolio',
loadChildren: () =>
@ -85,6 +90,13 @@ const routes: Routes = [
loadChildren: () =>
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
},
{
path: 'markets',
loadChildren: () =>
import('./pages/markets/markets-page.module').then(
(m) => m.MarketsPageModule
)
},
{
path: 'p',
loadChildren: () =>

View File

@ -15,13 +15,17 @@
>
<div class="row">
<div class="col-md-8 offset-md-2 text-center">
<a *ngIf="canCreateAccount" class="text-center" [routerLink]="['/']">
<a
*ngIf="canCreateAccount"
class="text-center"
[routerLink]="['/register']"
>
<div
class="cursor-pointer d-inline-block info-message px-3 py-2"
(click)="onCreateAccount()"
>
<span i18n>You are using the Live Demo.</span>
<a class="ml-2" href="#" i18n>Create Account</a>
<span class="a ml-2" i18n>Create Account</span>
</div></a
>
<div

View File

@ -17,7 +17,7 @@
border-radius: 2rem;
font-size: 80%;
a {
.a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
}

View File

@ -1,6 +1,8 @@
import { Platform } from '@angular/cdk/platform';
import { HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
import {
DateAdapter,
MAT_DATE_FORMATS,
@ -38,6 +40,8 @@ export function NgxStripeFactory(): string {
GfHeaderModule,
HttpClientModule,
MarkdownModule.forRoot(),
MatAutocompleteModule,
MatChipsModule,
MaterialCssVarsModule.forRoot({
darkThemeClass: 'is-dark-theme',
isAutoContrast: true,

View File

@ -10,7 +10,6 @@ import { AccessTableComponent } from './access-table.component';
declarations: [AccessTableComponent],
exports: [AccessTableComponent],
imports: [CommonModule, MatButtonModule, MatMenuModule, MatTableModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPortfolioAccessTableModule {}

View File

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

View File

@ -0,0 +1,112 @@
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Inject,
OnDestroy,
OnInit
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { AccountType } from '@prisma/client';
import { format, parseISO } from 'date-fns';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AccountDetailDialogParams } from './interfaces/interfaces';
@Component({
host: { class: 'd-flex flex-column h-100' },
selector: 'gf-account-detail-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'account-detail-dialog.html',
styleUrls: ['./account-detail-dialog.component.scss']
})
export class AccountDetailDialog implements OnDestroy, OnInit {
public accountType: AccountType;
public name: string;
public orders: OrderWithAccount[];
public platformName: string;
public user: User;
public valueInBaseCurrency: number;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService,
public dialogRef: MatDialogRef<AccountDetailDialog>,
private userService: UserService
) {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit(): void {
this.dataService
.fetchAccount(this.data.accountId)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ accountType, name, Platform, valueInBaseCurrency }) => {
this.accountType = accountType;
this.name = name;
this.platformName = Platform?.name;
this.valueInBaseCurrency = valueInBaseCurrency;
this.changeDetectorRef.markForCheck();
});
this.dataService
.fetchActivities({
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.orders = activities;
this.changeDetectorRef.markForCheck();
});
}
public onClose(): void {
this.dialogRef.close();
}
public onExport() {
this.dataService
.fetchExport(
this.orders.map((order) => {
return order.id;
})
)
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((data) => {
downloadAsFile({
content: data,
fileName: `ghostfolio-export-${this.name
.replace(/\s+/g, '-')
.toLowerCase()}-${format(
parseISO(data.meta.date),
'yyyyMMddHHmm'
)}.json`,
format: 'json'
});
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,65 @@
<gf-dialog-header
mat-dialog-title
position="center"
[deviceType]="data.deviceType"
[title]="name"
(closeButtonClicked)="onClose()"
></gf-dialog-header>
<div class="flex-grow-1" mat-dialog-content>
<div class="container p-0">
<div class="row">
<div class="col-12 d-flex justify-content-center mb-3">
<gf-value
size="large"
[currency]="user?.settings?.baseCurrency"
[locale]="user?.settings?.locale"
[value]="valueInBaseCurrency"
></gf-value>
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<gf-value
label="Account Type"
size="medium"
[value]="accountType"
></gf-value>
</div>
<div class="col-6 mb-3">
<gf-value
label="Platform"
size="medium"
[value]="platformName"
></gf-value>
</div>
</div>
<div *ngIf="orders?.length > 0" class="row">
<div class="col mb-3">
<div class="h5 mb-0" i18n>Activities</div>
<gf-activities-table
[activities]="orders"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId"
[hasPermissionToFilter]="false"
[hasPermissionToImportActivities]="false"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
[showActions]="false"
[showSymbolColumn]="false"
(export)="onExport()"
></gf-activities-table>
</div>
</div>
</div>
</div>
<gf-dialog-footer
mat-dialog-actions
[deviceType]="data.deviceType"
(closeButtonClicked)="onClose()"
></gf-dialog-footer>

View File

@ -0,0 +1,27 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { AccountDetailDialog } from './account-detail-dialog.component';
@NgModule({
declarations: [AccountDetailDialog],
imports: [
CommonModule,
GfActivitiesTableModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfValueModule,
MatButtonModule,
MatDialogModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAccountDetailDialogModule {}

View File

@ -0,0 +1,5 @@
export interface AccountDetailDialogParams {
accountId: string;
deviceType: string;
hasImpersonationId: boolean;
}

View File

@ -65,7 +65,7 @@
<ng-container matColumnDef="transactions">
<th *matHeaderCellDef class="px-1 text-right" mat-header-cell>
<span class="d-block d-sm-none">#</span>
<span class="d-none d-sm-block" i18n>Transactions</span>
<span class="d-none d-sm-block" i18n>Activities</span>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<ng-container *ngIf="element.accountType === 'SECURITIES'">{{
@ -212,7 +212,12 @@
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr *matRowDef="let row; columns: displayedColumns" mat-row></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
class="cursor-pointer"
mat-row
(click)="onOpenAccountDetailDialog(row.id)"
></tr>
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row

View File

@ -9,6 +9,7 @@ import {
Output
} from '@angular/core';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { Account as AccountModel } from '@prisma/client';
import { Subject, Subscription } from 'rxjs';
@ -39,7 +40,7 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public constructor(private router: Router) {}
public ngOnInit() {}
@ -75,6 +76,12 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit {
}
}
public onOpenAccountDetailDialog(accountId: string) {
this.router.navigate([], {
queryParams: { accountId, accountDetailDialog: true }
});
}
public onUpdateAccount(aAccount: AccountModel) {
this.accountToUpdate.emit(aAccount);
}

View File

@ -25,7 +25,6 @@ import { AccountsTableComponent } from './accounts-table.component';
NgxSkeletonLoaderModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAccountsTableModule {}

View File

@ -9,7 +9,6 @@ import { GfMarketDataDetailDialogModule } from './market-data-detail-dialog/mark
declarations: [AdminMarketDataDetailComponent],
exports: [AdminMarketDataDetailComponent],
imports: [CommonModule, GfLineChartModule, GfMarketDataDetailDialogModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAdminMarketDataDetailModule {}

View File

@ -11,7 +11,6 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
@NgModule({
declarations: [MarketDataDetailDialog],
exports: [],
imports: [
CommonModule,
FormsModule,
@ -22,7 +21,6 @@ import { MarketDataDetailDialog } from './market-data-detail-dialog.component';
MatInputModule,
ReactiveFormsModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfMarketDataDetailDialogModule {}

View File

@ -8,7 +8,6 @@ import { DialogFooterComponent } from './dialog-footer.component';
declarations: [DialogFooterComponent],
exports: [DialogFooterComponent],
imports: [CommonModule, MatButtonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfDialogFooterModule {}

View File

@ -8,7 +8,6 @@ import { DialogHeaderComponent } from './dialog-header.component';
declarations: [DialogHeaderComponent],
exports: [DialogHeaderComponent],
imports: [CommonModule, MatButtonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfDialogHeaderModule {}

View File

@ -269,6 +269,18 @@
[routerLink]="['/pricing']"
>Pricing</a
>
<a
*ngIf="hasPermissionToAccessFearAndGreedIndex"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
[ngClass]="{
'font-weight-bold': currentRoute === 'markets',
'text-decoration-underline': currentRoute === 'markets'
}"
[routerLink]="['/markets']"
>Markets</a
>
<a
class="d-none d-sm-block mx-1 no-min-width px-1"
href="https://github.com/ghostfolio/ghostfolio"

View File

@ -37,6 +37,7 @@ export class HeaderComponent implements OnChanges {
public hasPermissionForSocialLogin: boolean;
public hasPermissionForSubscription: boolean;
public hasPermissionToAccessAdminControl: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public impersonationId: string;
public isMenuOpen: boolean;
@ -73,6 +74,11 @@ export class HeaderComponent implements OnChanges {
this.user?.permissions,
permissions.accessAdminControl
);
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
}
public impersonateAccount(aId: string) {

View File

@ -21,7 +21,6 @@ import { HeaderComponent } from './header.component';
MatToolbarModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHeaderModule {}

View File

@ -11,7 +11,6 @@ import { HomeHoldingsComponent } from './home-holdings.component';
@NgModule({
declarations: [HomeHoldingsComponent],
exports: [],
imports: [
CommonModule,
GfPositionDetailDialogModule,
@ -21,7 +20,6 @@ import { HomeHoldingsComponent } from './home-holdings.component';
MatCardModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeHoldingsModule {}

View File

@ -44,49 +44,49 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
if (state?.user) {
this.user = state.user;
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.user.permissions,
permissions.accessFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
this.changeDetectorRef.markForCheck();
});
this.changeDetectorRef.markForCheck();
}
});
}
public ngOnInit() {}
public ngOnInit() {
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {
this.dataService
.fetchSymbolItem({
dataSource: this.info.fearAndGreedDataSource,
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalData = [
...historicalData,
{
date: resetHours(new Date()).toISOString(),
value: marketPrice
}
];
this.changeDetectorRef.markForCheck();
});
}
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();

View File

@ -28,16 +28,17 @@
<div class="mb-3 row">
<div class="col-xs-12 col-md-8 offset-md-2">
<gf-benchmark
*ngFor="let benchmark of benchmarks"
class="py-2"
[benchmark]="benchmark"
[benchmarks]="benchmarks"
[locale]="user?.settings?.locale"
></gf-benchmark>
<gf-benchmark
*ngIf="!benchmarks"
class="py-2"
[benchmark]="undefined"
></gf-benchmark>
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '1.5rem',
width: '100%'
}"
></ngx-skeleton-loader>
</div>
</div>
</div>

View File

@ -3,19 +3,20 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { GfFearAndGreedIndexModule } from '@ghostfolio/client/components/fear-and-greed-index/fear-and-greed-index.module';
import { GfBenchmarkModule } from '@ghostfolio/ui/benchmark/benchmark.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { HomeMarketComponent } from './home-market.component';
@NgModule({
declarations: [HomeMarketComponent],
exports: [],
exports: [HomeMarketComponent],
imports: [
CommonModule,
GfBenchmarkModule,
GfFearAndGreedIndexModule,
GfLineChartModule
GfLineChartModule,
NgxSkeletonLoaderModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeMarketModule {}

View File

@ -10,7 +10,6 @@ import { HomeOverviewComponent } from './home-overview.component';
@NgModule({
declarations: [HomeOverviewComponent],
exports: [],
imports: [
CommonModule,
GfLineChartModule,
@ -19,7 +18,6 @@ import { HomeOverviewComponent } from './home-overview.component';
GfToggleModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeOverviewModule {}

View File

@ -8,14 +8,12 @@ import { HomeSummaryComponent } from './home-summary.component';
@NgModule({
declarations: [HomeSummaryComponent],
exports: [],
imports: [
CommonModule,
GfPortfolioSummaryModule,
MatCardModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfHomeSummaryModule {}

View File

@ -13,7 +13,6 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
@NgModule({
declarations: [LoginWithAccessTokenDialog],
exports: [],
imports: [
CommonModule,
FormsModule,
@ -26,7 +25,6 @@ import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.com
ReactiveFormsModule,
TextFieldModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class LoginWithAccessTokenDialogModule {}

View File

@ -8,7 +8,6 @@ import { PortfolioPerformanceComponent } from './portfolio-performance.component
@NgModule({
declarations: [PortfolioPerformanceComponent],
exports: [PortfolioPerformanceComponent],
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule],
providers: []
imports: [CommonModule, GfValueModule, NgxSkeletonLoaderModule]
})
export class GfPortfolioPerformanceModule {}

View File

@ -15,7 +15,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
@NgModule({
declarations: [PositionDetailDialog],
exports: [],
imports: [
CommonModule,
GfActivitiesTableModule,
@ -29,7 +28,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
MatDialogModule,
NgxSkeletonLoaderModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionDetailDialogModule {}

View File

@ -23,7 +23,6 @@ import { PositionComponent } from './position.component';
NgxSkeletonLoaderModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionModule {}

View File

@ -15,7 +15,6 @@ import { PositionsComponent } from './positions.component';
GfPositionModule,
MatButtonModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionsModule {}

View File

@ -8,7 +8,6 @@ import { RuleComponent } from './rule.component';
declarations: [RuleComponent],
exports: [RuleComponent],
imports: [CommonModule, NgxSkeletonLoaderModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfRuleModule {}

View File

@ -19,7 +19,6 @@ import { RulesComponent } from './rules.component';
MatButtonModule,
MatCardModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class RulesModule {}

View File

@ -7,7 +7,6 @@ import { SymbolIconComponent } from './symbol-icon.component';
declarations: [SymbolIconComponent],
exports: [SymbolIconComponent],
imports: [CommonModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfSymbolIconModule {}

View File

@ -8,7 +8,6 @@ import { ToggleComponent } from './toggle.component';
@NgModule({
declarations: [ToggleComponent],
exports: [ToggleComponent],
imports: [CommonModule, MatRadioModule, ReactiveFormsModule],
providers: []
imports: [CommonModule, MatRadioModule, ReactiveFormsModule]
})
export class GfToggleModule {}

View File

@ -7,7 +7,6 @@ import { WorldMapChartComponent } from './world-map-chart.component';
@NgModule({
declarations: [WorldMapChartComponent],
exports: [WorldMapChartComponent],
imports: [CommonModule, NgxSkeletonLoaderModule],
providers: []
imports: [CommonModule, NgxSkeletonLoaderModule]
})
export class GfWorldMapChartModule {}

View File

@ -5,13 +5,12 @@ import {
Router,
RouterStateSnapshot
} from '@angular/router';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { ViewMode } from '@prisma/client';
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { SettingsStorageService } from '../services/settings-storage.service';
import { UserService } from '../services/user/user.service';
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
private static PUBLIC_PAGE_ROUTES = [
@ -20,8 +19,10 @@ export class AuthGuard implements CanActivate {
'/about/privacy-policy',
'/blog',
'/de/blog',
'/demo',
'/en/blog',
'/features',
'/markets',
'/p',
'/pricing',
'/register',
@ -35,11 +36,10 @@ export class AuthGuard implements CanActivate {
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
if (route.queryParams?.utm_source) {
this.settingsStorageService.setSetting(
'utm_source',
route.queryParams?.utm_source
);
const utmSource = route.queryParams?.utm_source;
if (utmSource) {
this.settingsStorageService.setSetting('utm_source', utmSource);
}
return new Promise<boolean>((resolve) => {
@ -47,7 +47,10 @@ export class AuthGuard implements CanActivate {
.get()
.pipe(
catchError(() => {
if (route.queryParams?.utm_source) {
if (utmSource === 'ios') {
this.router.navigate(['/demo']);
resolve(false);
} else if (utmSource === 'trusted-web-activity') {
this.router.navigate(['/register']);
resolve(false);
} else if (

View File

@ -5,7 +5,6 @@ import {
HttpRequest
} from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { ImpersonationStorageService } from '../services/impersonation-storage.service';
@ -18,7 +17,6 @@ const TOKEN_HEADER_KEY = 'Authorization';
export class AuthInterceptor implements HttpInterceptor {
public constructor(
private impersonationStorageService: ImpersonationStorageService,
private router: Router,
private tokenStorageService: TokenStorageService
) {}

View File

@ -9,7 +9,6 @@ import { AboutPageComponent } from './about-page.component';
@NgModule({
declarations: [AboutPageComponent],
exports: [],
imports: [
AboutPageRoutingModule,
CommonModule,
@ -17,7 +16,6 @@ import { AboutPageComponent } from './about-page.component';
MatButtonModule,
MatCardModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AboutPageModule {}

View File

@ -12,7 +12,10 @@
<div class="pr-1 w-50" i18n>Alias</div>
<div class="pl-1 w-50">{{ user.alias }}</div>
</div>
<div *ngIf="user?.subscription" class="d-flex py-1">
<div
*ngIf="hasPermissionToUpdateUserSettings && user?.subscription"
class="d-flex py-1"
>
<div class="pr-1 w-50" i18n>Membership</div>
<div class="pl-1 w-50">
<div class="align-items-center d-flex mb-1">
@ -171,7 +174,7 @@
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50" i18n>ID</div>
<div class="pr-1 w-50" i18n>User ID</div>
<div class="pl-1 w-50">{{ user?.id }}</div>
</div>
</mat-card-content>

View File

@ -19,7 +19,6 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
@NgModule({
declarations: [AccountPageComponent],
exports: [],
imports: [
AccountPageRoutingModule,
CommonModule,
@ -37,7 +36,6 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
MatSlideToggleModule,
ReactiveFormsModule,
RouterModule
],
providers: []
]
})
export class AccountPageModule {}

View File

@ -10,7 +10,6 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
@NgModule({
declarations: [CreateOrUpdateAccessDialog],
exports: [],
imports: [
CommonModule,
FormsModule,
@ -19,7 +18,6 @@ import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog.com
MatFormFieldModule,
MatSelectModule,
ReactiveFormsModule
],
providers: []
]
})
export class GfCreateOrUpdateAccessDialogModule {}

View File

@ -3,6 +3,8 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -48,12 +50,17 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (params['createDialog'] && this.hasPermissionToCreateAccount) {
if (params['accountId'] && params['accountDetailDialog']) {
this.openAccountDetailDialog(params['accountId']);
} else if (
params['createDialog'] &&
this.hasPermissionToCreateAccount
) {
this.openCreateAccountDialog();
} else if (params['editDialog']) {
if (this.accounts) {
const account = this.accounts.find((account) => {
return account.id === params['transactionId'];
return account.id === params['accountId'];
});
this.openUpdateAccountDialog(account);
@ -139,7 +146,7 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
public onUpdateAccount(aAccount: AccountModel) {
this.router.navigate([], {
queryParams: { editDialog: true, transactionId: aAccount.id }
queryParams: { accountId: aAccount.id, editDialog: true }
});
}
@ -197,6 +204,26 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete();
}
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, {
autoFocus: false,
data: <AccountDetailDialogParams>{
accountId: aAccountId,
deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private openCreateAccountDialog(): void {
const dialogRef = this.dialog.open(CreateOrUpdateAccountDialog, {
data: {

View File

@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { GfAccountDetailDialogModule } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.module';
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
import { AccountsPageRoutingModule } from './accounts-page-routing.module';
@ -10,16 +11,15 @@ import { GfCreateOrUpdateAccountDialogModule } from './create-or-update-account-
@NgModule({
declarations: [AccountsPageComponent],
exports: [],
imports: [
AccountsPageRoutingModule,
CommonModule,
GfAccountDetailDialogModule,
GfAccountsTableModule,
GfCreateOrUpdateAccountDialogModule,
MatButtonModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AccountsPageModule {}

View File

@ -50,6 +50,17 @@
</mat-select>
</mat-form-field>
</div>
<div *ngIf="data.account.id">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Account ID</mat-label>
<input
disabled
matInput
name="accountId"
[(ngModel)]="data.account.id"
/>
</mat-form-field>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button i18n mat-button (click)="onCancel()">Cancel</button>

View File

@ -11,7 +11,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
@NgModule({
declarations: [CreateOrUpdateAccountDialog],
exports: [],
imports: [
CommonModule,
FormsModule,
@ -21,7 +20,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
MatInputModule,
MatSelectModule,
ReactiveFormsModule
],
providers: []
]
})
export class GfCreateOrUpdateAccountDialogModule {}

View File

@ -6,8 +6,6 @@ import { AuthPageComponent } from './auth-page.component';
@NgModule({
declarations: [AuthPageComponent],
exports: [],
imports: [AuthPageRoutingModule, CommonModule],
providers: []
imports: [AuthPageRoutingModule, CommonModule]
})
export class AuthPageModule {}

View File

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { DemoPageComponent } from './demo-page.component';
const routes: Routes = [
{ path: '', component: DemoPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DemoPageRoutingModule {}

View File

@ -0,0 +1,44 @@
import { Component, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
selector: 'gf-demo-page',
templateUrl: './demo-page.html'
})
export class DemoPageComponent implements OnDestroy {
public info: InfoItem;
private unsubscribeSubject = new Subject<void>();
public constructor(
private dataService: DataService,
private router: Router,
private tokenStorageService: TokenStorageService
) {
this.info = this.dataService.fetchInfo();
}
public ngOnInit() {
const hasToken = this.tokenStorageService.getToken()?.length > 0;
if (hasToken) {
alert(
'As you are already logged in, you cannot access the demo account.'
);
} else {
this.tokenStorageService.saveToken(this.info.demoAuthToken, true);
}
this.router.navigate(['/']);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,12 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { DemoPageRoutingModule } from './demo-page-routing.module';
import { DemoPageComponent } from './demo-page.component';
@NgModule({
declarations: [DemoPageComponent],
imports: [CommonModule, DemoPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class DemoPageModule {}

View File

@ -7,7 +7,7 @@ import {
} from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { User } from '@ghostfolio/common/interfaces';
import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -24,6 +24,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
public hasMessage: boolean;
public hasPermissionToAccessFearAndGreedIndex: boolean;
public info: InfoItem;
public tabs: { iconName: string; path: string }[] = [];
public user: User;
@ -34,7 +35,7 @@ export class HomePageComponent implements OnDestroy, OnInit {
private dataService: DataService,
private userService: UserService
) {
const { systemMessage } = this.dataService.fetchInfo();
this.info = this.dataService.fetchInfo();
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
@ -51,11 +52,11 @@ export class HomePageComponent implements OnDestroy, OnInit {
hasPermission(
this.user?.permissions,
permissions.createUserAccount
) || !!systemMessage;
) || !!this.info.systemMessage;
this.hasPermissionToAccessFearAndGreedIndex = hasPermission(
this.user.permissions,
permissions.accessFearAndGreedIndex
this.info?.globalPermissions,
permissions.enableFearAndGreedIndex
);
if (this.hasPermissionToAccessFearAndGreedIndex) {

View File

@ -12,7 +12,6 @@ import { HomePageComponent } from './home-page.component';
@NgModule({
declarations: [HomePageComponent],
exports: [],
imports: [
CommonModule,
GfHomeHoldingsModule,
@ -23,7 +22,6 @@ import { HomePageComponent } from './home-page.component';
MatTabsModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HomePageModule {}

View File

@ -1,7 +1,4 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { format } from 'date-fns';
import { Subject } from 'rxjs';
@ -39,23 +36,9 @@ export class LandingPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
public constructor(
private dataService: DataService,
private router: Router,
private tokenStorageService: TokenStorageService
) {}
public constructor() {}
public ngOnInit() {
const { demoAuthToken } = this.dataService.fetchInfo();
this.demoAuthToken = demoAuthToken;
}
public setToken(aToken: string) {
this.tokenStorageService.saveToken(aToken, true);
this.router.navigate(['/']);
}
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();

View File

@ -31,20 +31,19 @@
color="primary"
i18n
mat-flat-button
[disabled]="!demoAuthToken"
[routerLink]="['/register']"
>
Get Started
</a>
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
<button
<a
class="d-inline-block"
i18n
mat-stroked-button
[disabled]="!demoAuthToken"
(click)="setToken(demoAuthToken)"
[routerLink]="['/demo']"
>
<span i18n>Live Demo</span>
</button>
Live Demo
</a>
</div>
</div>
</div>
@ -163,24 +162,18 @@
Join now or check out the example account
</p>
<div class="py-2 text-center">
<a
color="primary"
i18n
mat-flat-button
[disabled]="!demoAuthToken"
[routerLink]="['/register']"
>
<a color="primary" i18n mat-flat-button [routerLink]="['/register']">
Get Started
</a>
<div class="d-inline-block mx-3 text-muted" i18n>or</div>
<button
<a
class="d-inline-block"
i18n
mat-stroked-button
[disabled]="!demoAuthToken"
(click)="setToken(demoAuthToken)"
[routerLink]="['/demo']"
>
<span i18n>Live Demo</span>
</button>
Live Demo
</a>
</div>
</div>
</div>

View File

@ -9,7 +9,6 @@ import { LandingPageComponent } from './landing-page.component';
@NgModule({
declarations: [LandingPageComponent],
exports: [],
imports: [
CommonModule,
GfLogoModule,
@ -17,7 +16,6 @@ import { LandingPageComponent } from './landing-page.component';
MatButtonModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class LandingPageModule {}

View File

@ -0,0 +1,15 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { MarketsPageComponent } from './markets-page.component';
const routes: Routes = [
{ path: '', component: MarketsPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class MarketsPageRoutingModule {}

View File

@ -0,0 +1,21 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
selector: 'gf-markets-page',
styleUrls: ['./markets-page.scss'],
templateUrl: './markets-page.html'
})
export class MarketsPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
public constructor() {}
public ngOnInit() {}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -0,0 +1,7 @@
<div class="container">
<div class="row">
<div class="col">
<gf-home-market></gf-home-market>
</div>
</div>
</div>

View File

@ -0,0 +1,13 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { GfHomeMarketModule } from '@ghostfolio/client/components/home-market/home-market.module';
import { MarketsPageRoutingModule } from './markets-page-routing.module';
import { MarketsPageComponent } from './markets-page.component';
@NgModule({
declarations: [MarketsPageComponent],
imports: [CommonModule, GfHomeMarketModule, MarketsPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class MarketsPageModule {}

View File

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

View File

@ -1,6 +1,8 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
import { AccountDetailDialog } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component';
import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces';
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service';
@ -99,7 +101,9 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.routeQueryParams = route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (
if (params['accountId'] && params['accountDetailDialog']) {
this.openAccountDetailDialog(params['accountId']);
} else if (
params['dataSource'] &&
params['positionDetailDialog'] &&
params['symbol']
@ -379,13 +383,21 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.markets.otherMarkets.value / marketsTotal;
}
public onAccountChartClicked({ symbol }: UniqueAsset) {
if (symbol) {
this.router.navigate([], {
queryParams: { accountId: symbol, accountDetailDialog: true }
});
}
}
public onChangePeriod(aValue: string) {
this.period = aValue;
this.initializeAnalysisData(this.period);
}
public onProportionChartClicked({ dataSource, symbol }: UniqueAsset) {
public onSymbolChartClicked({ dataSource, symbol }: UniqueAsset) {
if (dataSource && symbol) {
this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true }
@ -398,6 +410,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.unsubscribeSubject.complete();
}
private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open(AccountDetailDialog, {
autoFocus: false,
data: <AccountDetailDialogParams>{
accountId: aAccountId,
deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId
},
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem'
});
dialogRef
.afterClosed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route });
});
}
private openPositionDialog({
dataSource,
symbol

View File

@ -24,11 +24,13 @@
</mat-card-header>
<mat-card-content>
<gf-portfolio-proportion-chart
cursor="pointer"
[baseCurrency]="user?.settings?.baseCurrency"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[keys]="['id']"
[locale]="user?.settings?.locale"
[positions]="accounts"
(proportionChartClicked)="onAccountChartClicked($event)"
></gf-portfolio-proportion-chart>
</mat-card-content>
</mat-card>
@ -92,13 +94,9 @@
<div class="col-md-12 allocations-by-symbol">
<mat-card class="mb-3">
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>By Position</span
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1"
></gf-premium-indicator
></mat-card-title>
<mat-card-title class="align-items-center d-flex text-truncate" i18n>
By Holding</mat-card-title
>
<gf-toggle
[defaultValue]="period"
[isLoading]="false"
@ -116,7 +114,7 @@
[locale]="user?.settings?.locale"
[positions]="symbols"
[showLabels]="deviceType !== 'mobile'"
(proportionChartClicked)="onProportionChartClicked($event)"
(proportionChartClicked)="onSymbolChartClicked($event)"
></gf-portfolio-proportion-chart>
</mat-card-content>
</mat-card>

View File

@ -10,7 +10,6 @@ import { AnalysisPageComponent } from './analysis-page.component';
@NgModule({
declarations: [AnalysisPageComponent],
exports: [],
imports: [
AnalysisPageRoutingModule,
CommonModule,
@ -19,7 +18,6 @@ import { AnalysisPageComponent } from './analysis-page.component';
MatCardModule,
NgxSkeletonLoaderModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AnalysisPageModule {}

View File

@ -10,7 +10,6 @@ import { PortfolioPageComponent } from './portfolio-page.component';
@NgModule({
declarations: [PortfolioPageComponent],
exports: [],
imports: [
CommonModule,
GfPremiumIndicatorModule,
@ -19,7 +18,6 @@ import { PortfolioPageComponent } from './portfolio-page.component';
PortfolioPageRoutingModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PortfolioPageModule {}

View File

@ -7,9 +7,7 @@ import { ReportPageComponent } from './report-page.component';
@NgModule({
declarations: [ReportPageComponent],
exports: [],
imports: [CommonModule, ReportPageRoutingModule, RulesModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class ReportPageModule {}

View File

@ -34,7 +34,7 @@
[ngClass]="{ 'd-none': !activityForm.controls['searchSymbol'].hasValidator(Validators.required) }"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Symbol or ISIN</mat-label>
<mat-label i18n>Name, symbol or ISIN</mat-label>
<input
autocapitalize="off"
autocomplete="off"

View File

@ -17,7 +17,6 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
@NgModule({
declarations: [CreateOrUpdateTransactionDialog],
exports: [],
imports: [
CommonModule,
GfSymbolModule,
@ -34,7 +33,6 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
MatSelectModule,
ReactiveFormsModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfCreateOrUpdateTransactionDialogModule {}

View File

@ -10,7 +10,6 @@ import { ImportTransactionDialog } from './import-transaction-dialog.component';
@NgModule({
declarations: [ImportTransactionDialog],
exports: [],
imports: [
CommonModule,
GfDialogFooterModule,
@ -19,7 +18,6 @@ import { ImportTransactionDialog } from './import-transaction-dialog.component';
MatDialogModule,
MatExpansionModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfImportTransactionDialogModule {}

View File

@ -111,12 +111,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
}
});
this.fetchOrders();
this.fetchActivities();
}
public fetchOrders() {
public fetchActivities() {
this.dataService
.fetchOrders()
.fetchActivities({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
@ -139,7 +139,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.fetchOrders();
this.fetchActivities();
}
});
}
@ -298,7 +298,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.fetchOrders();
this.fetchActivities();
}
});
}
@ -332,7 +332,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
}
private handleImportSuccess() {
this.fetchOrders();
this.fetchActivities();
this.snackBar.open('✅ Import has been completed', undefined, {
duration: 3000
@ -376,7 +376,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
if (transaction) {
this.dataService.postOrder(transaction).subscribe({
next: () => {
this.fetchOrders();
this.fetchActivities();
}
});
}

View File

@ -10,7 +10,6 @@ import { PricingPageComponent } from './pricing-page.component';
@NgModule({
declarations: [PricingPageComponent],
exports: [],
imports: [
CommonModule,
GfPremiumIndicatorModule,
@ -19,7 +18,6 @@ import { PricingPageComponent } from './pricing-page.component';
PricingPageRoutingModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PricingPageModule {}

View File

@ -10,7 +10,7 @@
<div class="col-md-12 allocations-by-symbol">
<mat-card class="mb-3">
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="text-truncate" i18n>Positions</mat-card-title>
<mat-card-title class="text-truncate" i18n>Holdings</mat-card-title>
</mat-card-header>
<mat-card-content>
<gf-portfolio-proportion-chart

View File

@ -12,7 +12,6 @@ import { PublicPageComponent } from './public-page.component';
@NgModule({
declarations: [PublicPageComponent],
exports: [],
imports: [
CommonModule,
GfPortfolioProportionChartModule,
@ -23,7 +22,6 @@ import { PublicPageComponent } from './public-page.component';
MatCardModule,
PublicPageRoutingModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PublicPageModule {}

View File

@ -11,7 +11,6 @@ import { ShowAccessTokenDialogModule } from './show-access-token-dialog/show-acc
@NgModule({
declarations: [RegisterPageComponent],
exports: [],
imports: [
CommonModule,
GfLogoModule,
@ -21,7 +20,6 @@ import { ShowAccessTokenDialogModule } from './show-access-token-dialog/show-acc
RouterModule,
ShowAccessTokenDialogModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class RegisterPageModule {}

View File

@ -12,7 +12,6 @@ import { ShowAccessTokenDialog } from './show-access-token-dialog.component';
@NgModule({
declarations: [ShowAccessTokenDialog],
exports: [],
imports: [
ClipboardModule,
CommonModule,
@ -24,7 +23,6 @@ import { ShowAccessTokenDialog } from './show-access-token-dialog.component';
ReactiveFormsModule,
TextFieldModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class ShowAccessTokenDialogModule {}

View File

@ -7,9 +7,7 @@ import { ResourcesPageComponent } from './resources-page.component';
@NgModule({
declarations: [ResourcesPageComponent],
exports: [],
imports: [CommonModule, MatCardModule, ResourcesPageRoutingModule],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class ResourcesPageModule {}

View File

@ -9,14 +9,12 @@ import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
@NgModule({
declarations: [WebauthnPageComponent],
exports: [],
imports: [
CommonModule,
GfLogoModule,
MatButtonModule,
MatProgressSpinnerModule,
WebauthnPageRoutingModule
],
providers: []
]
})
export class WebauthnPageModule {}

View File

@ -10,7 +10,6 @@ import { ZenPageComponent } from './zen-page.component';
@NgModule({
declarations: [ZenPageComponent],
exports: [],
imports: [
CommonModule,
GfHomeHoldingsModule,
@ -19,7 +18,6 @@ import { ZenPageComponent } from './zen-page.component';
RouterModule,
ZenPageRoutingModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class ZenPageModule {}

View File

@ -3,9 +3,7 @@ import { NgModule } from '@angular/core';
import { SymbolPipe } from './symbol.pipe';
@NgModule({
imports: [],
declarations: [SymbolPipe],
exports: [SymbolPipe],
providers: []
exports: [SymbolPipe]
})
export class GfSymbolModule {}

View File

@ -33,7 +33,7 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { AccountWithValue, DateRange } from '@ghostfolio/common/types';
import { DataSource, Order as OrderModel } from '@prisma/client';
import { parseISO } from 'date-fns';
import { cloneDeep, groupBy } from 'lodash';
@ -59,10 +59,75 @@ export class DataService {
});
}
public fetchAccount(aAccountId: string) {
return this.http.get<AccountWithValue>(`/api/v1/account/${aAccountId}`);
}
public fetchAccounts() {
return this.http.get<Accounts>('/api/v1/account');
}
public fetchActivities({
filters
}: {
filters?: Filter[];
}): Observable<Activities> {
let params = new HttpParams();
if (filters?.length > 0) {
const {
ACCOUNT: filtersByAccount,
ASSET_CLASS: filtersByAssetClass,
TAG: filtersByTag
} = groupBy(filters, (filter) => {
return filter.type;
});
if (filtersByAccount) {
params = params.append(
'accounts',
filtersByAccount
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByAssetClass) {
params = params.append(
'assetClasses',
filtersByAssetClass
.map(({ id }) => {
return id;
})
.join(',')
);
}
if (filtersByTag) {
params = params.append(
'tags',
filtersByTag
.map(({ id }) => {
return id;
})
.join(',')
);
}
}
return this.http.get<any>('/api/v1/order', { params }).pipe(
map(({ activities }) => {
for (const activity of activities) {
activity.createdAt = parseISO(activity.createdAt);
activity.date = parseISO(activity.date);
}
return { activities };
})
);
}
public fetchAdminData() {
return this.http.get<AdminData>('/api/v1/admin');
}
@ -179,18 +244,6 @@ export class DataService {
);
}
public fetchOrders(): Observable<Activities> {
return this.http.get<any>('/api/v1/order').pipe(
map(({ activities }) => {
for (const activity of activities) {
activity.createdAt = parseISO(activity.createdAt);
activity.date = parseISO(activity.date);
}
return { activities };
})
);
}
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
let params = new HttpParams();

View File

@ -6,46 +6,54 @@
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://ghostfol.io</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/about</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/about/changelog</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/blog</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/demo</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/features</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/markets</loc>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/pricing</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/register</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
<url>
<loc>https://ghostfol.io/resources</loc>
<lastmod>2022-05-28T00:00:00+00:00</lastmod>
<lastmod>2022-07-01T00:00:00+00:00</lastmod>
</url>
</urlset>

View File

@ -4,7 +4,6 @@ import { UserWithSettings } from './interfaces';
export const permissions = {
accessAdminControl: 'accessAdminControl',
accessFearAndGreedIndex: 'accessFearAndGreedIndex',
createAccess: 'createAccess',
createAccount: 'createAccount',
createOrder: 'createOrder',
@ -14,6 +13,7 @@ export const permissions = {
deleteAuthDevice: 'deleteAuthDevice',
deleteOrder: 'deleteOrder',
deleteUser: 'deleteUser',
enableFearAndGreedIndex: 'enableFearAndGreedIndex',
enableImport: 'enableImport',
enableBlog: 'enableBlog',
enableSocialLogin: 'enableSocialLogin',

View File

@ -1,7 +1,8 @@
import { Account as AccountModel } from '@prisma/client';
import { Account as AccountModel, Platform } from '@prisma/client';
export type AccountWithValue = AccountModel & {
balanceInBaseCurrency: number;
Platform?: Platform;
transactionCount: number;
value: number;
valueInBaseCurrency: number;

View File

@ -1,49 +1,50 @@
<div class="align-items-center d-flex">
<div *ngIf="benchmark?.name" class="flex-grow-1 text-truncate">
{{ benchmark.name }}
</div>
<div *ngIf="!benchmark?.name" class="flex-grow-1">
<ngx-skeleton-loader
animation="pulse"
[theme]="{
width: '67%'
}"
></ngx-skeleton-loader>
</div>
<gf-value
class="mx-2"
size="medium"
[isPercent]="true"
[locale]="locale"
[ngClass]="{
'text-danger':
benchmark?.performances?.allTimeHigh?.performancePercent < 0,
'text-success':
benchmark?.performances?.allTimeHigh?.performancePercent > 0
}"
[value]="
benchmark?.performances?.allTimeHigh?.performancePercent ?? undefined
"
></gf-value>
<div class="text-muted">
<small class="d-none d-sm-block text-nowrap" i18n>from All Time High</small
><small class="d-block d-sm-none text-nowrap" i18n>from ATH</small>
</div>
<div class="ml-2">
<div
*ngIf="benchmark?.marketCondition"
[title]="benchmark?.marketCondition"
>
{{ resolveMarketCondition(benchmark.marketCondition).emoji }}
</div>
<ngx-skeleton-loader
*ngIf="!benchmark?.marketCondition"
animation="pulse"
appearance="circle"
[theme]="{
height: '1rem',
width: '1rem'
}"
></ngx-skeleton-loader>
</div>
</div>
<table class="gf-table w-100">
<thead>
<tr class="mat-header-row">
<th class="mat-header-cell px-1 py-2" i18n>Index</th>
<th class="mat-header-cell px-1 py-2 text-right">
<span class="d-none d-sm-block text-nowrap" i18n
>Change from All Time High</span
>
<span class="d-block d-sm-none text-nowrap" i18n>from ATH</span>
</th>
<th class="mat-header-cell px-1 py-2 text-right" i18n></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let benchmark of benchmarks" class="mat-row">
<td class="mat-cell px-1 py-2">
<div class="d-flex align-items-center">
{{ benchmark.name }}
</div>
</td>
<td class="mat-cell px-1 py-2 text-right">
<gf-value
class="d-inline-block justify-content-end"
size="medium"
[isPercent]="true"
[locale]="locale"
[ngClass]="{
'text-danger':
benchmark?.performances?.allTimeHigh?.performancePercent < 0,
'text-success':
benchmark?.performances?.allTimeHigh?.performancePercent > 0
}"
[value]="
benchmark?.performances?.allTimeHigh?.performancePercent ??
undefined
"
></gf-value>
</td>
<td class="mat-cell px-1 py-2">
<div
*ngIf="benchmark?.marketCondition"
class="text-center"
[title]="benchmark?.marketCondition"
>
{{ resolveMarketCondition(benchmark.marketCondition).emoji }}
</div>
</td>
</tr>
</tbody>
</table>

View File

@ -1,4 +1,10 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges
} from '@angular/core';
import { locale } from '@ghostfolio/common/config';
import { resolveMarketCondition } from '@ghostfolio/common/helper';
import { Benchmark } from '@ghostfolio/common/interfaces';
@ -8,11 +14,17 @@ import { Benchmark } from '@ghostfolio/common/interfaces';
templateUrl: './benchmark.component.html',
styleUrls: ['./benchmark.component.scss']
})
export class BenchmarkComponent {
@Input() benchmark: Benchmark;
export class BenchmarkComponent implements OnChanges {
@Input() benchmarks: Benchmark[];
@Input() locale: string;
public resolveMarketCondition = resolveMarketCondition;
public constructor() {}
public ngOnChanges() {
if (!this.locale) {
this.locale = locale;
}
}
}

View File

@ -15,7 +15,11 @@ import {
getTooltipPositionerMapTop,
getVerticalHoverLinePlugin
} from '@ghostfolio/common/chart-helper';
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
import {
locale,
primaryColorRgb,
secondaryColorRgb
} from '@ghostfolio/common/config';
import {
getBackgroundColor,
getDateFormatString,
@ -97,6 +101,10 @@ export class LineChartComponent implements AfterViewInit, OnChanges, OnDestroy {
this.changeDetectorRef.markForCheck();
});
}
if (!this.locale) {
this.locale = locale;
}
}
public ngOnDestroy() {

View File

@ -205,7 +205,7 @@ export class PortfolioProportionChartComponent
chartDataSorted.forEach(([, item]) => {
let lightnessRatio = 0.2;
Object.keys(item.subCategory).forEach((subCategory) => {
Object.keys(item.subCategory ?? {}).forEach((subCategory) => {
backgroundColorSubCategory.push(
Color(item.color).lighten(lightnessRatio).hex()
);
@ -276,12 +276,14 @@ export class PortfolioProportionChartComponent
padding: this.showLabels === true ? 100 : 0
},
onClick: (event, activeElements) => {
const dataIndex = activeElements[0].index;
const symbol: string = event.chart.data.labels[dataIndex];
try {
const dataIndex = activeElements[0].index;
const symbol: string = event.chart.data.labels[dataIndex];
const dataSource = this.positions[symbol]?.dataSource;
const dataSource = this.positions[symbol]?.dataSource;
this.proportionChartClicked.emit({ dataSource, symbol });
this.proportionChartClicked.emit({ dataSource, symbol });
} catch {}
},
onHover: (event, chartElement) => {
if (this.cursor) {

View File

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "1.165.0",
"version": "1.167.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"scripts": {
@ -63,16 +63,16 @@
"@codewithdan/observable-store": "2.2.11",
"@dinero.js/currencies": "2.0.0-alpha.8",
"@nestjs/bull": "0.5.5",
"@nestjs/common": "8.2.3",
"@nestjs/config": "1.1.3",
"@nestjs/core": "8.2.3",
"@nestjs/jwt": "8.0.0",
"@nestjs/passport": "8.0.1",
"@nestjs/platform-express": "8.2.3",
"@nestjs/schedule": "1.0.2",
"@nestjs/common": "8.4.7",
"@nestjs/config": "2.1.0",
"@nestjs/core": "8.4.7",
"@nestjs/jwt": "8.0.1",
"@nestjs/passport": "8.2.2",
"@nestjs/platform-express": "8.4.7",
"@nestjs/schedule": "2.0.1",
"@nestjs/serve-static": "2.2.2",
"@nrwl/angular": "14.3.5",
"@prisma/client": "3.14.0",
"@prisma/client": "3.15.2",
"@simplewebauthn/browser": "5.2.1",
"@simplewebauthn/server": "5.2.1",
"@stripe/stripe-js": "1.22.0",
@ -101,29 +101,29 @@
"lodash": "4.17.21",
"ms": "3.0.0-canary.1",
"ngx-device-detector": "3.0.0",
"ngx-markdown": "13.0.0",
"ngx-markdown": "14.0.1",
"ngx-skeleton-loader": "5.0.0",
"ngx-stripe": "13.0.0",
"papaparse": "5.3.1",
"passport": "0.4.1",
"passport": "0.6.0",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0",
"prisma": "3.14.0",
"prisma": "3.15.2",
"reflect-metadata": "0.1.13",
"rxjs": "7.4.0",
"stripe": "8.199.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.10.3",
"uuid": "8.3.2",
"yahoo-finance2": "2.3.2",
"zone.js": "0.11.4"
"yahoo-finance2": "2.3.3",
"zone.js": "0.11.6"
},
"devDependencies": {
"@angular-devkit/build-angular": "14.0.2",
"@angular-eslint/eslint-plugin": "13.2.1",
"@angular-eslint/eslint-plugin-template": "13.2.1",
"@angular-eslint/template-parser": "13.2.1",
"@angular/cli": "~14.0.0",
"@angular/cli": "14.0.2",
"@angular/compiler-cli": "14.0.2",
"@angular/language-service": "14.0.2",
"@angular/localize": "14.0.2",

Some files were not shown because too many files have changed in this diff Show More