Compare commits

..

26 Commits

Author SHA1 Message Date
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
3fe8f9c882 Release 1.165.0 (#1044) 2022-06-25 17:34:06 +02:00
d130efad47 Clean up comments (#1043)
* Clean up comments
2022-06-25 17:30:43 +02:00
109f0ebd70 Feature/move positions table to holdings section (#1042)
* Move positions table to holdings section

* Update changelog
2022-06-25 14:47:20 +02:00
069ddcc6b2 Feature/add reusable premium indicator component (#1041)
* Add premium indicator component

* Update changelog
2022-06-25 12:38:15 +02:00
f7bf6e652b Feature/add icon and name to positions table (#1040)
* Add icon and name

* Update changelog
2022-06-25 11:33:59 +02:00
eb059a024a Feature/delete data in data gathering by symbol (#1039)
* Delete market data

* Update changelog
2022-06-25 11:15:01 +02:00
ad88acff1c Release 1.164.0 (#1038) 2022-06-23 19:25:46 +02:00
1ff736537c Feature/add positions table to public page (#1037)
* Add positions table

* Update changelog
2022-06-23 19:24:24 +02:00
1fa65e1efd Release 1.163.0 (#1036) 2022-06-22 20:32:06 +02:00
df6bb489c2 Feature/improve onboarding for ios (#1035)
* Improve onboarding for iOS

* Update changelog
2022-06-22 20:30:03 +02:00
928a13310d Improve title (#1031) 2022-06-21 17:58:13 +02:00
2384861953 Remove experimental (#1032) 2022-06-20 20:32:51 +02:00
fe90bda6fb Release 1.162.0 (#1029) 2022-06-18 17:48:54 +02:00
d4b29ff11c Feature/add privacy policy page (#1028)
* Add privacy policy page

* Update changelog
2022-06-18 17:46:51 +02:00
a0a26cfa58 Feature/simplify header (#1027)
* Hide pricing page link for Premium users

* Harmonize content

* Update changelog
2022-06-18 11:57:27 +02:00
1610150427 Bugfix/fix currency conversion of ila to ils (#1026)
* Fix currency conversion: ILA to ILS

* Update changelog
2022-06-17 20:34:41 +02:00
cff8acd7b1 Clean up (#1022) 2022-06-17 20:04:52 +02:00
109 changed files with 1673 additions and 856 deletions

View File

@ -5,6 +5,59 @@ 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.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
- Added an icon and name column to the positions table
- Added a reusable premium indicator component
### Changed
- Moved the positions table to a dedicated section (_Holdings_)
- Changed the data gathering by symbol endpoint to delete data first
## 1.164.0 - 23.06.2022
### Added
- Added the positions table including performance to the public page
## 1.163.0 - 22.06.2022
### Changed
- Improved the onboarding for iOS
## 1.162.0 - 18.06.2022
### Added
- Added a _Privacy Policy_ page
### Changed
- Simplified the header
### Fixed
- Fixed an issue with the currency inconsistency in the _Yahoo Finance_ service (convert from `ILA` to `ILS`)
## 1.161.1 - 16.06.2022
### Added

View File

@ -136,7 +136,7 @@ Open http://localhost:3333 in your browser and accomplish these steps:
1. Run the following command to start the new Docker image: `docker-compose --env-file ./.env -f docker/docker-compose.yml up -d`
1. Then, run the following command to keep your database schema in sync: `docker-compose --env-file ./.env -f docker/docker-compose.yml exec ghostfolio yarn database:migrate`
### Run with _Unraid_ (unofficial)
### Run with _Unraid_ (Community)
Please follow the instructions of the Ghostfolio [Unraid Community App](https://unraid.net/community/apps?q=ghostfolio).
@ -186,7 +186,7 @@ yarn database:push
Run `yarn test`
## Public API (experimental)
## Public API
### Import Activities

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

@ -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

@ -317,7 +317,7 @@ export class PortfolioController {
const { holdings } = await this.portfolioService.getDetails(
access.userId,
access.userId,
'1d',
'max',
[{ id: 'EQUITY', type: 'ASSET_CLASS' }]
);
@ -338,12 +338,15 @@ export class PortfolioController {
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPublicDetails.holdings[symbol] = {
allocationCurrent: portfolioPosition.allocationCurrent,
allocationCurrent: portfolioPosition.value / totalValue,
countries: hasDetails ? portfolioPosition.countries : [],
currency: portfolioPosition.currency,
markets: portfolioPosition.markets,
name: portfolioPosition.name,
netPerformancePercent: portfolioPosition.netPerformancePercent,
sectors: hasDetails ? portfolioPosition.sectors : [],
symbol: portfolioPosition.symbol,
url: portfolioPosition.url,
value: portfolioPosition.value / totalValue
};
}

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;
@ -273,7 +286,6 @@ export class PortfolioService {
.filter((timelineItem) => timelineItem !== null)
.map((timelineItem) => ({
date: timelineItem.date,
marketPrice: timelineItem.value,
value: timelineItem.netPerformance.toNumber()
}));
@ -442,6 +454,7 @@ export class PortfolioService {
sectors: symbolProfile.sectors,
symbol: item.symbol,
transactionCount: item.transactionCount,
url: symbolProfile.url,
value: value.toNumber()
};
}
@ -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

@ -10,6 +10,7 @@ import ms from 'ms';
import { DataGatheringProcessor } from './data-gathering.processor';
import { ExchangeRateDataModule } from './exchange-rate-data.module';
import { MarketDataModule } from './market-data.module';
import { SymbolProfileModule } from './symbol-profile.module';
@Module({
@ -25,6 +26,7 @@ import { SymbolProfileModule } from './symbol-profile.module';
DataEnhancerModule,
DataProviderModule,
ExchangeRateDataModule,
MarketDataModule,
PrismaModule,
SymbolProfileModule
],

View File

@ -17,6 +17,7 @@ import { DataProviderService } from './data-provider/data-provider.service';
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
import { ExchangeRateDataService } from './exchange-rate-data.service';
import { IDataGatheringItem } from './interfaces/interfaces';
import { MarketDataService } from './market-data.service';
import { PrismaService } from './prisma.service';
@Injectable()
@ -28,6 +29,7 @@ export class DataGatheringService {
private readonly dataGatheringQueue: Queue,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly marketDataService: MarketDataService,
private readonly prismaService: PrismaService,
private readonly symbolProfileService: SymbolProfileService
) {}
@ -56,6 +58,8 @@ export class DataGatheringService {
}
public async gatherSymbol({ dataSource, symbol }: UniqueAsset) {
await this.marketDataService.deleteMany({ dataSource, symbol });
const symbols = (await this.getSymbolsMax()).filter((dataGatheringItem) => {
return (
dataGatheringItem.dataSource === dataSource &&

View File

@ -181,6 +181,9 @@ export class YahooFinanceService implements DataProviderInterface {
if (symbol === 'USDGBp') {
// Convert GPB to GBp (pence)
marketPrice = new Big(marketPrice).mul(100).toNumber();
} else if (symbol === 'USDILA') {
// Convert ILS to ILA
marketPrice = new Big(marketPrice).mul(100).toNumber();
}
response[symbol][format(historicalItem.date, DATE_FORMAT)] = {
@ -243,6 +246,18 @@ export class YahooFinanceService implements DataProviderInterface {
.mul(100)
.toNumber()
};
} else if (
symbol === 'USDILS' &&
yahooFinanceSymbols.includes('USDILA=X')
) {
// Convert ILS to ILA
response['USDILA'] = {
...response[symbol],
currency: 'ILA',
marketPrice: new Big(response[symbol].marketPrice)
.mul(100)
.toNumber()
};
}
}

View File

@ -5,9 +5,6 @@ import { getDateFormatString } from '@ghostfolio/common/helper';
import { format, parse } from 'date-fns';
export class CustomDateAdapter extends NativeDateAdapter {
/**
* @constructor
*/
public constructor(
@Inject(MAT_DATE_LOCALE) public locale: string,
@Inject(forwardRef(() => MAT_DATE_LOCALE)) matDateLocale: string,

View File

@ -16,6 +16,13 @@ const routes: Routes = [
(m) => m.ChangelogPageModule
)
},
{
path: 'about/privacy-policy',
loadChildren: () =>
import('./pages/about/privacy-policy/privacy-policy-page.module').then(
(m) => m.PrivacyPolicyPageModule
)
},
{
path: 'account',
loadChildren: () =>
@ -52,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: () =>
@ -120,6 +132,13 @@ const routes: Routes = [
(m) => m.FirePageModule
)
},
{
path: 'portfolio/holdings',
loadChildren: () =>
import('./pages/portfolio/holdings/holdings-page.module').then(
(m) => m.HoldingsPageModule
)
},
{
path: 'portfolio/report',
loadChildren: () =>

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

@ -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,29 @@
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],
exports: [],
imports: [
CommonModule,
GfActivitiesTableModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfValueModule,
MatButtonModule,
MatDialogModule,
NgxSkeletonLoaderModule
],
providers: [],
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

@ -30,9 +30,6 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@ -52,9 +49,6 @@ export class AdminJobsComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.filterForm = this.formBuilder.group({
status: []

View File

@ -31,9 +31,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef,
@ -53,9 +50,6 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.fetchAdminMarketData();
}

View File

@ -42,9 +42,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private adminService: AdminService,
private cacheService: CacheService,
@ -78,9 +75,6 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.fetchAdminData();
}

View File

@ -21,9 +21,6 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -38,9 +35,6 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.fetchAdminData();
}

View File

@ -35,11 +35,10 @@
>{{ userItem.alias || (userItem.id | slice:0:5) +
'...' }}</span
>
<ion-icon
<gf-premium-indicator
*ngIf="userItem?.subscription?.type === 'Premium'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</div>
</td>
<td class="mat-cell px-1 py-2 text-right">

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 { MatMenuModule } from '@angular/material/menu';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
import { AdminUsersComponent } from './admin-users.component';
@ -9,7 +10,13 @@ import { AdminUsersComponent } from './admin-users.component';
@NgModule({
declarations: [AdminUsersComponent],
exports: [],
imports: [CommonModule, GfValueModule, MatButtonModule, MatMenuModule],
imports: [
CommonModule,
GfPremiumIndicatorModule,
GfValueModule,
MatButtonModule,
MatMenuModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfAdminUsersModule {}

View File

@ -66,7 +66,9 @@
>Resources</a
>
<a
*ngIf="hasPermissionForSubscription"
*ngIf="
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
"
class="d-none d-sm-block mx-1"
i18n
mat-flat-button
@ -203,7 +205,9 @@
>Resources</a
>
<a
*ngIf="hasPermissionForSubscription"
*ngIf="
hasPermissionForSubscription && user?.subscription?.type === 'Basic'
"
class="d-block d-sm-none"
i18n
mat-menu-item

View File

@ -36,9 +36,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -81,9 +78,6 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -30,9 +30,6 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -89,9 +86,6 @@ export class HomeMarketComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {}
public ngOnDestroy() {

View File

@ -42,9 +42,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -69,9 +66,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -21,9 +21,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -46,9 +43,6 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.impersonationStorageService
.onChangeHasImpersonation()

View File

@ -6,12 +6,40 @@
mat-table
[dataSource]="dataSource"
>
<ng-container matColumnDef="icon">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-symbol-icon
*ngIf="element.url"
[tooltip]="element.name"
[url]="element.url"
></gf-symbol-icon>
</td>
</ng-container>
<ng-container matColumnDef="symbol">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Symbol
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
{{ element.symbol | gfSymbol }}
<span [title]="element.name">{{ element.symbol | gfSymbol }}</span>
</td>
</ng-container>
<ng-container matColumnDef="name">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell
mat-sort-header
>
Name
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<ng-container *ngIf="element.name !== element.symbol">{{
element.name
}}</ng-container>
</td>
</ng-container>
@ -36,48 +64,6 @@
</td>
</ng-container>
<ng-container matColumnDef="performance">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
>
Performance
</th>
<td class="d-none d-lg-table-cell px-1" mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end">
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.netPerformancePercent"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="allocationInvestment">
<th
*matHeaderCellDef
class="justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Initial Allocation
</th>
<td mat-cell *matCellDef="let element">
<div class="d-flex justify-content-end px-1">
<gf-value
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.allocationInvestment"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="allocationCurrent">
<th
*matHeaderCellDef
@ -86,7 +72,7 @@
mat-header-cell
mat-sort-header
>
Current Allocation
Allocation
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end">
@ -99,15 +85,39 @@
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<ng-container matColumnDef="performance">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
>
Performance
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.netPerformancePercent"
></gf-value>
</div>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
mat-row
[ngClass]="{
'cursor-pointer': !ignoreAssetSubClasses.includes(row.assetSubClass)
'cursor-pointer':
hasPermissionToShowValues &&
!ignoreAssetSubClasses.includes(row.assetSubClass)
}"
(click)="
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
hasPermissionToShowValues &&
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
onOpenPositionDialog({ dataSource: row.dataSource, symbol: row.symbol })
"
></tr>

View File

@ -27,7 +27,9 @@ import { Subject, Subscription } from 'rxjs';
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToShowValues = true;
@Input() locale: string;
@Input() pageSize = Number.MAX_SAFE_INTEGER;
@Input() positions: PortfolioPosition[];
@Output() transactionDeleted = new EventEmitter<string>();
@ -44,7 +46,6 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
ASSET_SUB_CLASS_EMERGENCY_FUND
];
public isLoading = true;
public pageSize = 7;
public routeQueryParams: Subscription;
private unsubscribeSubject = new Subject<void>();
@ -54,13 +55,14 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
public ngOnInit() {}
public ngOnChanges() {
this.displayedColumns = [
'symbol',
'value',
'performance',
'allocationInvestment',
'allocationCurrent'
];
this.displayedColumns = ['icon', 'symbol', 'name'];
if (this.hasPermissionToShowValues) {
this.displayedColumns.push('value');
}
this.displayedColumns.push('allocationCurrent');
this.displayedColumns.push('performance');
this.isLoading = true;

View File

@ -35,7 +35,6 @@ import { PositionsTableComponent } from './positions-table.component';
NgxSkeletonLoaderModule,
RouterModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionsTableModule {}

View File

@ -5,20 +5,21 @@ 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 = [
'/about',
'/about/changelog',
'/about/privacy-policy',
'/blog',
'/de/blog',
'/demo',
'/en/blog',
'/features',
'/p',
@ -34,11 +35,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) => {
@ -46,7 +46,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

@ -26,9 +26,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -54,9 +51,6 @@ export class AboutPageComponent implements OnDestroy, OnInit {
this.statistics = statistics;
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))

View File

@ -4,11 +4,11 @@
<h3 class="d-flex justify-content-center mb-3" i18n>About Ghostfolio</h3>
<div class="about-container">
<p>
<strong>Ghostfolio</strong> is a lightweight wealth management
application for individuals to keep track of stocks, ETFs or
cryptocurrencies and make solid, data-driven investment decisions. The
source code is fully available as open source software (OSS). The
project has been initiated by
Ghostfolio is a lightweight wealth management application for
individuals to keep track of stocks, ETFs or cryptocurrencies and make
solid, data-driven investment decisions. The source code is fully
available as open source software (OSS). The project has been
initiated by
<a href="https://dotsilver.ch" title="Website of Thomas Kaul"
>Thomas Kaul</a
>
@ -174,8 +174,8 @@
<div class="row">
<div
class="col-md-6 col-xs-12 my-2"
[ngClass]="{ 'offset-md-3': !hasPermissionForBlog }"
class="col-md-4 col-xs-12 my-2"
[ngClass]="{ 'offset-md-4': !hasPermissionForBlog }"
>
<a
class="py-2 w-100"
@ -186,7 +186,17 @@
>Changelog & License</a
>
</div>
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
<div *ngIf="hasPermissionForSubscription" class="col-md-4 col-xs-12 my-2">
<a
class="py-2 w-100"
color="primary"
i18n
mat-stroked-button
[routerLink]="['/about', 'privacy-policy']"
>Privacy Policy</a
>
</div>
<div *ngIf="hasPermissionForBlog" class="col-md-4 col-xs-12 my-2">
<a
class="py-2 w-100"
color="primary"

View File

@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
export class ChangelogPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor() {}
public ngOnDestroy() {

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 { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
const routes: Routes = [
{ path: '', component: PrivacyPolicyPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
exports: [RouterModule],
imports: [RouterModule.forChild(routes)]
})
export class PrivacyPolicyPageRoutingModule {}

View File

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

View File

@ -0,0 +1,8 @@
<div class="container">
<div class="mb-5 row">
<div class="col">
<h3 class="mb-3 text-center" i18n>Privacy Policy</h3>
<markdown [src]="'assets/privacy-policy.md'"></markdown>
</div>
</div>
</div>

View File

@ -0,0 +1,17 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MarkdownModule } from 'ngx-markdown';
import { PrivacyPolicyPageRoutingModule } from './privacy-policy-page-routing.module';
import { PrivacyPolicyPageComponent } from './privacy-policy-page.component';
@NgModule({
declarations: [PrivacyPolicyPageComponent],
imports: [
CommonModule,
MarkdownModule.forChild(),
PrivacyPolicyPageRoutingModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class PrivacyPolicyPageModule {}

View File

@ -0,0 +1,21 @@
:host {
color: rgb(var(--dark-primary-text));
display: block;
::ng-deep {
markdown {
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}
}
}
:host-context(.is-dark-theme) {
color: rgb(var(--light-primary-text));
}

View File

@ -63,9 +63,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -145,9 +142,6 @@ export class AccountPageComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -12,16 +12,20 @@
<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">
{{ user?.subscription?.type }}
<ion-icon
<a [routerLink]="['/pricing']"
>{{ user?.subscription?.type }}</a
>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Premium'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</div>
<div *ngIf="user?.subscription?.type === 'Premium'">
Valid until {{ user?.subscription?.expiresAt | date:
@ -54,11 +58,11 @@
class="mr-2 my-2"
mat-stroked-button
[href]="trySubscriptionMail"
><span i18n>Try Premium</span
><ion-icon
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
><span i18n>Try Premium</span>
<gf-premium-indicator
class="d-inline-block ml-1"
[enableLink]="false"
></gf-premium-indicator
></a>
<a
class="mr-2 my-2"
@ -170,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

@ -10,6 +10,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
import { AccountPageRoutingModule } from './account-page-routing.module';
@ -25,6 +26,7 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
FormsModule,
GfCreateOrUpdateAccessDialogModule,
GfPortfolioAccessTableModule,
GfPremiumIndicatorModule,
GfValueModule,
MatButtonModule,
MatCardModule,

View File

@ -2,15 +2,6 @@
color: rgb(var(--dark-primary-text));
display: block;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
gf-access-table {
overflow-x: auto;

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';
@ -35,9 +37,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -51,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);
@ -67,9 +71,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -145,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 }
});
}
@ -203,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

@ -16,18 +16,12 @@ export class AdminPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(private dataService: DataService) {
const { systemMessage } = this.dataService.fetchInfo();
this.hasMessage = !!systemMessage;
}
/**
* Initializes the controller
*/
public ngOnInit() {}
public ngOnDestroy() {

View File

@ -16,9 +16,6 @@ import { takeUntil } from 'rxjs/operators';
export class AuthPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private route: ActivatedRoute,
private router: Router,
@ -26,9 +23,6 @@ export class AuthPageComponent implements OnDestroy, OnInit {
private tokenStorageService: TokenStorageService
) {}
/**
* Initializes the controller
*/
public ngOnInit() {
this.route.params
.pipe(takeUntil(this.unsubscribeSubject))

View File

@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
export class BlogPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor() {}
public ngOnDestroy() {

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

@ -18,9 +18,6 @@ export class FeaturesPageComponent implements OnDestroy {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -29,9 +26,6 @@ export class FeaturesPageComponent implements OnDestroy {
this.info = this.dataService.fetchInfo();
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))

View File

@ -108,11 +108,10 @@
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span i18n>Portfolio Calculations</span>
<ion-icon
<gf-premium-indicator
*ngIf="hasPermissionForSubscription"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</h4>
<p class="m-0">
Check the rate of return of your portfolio for
@ -127,11 +126,10 @@
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span i18n>Portfolio Allocations</span>
<ion-icon
<gf-premium-indicator
*ngIf="hasPermissionForSubscription"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</h4>
<p class="m-0">
Check the allocations of your portfolio by account, asset class,
@ -169,10 +167,7 @@
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span i18n>Market Mood</span>
<ion-icon
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
</h4>
<p class="m-0">
Check the current market mood (<a [routerLink]="['/resources']"
@ -187,11 +182,10 @@
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span i18n>Static Analysis</span>
<ion-icon
<gf-premium-indicator
*ngIf="hasPermissionForSubscription"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</h4>
<p class="m-0">
Identify potential risks in your portfolio with Ghostfolio

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 { MatCardModule } from '@angular/material/card';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { FeaturesPageRoutingModule } from './features-page-routing.module';
import { FeaturesPageComponent } from './features-page.component';
@ -9,8 +10,9 @@ import { FeaturesPageComponent } from './features-page.component';
@NgModule({
declarations: [FeaturesPageComponent],
imports: [
FeaturesPageRoutingModule,
CommonModule,
FeaturesPageRoutingModule,
GfPremiumIndicatorModule,
MatButtonModule,
MatCardModule
],

View File

@ -29,9 +29,6 @@ export class HomePageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -70,9 +67,6 @@ export class HomePageComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {}
public ngOnDestroy() {

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,29 +36,9 @@ export class LandingPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private dataService: DataService,
private router: Router,
private tokenStorageService: TokenStorageService
) {}
public constructor() {}
/**
* Initializes the controller
*/
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>
@ -56,9 +55,8 @@
<strong>personal investment strategy</strong>.
</h2>
<p class="lead">
<strong>Ghostfolio</strong> empowers busy people to keep track of
stocks, ETFs or cryptocurrencies and make solid, data-driven investment
decisions.
Ghostfolio empowers busy people to keep track of stocks, ETFs or
cryptocurrencies and make solid, data-driven investment decisions.
</p>
</div>
</div>
@ -164,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

@ -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';
@ -68,7 +70,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
| 'value'
>;
};
public positionsArray: PortfolioPosition[];
public routeQueryParams: Subscription;
public sectors: {
[name: string]: { name: string; value: number };
@ -87,9 +88,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
private readonly SEARCH_PLACEHOLDER = 'Filter by account or tag...';
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -103,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']
@ -116,9 +116,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -229,7 +226,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
}
};
this.positions = {};
this.positionsArray = [];
this.sectors = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
@ -285,7 +281,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
exchange: position.exchange,
name: position.name
};
this.positionsArray.push(position);
if (position.assetClass !== AssetClass.CASH) {
// Prepare analysis data by continents, countries and sectors except for cash
@ -388,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 }
@ -407,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>
@ -37,12 +39,11 @@
<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 Currency</span
><ion-icon
><span i18n>By Currency</span>
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
class="ml-1"
></gf-premium-indicator
></mat-card-title>
<gf-toggle
[defaultValue]="period"
@ -67,11 +68,10 @@
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>By Asset Class</span
><ion-icon
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
class="ml-1"
></gf-premium-indicator
></mat-card-title>
<gf-toggle
[defaultValue]="period"
@ -96,11 +96,10 @@
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>By Position</span
><ion-icon
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
class="ml-1"
></gf-premium-indicator
></mat-card-title>
<gf-toggle
[defaultValue]="period"
@ -119,7 +118,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>
@ -129,11 +128,10 @@
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>By Sector</span
><ion-icon
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
class="ml-1"
></gf-premium-indicator
></mat-card-title>
<gf-toggle
[defaultValue]="period"
@ -159,11 +157,10 @@
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>By Continent</span
><ion-icon
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
class="ml-1"
></gf-premium-indicator
></mat-card-title>
<gf-toggle
[defaultValue]="period"
@ -188,11 +185,10 @@
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>By Country</span
><ion-icon
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
class="ml-1"
></gf-premium-indicator
></mat-card-title>
<gf-toggle
[defaultValue]="period"
@ -220,11 +216,10 @@
<mat-card-header class="overflow-hidden w-100">
<mat-card-title class="align-items-center d-flex text-truncate"
><span i18n>Regions</span
><ion-icon
><gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon
class="ml-1"
></gf-premium-indicator
></mat-card-title>
<gf-toggle
[defaultValue]="period"
@ -269,14 +264,4 @@
</mat-card>
</div>
</div>
<div class="row">
<div class="col-lg">
<gf-positions-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[positions]="positionsArray"
></gf-positions-table>
</div>
</div>
</div>

View File

@ -1,11 +1,11 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
import { AllocationsPageRoutingModule } from './allocations-page-routing.module';
@ -13,19 +13,17 @@ import { AllocationsPageComponent } from './allocations-page.component';
@NgModule({
declarations: [AllocationsPageComponent],
exports: [],
imports: [
AllocationsPageRoutingModule,
CommonModule,
GfActivitiesFilterModule,
GfPortfolioProportionChartModule,
GfPositionsTableModule,
GfPremiumIndicatorModule,
GfToggleModule,
GfWorldMapChartModule,
GfValueModule,
MatCardModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class AllocationsPageModule {}

View File

@ -27,14 +27,5 @@
font-size: 90%;
}
}
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
}
}

View File

@ -27,9 +27,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -38,9 +35,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
private userService: UserService
) {}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;

View File

@ -25,9 +25,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -35,9 +32,6 @@ export class FirePageComponent implements OnDestroy, OnInit {
private userService: UserService
) {}
/**
* Initializes the controller
*/
public ngOnInit() {
this.isLoading = true;
this.deviceType = this.deviceService.getDeviceInfo().deviceType;

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 { HoldingsPageComponent } from './holdings-page.component';
const routes: Routes = [
{ path: '', component: HoldingsPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class HoldingsPageRoutingModule {}

View File

@ -0,0 +1,214 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router';
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';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
Filter,
PortfolioDetails,
PortfolioPosition,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { AssetClass, DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject, Subscription } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'page' },
selector: 'gf-holdings-page',
styleUrls: ['./holdings-page.scss'],
templateUrl: './holdings-page.html'
})
export class HoldingsPageComponent implements OnDestroy, OnInit {
public activeFilters: Filter[] = [];
public allFilters: Filter[];
public deviceType: string;
public filters$ = new Subject<Filter[]>();
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;
public isLoading = false;
public placeholder = '';
public portfolioDetails: PortfolioDetails;
public positionsArray: PortfolioPosition[];
public routeQueryParams: Subscription;
public user: User;
private readonly SEARCH_PLACEHOLDER = 'Filter by account or tag...';
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private route: ActivatedRoute,
private router: Router,
private userService: UserService
) {
this.routeQueryParams = route.queryParams
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((params) => {
if (
params['dataSource'] &&
params['positionDetailDialog'] &&
params['symbol']
) {
this.openPositionDialog({
dataSource: params['dataSource'],
symbol: params['symbol']
});
}
});
}
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((aId) => {
this.hasImpersonationId = !!aId;
});
this.filters$
.pipe(
distinctUntilChanged(),
switchMap((filters) => {
this.isLoading = true;
this.activeFilters = filters;
this.placeholder =
this.activeFilters.length <= 0 ? this.SEARCH_PLACEHOLDER : '';
return this.dataService.fetchPortfolioDetails({
filters: this.activeFilters
});
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe((portfolioDetails) => {
this.portfolioDetails = portfolioDetails;
this.initializeAnalysisData();
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
this.hasPermissionToCreateOrder = hasPermission(
this.user.permissions,
permissions.createOrder
);
const accountFilters: Filter[] = this.user.accounts
.filter(({ accountType }) => {
return accountType === 'SECURITIES';
})
.map(({ id, name }) => {
return {
id,
label: name,
type: 'ACCOUNT'
};
});
const assetClassFilters: Filter[] = [];
for (const assetClass of Object.keys(AssetClass)) {
assetClassFilters.push({
id: assetClass,
label: assetClass,
type: 'ASSET_CLASS'
});
}
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => {
return {
id,
label: name,
type: 'TAG'
};
});
this.allFilters = [
...accountFilters,
...assetClassFilters,
...tagFilters
];
this.changeDetectorRef.markForCheck();
}
});
}
public initialize() {
this.positionsArray = [];
}
public initializeAnalysisData() {
this.initialize();
for (const [symbol, position] of Object.entries(
this.portfolioDetails.holdings
)) {
this.positionsArray.push(position);
}
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openPositionDialog({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
const dialogRef = this.dialog.open(PositionDetailDialog, {
autoFocus: false,
data: <PositionDetailDialogParams>{
dataSource,
symbol,
baseCurrency: this.user?.settings?.baseCurrency,
deviceType: this.deviceType,
hasImpersonationId: this.hasImpersonationId,
hasPermissionToReportDataGlitch: hasPermission(
this.user?.permissions,
permissions.reportDataGlitch
),
locale: this.user?.settings?.locale
},
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 });
});
});
}
}

View File

@ -0,0 +1,32 @@
<div class="container">
<div class="row">
<div class="col">
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3>
<gf-activities-filter
[allFilters]="allFilters"
[isLoading]="isLoading"
[placeholder]="placeholder"
(valueChanged)="filters$.next($event)"
></gf-activities-filter>
</div>
</div>
<div class="row">
<div class="col-lg">
<gf-positions-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[positions]="positionsArray"
></gf-positions-table>
<div *ngIf="hasPermissionToCreateOrder" class="text-center">
<a
class="mt-3"
i18n
mat-stroked-button
[routerLink]="['/portfolio', 'activities']"
>Manage Activities</a
>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { HoldingsPageRoutingModule } from './holdings-page-routing.module';
import { HoldingsPageComponent } from './holdings-page.component';
@NgModule({
declarations: [HoldingsPageComponent],
imports: [
CommonModule,
GfActivitiesFilterModule,
GfPositionsTableModule,
HoldingsPageRoutingModule,
MatButtonModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class HoldingsPageModule {}

View File

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

View File

@ -18,9 +18,6 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -34,9 +31,6 @@ export class PortfolioPageComponent implements OnDestroy, OnInit {
);
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))

View File

@ -1,10 +1,28 @@
<div class="container">
<h3 class="d-flex justify-content-center mb-3" i18n>Portfolio</h3>
<div class="row">
<div class="col-xs-12 col-md-6 mb-3">
<mat-card class="d-flex flex-column h-100">
<h4 i18n>Holdings</h4>
<div class="flex-grow-1" i18n>
Get an overview of your current holdings.
</div>
<div class="mt-2 text-right">
<a
color="primary"
mat-button
[routerLink]="['/portfolio', 'holdings']"
>
<span i18n>Open Holdings</span>
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
</a>
</div>
</mat-card>
</div>
<div class="col-xs-12 col-md-6 mb-3">
<mat-card class="d-flex flex-column h-100">
<h4 i18n>Activities</h4>
<div class="flex-grow-1">
<div class="flex-grow-1" i18n>
Manage your activities: stocks, ETFs, cryptocurrencies, dividend, and
valuables.
</div>
@ -24,13 +42,12 @@
<mat-card class="d-flex flex-column h-100">
<h4 class="align-items-center d-flex">
<span i18n>Allocations</span>
<ion-icon
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</h4>
<div class="flex-grow-1">
<div class="flex-grow-1" i18n>
Check the allocations of your portfolio by account, asset class,
currency, sector and region.
</div>
@ -50,13 +67,12 @@
<mat-card class="d-flex flex-column h-100">
<h4 class="align-items-center d-flex">
<span i18n>Analysis</span>
<ion-icon
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</h4>
<div class="flex-grow-1">
<div class="flex-grow-1" i18n>
Ghostfolio Analysis visualizes your portfolio and shows your top and
bottom performers.
</div>
@ -76,13 +92,12 @@
<mat-card class="d-flex flex-column h-100">
<h4 class="align-items-center d-flex">
<span i18n>X-ray</span>
<ion-icon
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</h4>
<div class="flex-grow-1">
<div class="flex-grow-1" i18n>
Ghostfolio X-ray uses static analysis to identify potential issues and
risks in your portfolio.
</div>
@ -98,13 +113,12 @@
<mat-card class="d-flex flex-column h-100">
<h4 class="align-items-center d-flex">
<span i18n>FIRE</span>
<ion-icon
<gf-premium-indicator
*ngIf="user?.subscription?.type === 'Basic'"
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
class="ml-1"
></gf-premium-indicator>
</h4>
<div class="flex-grow-1">
<div class="flex-grow-1" i18n>
Ghostfolio FIRE calculates metrics for the
<i>Financial Independence, Retire Early</i> lifestyle.
</div>

View File

@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { PortfolioPageRoutingModule } from './portfolio-page-routing.module';
import { PortfolioPageComponent } from './portfolio-page.component';
@ -12,6 +13,7 @@ import { PortfolioPageComponent } from './portfolio-page.component';
exports: [],
imports: [
CommonModule,
GfPremiumIndicatorModule,
MatButtonModule,
MatCardModule,
PortfolioPageRoutingModule,

View File

@ -21,18 +21,12 @@ export class ReportPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private userService: UserService
) {}
/**
* Initializes the controller
*/
public ngOnInit() {
this.dataService
.fetchPortfolioReport()

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

@ -44,9 +44,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -88,9 +85,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
const { globalPermissions } = this.dataService.fetchInfo();
@ -117,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;
@ -145,7 +139,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.fetchOrders();
this.fetchActivities();
}
});
}
@ -304,7 +298,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe({
next: () => {
this.fetchOrders();
this.fetchActivities();
}
});
}
@ -338,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
@ -382,7 +376,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
if (transaction) {
this.dataService.postOrder(transaction).subscribe({
next: () => {
this.fetchOrders();
this.fetchActivities();
}
});
}

View File

@ -20,9 +20,6 @@ export class PricingPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
@ -35,9 +32,6 @@ export class PricingPageComponent implements OnDestroy, OnInit {
this.price = subscriptions?.[0]?.price;
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))

View File

@ -6,15 +6,14 @@
</h3>
<div class="mb-4">
<p>
Our official
<strong>Ghostfolio Premium</strong> cloud offering is the easiest way
to get started. Due to the time it saves, this will be the best option
Our official Ghostfolio Premium cloud offering is the easiest way to
get started. Due to the time it saves, this will be the best option
for most people. The revenue is used for covering the hosting costs.
</p>
<p>
If you prefer to run <strong>Ghostfolio</strong> on your own
infrastructure, please find the source code and further instructions
on <a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
If you prefer to run Ghostfolio on your own infrastructure, please
find the source code and further instructions on
<a href="https://github.com/ghostfolio/ghostfolio">GitHub</a>.
</p>
</div>
<div class="row">
@ -126,10 +125,10 @@
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span i18n>Premium</span>
<ion-icon
class="ml-1 text-muted"
name="diamond-outline"
></ion-icon>
<gf-premium-indicator
class="ml-1"
[enableLink]="false"
></gf-premium-indicator>
</h4>
<p>
For ambitious investors who need the full picture of their

View File

@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { PricingPageRoutingModule } from './pricing-page-routing.module';
import { PricingPageComponent } from './pricing-page.component';
@ -12,6 +13,7 @@ import { PricingPageComponent } from './pricing-page.component';
exports: [],
imports: [
CommonModule,
GfPremiumIndicatorModule,
MatButtonModule,
MatCardModule,
PricingPageRoutingModule,

View File

@ -34,6 +34,10 @@ export class PublicPageComponent implements OnInit {
public positions: {
[symbol: string]: Pick<PortfolioPosition, 'currency' | 'name' | 'value'>;
};
public positionsArray: Pick<
PortfolioPosition,
'currency' | 'name' | 'netPerformancePercent' | 'symbol' | 'value'
>[];
public sectors: {
[name: string]: { name: string; value: number };
};
@ -44,9 +48,6 @@ export class PublicPageComponent implements OnInit {
private id: string;
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private activatedRoute: ActivatedRoute,
private changeDetectorRef: ChangeDetectorRef,
@ -59,9 +60,6 @@ export class PublicPageComponent implements OnInit {
});
}
/**
* Initializes the controller
*/
public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -115,6 +113,7 @@ export class PublicPageComponent implements OnInit {
}
};
this.positions = {};
this.positionsArray = [];
this.sectors = {
[UNKNOWN_KEY]: {
name: UNKNOWN_KEY,
@ -139,6 +138,7 @@ export class PublicPageComponent implements OnInit {
currency: position.currency,
name: position.name
};
this.positionsArray.push(position);
if (position.countries.length > 0) {
this.markets.developedMarkets.value +=

View File

@ -109,6 +109,16 @@
</mat-card>
</div>
</div>
<div class="row">
<div class="col-lg">
<gf-positions-table
pageSize="7"
[deviceType]="deviceType"
[hasPermissionToShowValues]="false"
[positions]="positionsArray"
></gf-positions-table>
</div>
</div>
<div class="row my-5">
<div class="col-md-10 offset-md-1">
<h2 class="h4 mb-1 text-center">

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 { MatCardModule } from '@angular/material/card';
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value';
@ -15,6 +16,7 @@ import { PublicPageComponent } from './public-page.component';
imports: [
CommonModule,
GfPortfolioProportionChartModule,
GfPositionsTableModule,
GfValueModule,
GfWorldMapChartModule,
MatButtonModule,

View File

@ -30,9 +30,6 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private dataService: DataService,
private deviceService: DeviceDetectorService,
@ -45,9 +42,6 @@ export class RegisterPageComponent implements OnDestroy, OnInit {
this.tokenStorageService.signOut();
}
/**
* Initializes the controller
*/
public ngOnInit() {
const { demoAuthToken, globalPermissions } = this.dataService.fetchInfo();

View File

@ -10,14 +10,8 @@ import { Subject } from 'rxjs';
export class ResourcesPageComponent implements OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor() {}
/**
* Initializes the controller
*/
public ngOnInit() {}
public ngOnDestroy() {

View File

@ -23,9 +23,6 @@ export class ZenPageComponent implements AfterViewInit, OnDestroy, OnInit {
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private route: ActivatedRoute,
private changeDetectorRef: ChangeDetectorRef,

View File

@ -32,8 +32,8 @@ import {
PortfolioSummary,
User
} from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { DateRange } from '@ghostfolio/common/types';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
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');
}
@ -115,12 +180,14 @@ export class DataService {
public fetchInfo(): InfoItem {
const info = cloneDeep((window as any).info);
const utmSource = <'ios' | 'trusted-web-activity'>(
window.localStorage.getItem('utm_source')
);
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {
info.globalPermissions = info.globalPermissions.filter(
(permission) => permission !== permissions.enableSubscription
);
}
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
return info;
}
@ -177,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

@ -0,0 +1,80 @@
Last updated: June 18, 2022
This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.
We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy.
## Interpretation and Definitions
### Interpretation
The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.
### Definitions
For the purposes of this Privacy Policy:
- **Account** means a unique account created for You to access our Service or parts of our Service.
- **Application** means the software program provided by the Company downloaded by You on any electronic device, named Ghostfolio App.
- **Company** (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Ghostfolio.
- **Country** refers to: Switzerland
- **Device** means any device that can access the Service such as a computer, a cellphone or a digital tablet.
- **Personal Data** is any information that relates to an identified or identifiable individual.
- **Service** refers to the Application.
- **Service Provider** means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.
- **Usage Data** refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).
- **You** means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.
## Collecting and Using Your Personal Data
### Types of Data Collected
#### Personal Data
While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to identify You. Personally identifiable information may include, but is not limited to:
- Usage Data
- User Id
#### Usage Data
Usage Data is collected automatically when using the Service.
Usage Data may include information such as the time and date of Your visit, the unique user identifier and other diagnostic data.
When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the unique user identifier and other diagnostic data.
### Use of Your Personal Data
The Company may use Personal Data for the following purposes:
- **To provide and maintain our Service**, including to monitor the usage of our Service.
- **For other purposes**: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.
### Retention of Your Personal Data
The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.
The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.
### Disclosure of Your Personal Data
#### Security of Your Personal Data
The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to store no personal data at all to protect Your Personal Data, We cannot guarantee its absolute security.
## Links to Other Websites
Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.
We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.
## Changes to this Privacy Policy
We may update Our Privacy Policy from time to time.
You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.
## Contact Us
If you have any questions about this Privacy Policy, You can contact us [here](https://ghostfol.io/about).

View File

@ -1,5 +1,6 @@
User-agent: *
Allow: /
Disallow: /about/privacy-policy
Disallow: /p/*
Sitemap: https://ghostfol.io/sitemap.xml

View File

@ -3,7 +3,7 @@ import { LOCALE_ID } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { locale } from '@ghostfolio/common/config';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions';
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
import { AppModule } from './app/app.module';
import { environment } from './environments/environment';
@ -11,12 +11,14 @@ import { environment } from './environments/environment';
(async () => {
const response = await fetch('/api/v1/info');
const info: InfoItem = await response.json();
const utmSource = <'ios' | 'trusted-web-activity'>(
window.localStorage.getItem('utm_source')
);
if (window.localStorage.getItem('utm_source') === 'trusted-web-activity') {
info.globalPermissions = info.globalPermissions.filter(
(permission) => permission !== permissions.enableSubscription
);
}
info.globalPermissions = filterGlobalPermissions(
info.globalPermissions,
utmSource
);
(window as any).info = info;

View File

@ -18,4 +18,5 @@ export interface EnhancedSymbolProfile {
symbol: string;
symbolMapping?: { [key: string]: string };
updatedAt: Date;
url?: string;
}

View File

@ -10,7 +10,10 @@ export interface PortfolioPublicDetails {
| 'currency'
| 'markets'
| 'name'
| 'netPerformancePercent'
| 'sectors'
| 'symbol'
| 'url'
| 'value'
>;
};

View File

@ -73,6 +73,28 @@ export function getPermissions(aRole: Role): string[] {
}
}
export function filterGlobalPermissions(
aGlobalPermissions: string[],
aUtmSource: 'ios' | 'trusted-web-activity'
) {
const globalPermissions = aGlobalPermissions;
if (aUtmSource === 'ios') {
return globalPermissions.filter((permission) => {
return (
permission !== permissions.enableSocialLogin &&
permission !== permissions.enableSubscription
);
});
} else if (aUtmSource === 'trusted-web-activity') {
return globalPermissions.filter((permission) => {
return permission !== permissions.enableSubscription;
});
}
return globalPermissions;
}
export function hasPermission(
aPermissions: string[] = [],
aPermission: string

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

@ -62,9 +62,6 @@ export class FireCalculatorComponent
private unsubscribeSubject = new Subject<void>();
/**
* @constructor
*/
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private fireCalculatorService: FireCalculatorService,

View File

@ -6,9 +6,6 @@ export class FireCalculatorService {
private readonly COMPOUND_PERIOD = 12;
private readonly CONTRIBUTION_PERIOD = 12;
/**
* @constructor
*/
public constructor() {}
public calculateCompoundInterest({

View File

@ -1,3 +1,4 @@
import { RouterTestingModule } from '@angular/router/testing';
import { GfLogoModule } from '@ghostfolio/ui/logo';
import { Meta, Story, moduleMetadata } from '@storybook/angular';
@ -8,7 +9,7 @@ export default {
component: NoTransactionsInfoComponent,
decorators: [
moduleMetadata({
imports: [GfLogoModule]
imports: [GfLogoModule, RouterTestingModule]
})
]
} as Meta<NoTransactionsInfoComponent>;

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