Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
3fe8f9c882 | |||
d130efad47 | |||
109f0ebd70 | |||
069ddcc6b2 | |||
f7bf6e652b | |||
eb059a024a | |||
ad88acff1c | |||
1ff736537c | |||
1fa65e1efd | |||
df6bb489c2 | |||
928a13310d | |||
2384861953 | |||
fe90bda6fb | |||
d4b29ff11c | |||
a0a26cfa58 | |||
1610150427 | |||
cff8acd7b1 | |||
0f36d6cbdb | |||
046e28b521 | |||
aba562cb35 | |||
03f2f33344 | |||
a996dd7ed5 | |||
002b883668 |
55
CHANGELOG.md
55
CHANGELOG.md
@ -5,6 +5,61 @@ 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.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
|
||||
|
||||
- Added the vertical hover line to inspect data points in the performance chart on the home page
|
||||
|
||||
### Changed
|
||||
|
||||
- Improved the landing page
|
||||
- Upgraded `angular` from version `13.3.6` to `14.0.2`
|
||||
- Upgraded `Nx` from version `14.1.4` to `14.3.5`
|
||||
- Upgraded `storybook` from version `6.4.22` to `6.5.9`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved the error handling of missing market prices
|
||||
|
||||
## 1.160.0 - 15.06.2022
|
||||
|
||||
### Fixed
|
||||
|
@ -22,7 +22,7 @@ RUN node decorate-angular-cli.js
|
||||
COPY ./angular.json angular.json
|
||||
COPY ./nx.json nx.json
|
||||
COPY ./replace.build.js replace.build.js
|
||||
COPY ./jest.preset.ts jest.preset.ts
|
||||
COPY ./jest.preset.js jest.preset.js
|
||||
COPY ./jest.config.ts jest.config.ts
|
||||
COPY ./tsconfig.base.json tsconfig.base.json
|
||||
COPY ./libs libs
|
||||
|
@ -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
|
||||
|
||||
|
28
angular.json
28
angular.json
@ -2,6 +2,7 @@
|
||||
"version": 1,
|
||||
"projects": {
|
||||
"api": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "apps/api",
|
||||
"sourceRoot": "apps/api/src",
|
||||
"projectType": "application",
|
||||
@ -56,6 +57,7 @@
|
||||
"tags": []
|
||||
},
|
||||
"client": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
@ -189,6 +191,7 @@
|
||||
"tags": []
|
||||
},
|
||||
"client-e2e": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "apps/client-e2e",
|
||||
"sourceRoot": "apps/client-e2e/src",
|
||||
"projectType": "application",
|
||||
@ -211,6 +214,7 @@
|
||||
"implicitDependencies": ["client"]
|
||||
},
|
||||
"common": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "libs/common",
|
||||
"sourceRoot": "libs/common/src",
|
||||
"projectType": "library",
|
||||
@ -233,6 +237,7 @@
|
||||
"tags": []
|
||||
},
|
||||
"ui": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "library",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
@ -258,14 +263,12 @@
|
||||
}
|
||||
},
|
||||
"storybook": {
|
||||
"builder": "@nrwl/storybook:storybook",
|
||||
"builder": "@storybook/angular:start-storybook",
|
||||
"options": {
|
||||
"uiFramework": "@storybook/angular",
|
||||
"port": 4400,
|
||||
"config": {
|
||||
"configFolder": "libs/ui/.storybook"
|
||||
},
|
||||
"projectBuildConfig": "ui:build-storybook"
|
||||
"configDir": "libs/ui/.storybook",
|
||||
"browserTarget": "ui:build-storybook",
|
||||
"compodoc": false
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
@ -274,15 +277,13 @@
|
||||
}
|
||||
},
|
||||
"build-storybook": {
|
||||
"builder": "@nrwl/storybook:build",
|
||||
"builder": "@storybook/angular:build-storybook",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"uiFramework": "@storybook/angular",
|
||||
"outputPath": "dist/storybook/ui",
|
||||
"config": {
|
||||
"configFolder": "libs/ui/.storybook"
|
||||
},
|
||||
"projectBuildConfig": "ui:build-storybook"
|
||||
"outputDir": "dist/storybook/ui",
|
||||
"configDir": "libs/ui/.storybook",
|
||||
"browserTarget": "ui:build-storybook",
|
||||
"compodoc": false
|
||||
},
|
||||
"configurations": {
|
||||
"ci": {
|
||||
@ -294,6 +295,7 @@
|
||||
"tags": []
|
||||
},
|
||||
"ui-e2e": {
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"root": "apps/ui-e2e",
|
||||
"sourceRoot": "apps/ui-e2e/src",
|
||||
"projectType": "application",
|
||||
|
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
displayName: 'api',
|
||||
|
||||
globals: {
|
||||
@ -13,5 +13,5 @@ module.exports = {
|
||||
coverageDirectory: '../../coverage/apps/api',
|
||||
testTimeout: 10000,
|
||||
testEnvironment: 'node',
|
||||
preset: '../../jest.preset.ts'
|
||||
preset: '../../jest.preset.js'
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
@ -273,7 +273,6 @@ export class PortfolioService {
|
||||
.filter((timelineItem) => timelineItem !== null)
|
||||
.map((timelineItem) => ({
|
||||
date: timelineItem.date,
|
||||
marketPrice: timelineItem.value,
|
||||
value: timelineItem.netPerformance.toNumber()
|
||||
}));
|
||||
|
||||
@ -394,7 +393,7 @@ export class PortfolioService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = item.quantity.mul(item.marketPrice);
|
||||
const value = item.quantity.mul(item.marketPrice ?? 0);
|
||||
const symbolProfile = symbolProfileMap[item.symbol];
|
||||
const dataProviderResponse = dataProviderResponses[item.symbol];
|
||||
|
||||
@ -442,6 +441,7 @@ export class PortfolioService {
|
||||
sectors: symbolProfile.sectors,
|
||||
symbol: item.symbol,
|
||||
transactionCount: item.transactionCount,
|
||||
url: symbolProfile.url,
|
||||
value: value.toNumber()
|
||||
};
|
||||
}
|
||||
@ -658,7 +658,7 @@ export class PortfolioService {
|
||||
netPerformancePercent: position.netPerformancePercentage?.toNumber(),
|
||||
quantity: quantity.toNumber(),
|
||||
value: this.exchangeRateDataService.toCurrency(
|
||||
quantity.mul(marketPrice).toNumber(),
|
||||
quantity.mul(marketPrice ?? 0).toNumber(),
|
||||
currency,
|
||||
userCurrency
|
||||
)
|
||||
|
@ -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
|
||||
],
|
||||
|
@ -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 &&
|
||||
|
@ -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()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
displayName: 'client',
|
||||
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
@ -18,5 +18,5 @@ module.exports = {
|
||||
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular'
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
|
||||
preset: '../../jest.preset.ts'
|
||||
preset: '../../jest.preset.js'
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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: () =>
|
||||
@ -120,6 +127,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: () =>
|
||||
|
@ -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: []
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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 {}
|
||||
|
@ -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
|
||||
@ -229,13 +233,7 @@
|
||||
mat-button
|
||||
[routerLink]="['/']"
|
||||
>
|
||||
<gf-logo
|
||||
[hideName]="
|
||||
!currentRoute ||
|
||||
currentRoute === 'register' ||
|
||||
currentRoute === 'start'
|
||||
"
|
||||
></gf-logo>
|
||||
<gf-logo [hideName]="currentRoute === 'register'"></gf-logo>
|
||||
</a>
|
||||
<span class="spacer"></span>
|
||||
<a
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -4,7 +4,9 @@
|
||||
<div class="row w-100">
|
||||
<div class="chart-container col">
|
||||
<gf-line-chart
|
||||
class="position-absolute"
|
||||
symbol="Performance"
|
||||
[currency]="user?.settings?.baseCurrency"
|
||||
[historicalDataItems]="historicalDataItems"
|
||||
[locale]="user?.settings?.locale"
|
||||
[ngClass]="{ 'pr-3': deviceType === 'mobile' }"
|
||||
|
@ -25,10 +25,8 @@
|
||||
gf-line-chart {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
@ -35,7 +35,6 @@ import { PositionsTableComponent } from './positions-table.component';
|
||||
NgxSkeletonLoaderModule,
|
||||
RouterModule
|
||||
],
|
||||
providers: [],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
})
|
||||
export class GfPositionsTableModule {}
|
||||
|
@ -23,7 +23,7 @@ export class ToggleComponent implements OnChanges, OnInit {
|
||||
|
||||
@Output() change = new EventEmitter<Pick<ToggleOption, 'value'>>();
|
||||
|
||||
public option = new FormControl();
|
||||
public option = new FormControl<string>(undefined);
|
||||
|
||||
public constructor() {}
|
||||
|
||||
|
@ -17,6 +17,7 @@ export class AuthGuard implements CanActivate {
|
||||
private static PUBLIC_PAGE_ROUTES = [
|
||||
'/about',
|
||||
'/about/changelog',
|
||||
'/about/privacy-policy',
|
||||
'/blog',
|
||||
'/de/blog',
|
||||
'/en/blog',
|
||||
|
@ -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))
|
||||
|
@ -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"
|
||||
|
@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
|
||||
export class ChangelogPageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -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 {}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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>
|
@ -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 {}
|
@ -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));
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -16,12 +16,13 @@
|
||||
<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 +55,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"
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -35,9 +35,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private dataService: DataService,
|
||||
@ -67,9 +64,6 @@ export class AccountsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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))
|
||||
|
@ -10,9 +10,6 @@ import { Subject } from 'rxjs';
|
||||
export class BlogPageComponent implements OnDestroy {
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
],
|
||||
|
@ -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() {
|
||||
|
@ -39,18 +39,12 @@ export class LandingPageComponent implements OnDestroy, OnInit {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private dataService: DataService,
|
||||
private router: Router,
|
||||
private tokenStorageService: TokenStorageService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
const { demoAuthToken } = this.dataService.fetchInfo();
|
||||
|
||||
|
@ -1,19 +1,28 @@
|
||||
<div class="intro-container mb-5">
|
||||
<div class="intro-inner-container mx-auto">
|
||||
<div class="h-100 intro w-100"></div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col text-center">
|
||||
<h1 class="font-weight-bold intro my-5" i18n>
|
||||
Manage your wealth like a boss
|
||||
</h1>
|
||||
<div>
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=yY6ObSQVJZk"
|
||||
target="_blank"
|
||||
title="Watch the Ghostfol.io Trailer on YouTube"
|
||||
>
|
||||
<img
|
||||
alt="Ghostfol.io Trailer"
|
||||
class="rounded video"
|
||||
src="./assets/images/video-preview.jpg"
|
||||
style="max-width: 100%; width: 40rem"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center w-100"
|
||||
>
|
||||
<gf-logo size="large"></gf-logo>
|
||||
<p class="lead m-0">Wealth Management Software</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="button-container row">
|
||||
<div class="align-items-center col d-flex justify-content-center">
|
||||
<div class="py-5 text-center">
|
||||
@ -43,25 +52,12 @@
|
||||
<div class="row my-5">
|
||||
<div class="col text-center">
|
||||
<h2 class="h4 mb-1 text-center">
|
||||
Protect your <strong>wealth</strong>. Refine your
|
||||
Protect your <strong>assets</strong>. Refine your
|
||||
<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.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
href="https://www.youtube.com/watch?v=yY6ObSQVJZk"
|
||||
title="Watch the Ghostfol.io Trailer on YouTube"
|
||||
>
|
||||
<img
|
||||
alt="Ghostfol.io Trailer"
|
||||
src="./assets/images/video-preview.jpg"
|
||||
style="max-width: 100%; width: 40rem"
|
||||
/>
|
||||
</a>
|
||||
Ghostfolio empowers busy people to keep track of stocks, ETFs or
|
||||
cryptocurrencies and make solid, data-driven investment decisions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -198,3 +194,19 @@
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="d-block row">
|
||||
<div class="outro-inner-container mx-auto">
|
||||
<div class="h-100 w-100"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div
|
||||
class="align-items-center d-flex flex-column justify-content-center w-100"
|
||||
>
|
||||
<gf-logo size="medium"></gf-logo>
|
||||
<div>Wealth Management Software</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import '~apps/client/src/styles/ghostfolio-style';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
|
||||
@ -13,19 +15,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
.intro-container {
|
||||
margin-top: -5rem;
|
||||
.intro {
|
||||
font-size: 4vw;
|
||||
line-height: 1;
|
||||
|
||||
.intro-inner-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
max-height: 66vh;
|
||||
@media (max-width: 575.98px) {
|
||||
font-size: 10vw;
|
||||
}
|
||||
}
|
||||
|
||||
.intro {
|
||||
background-image: url('/assets/intro.jpg');
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
.outro-inner-container {
|
||||
aspect-ratio: 16 / 9;
|
||||
max-height: 66vh;
|
||||
|
||||
div {
|
||||
background-image: url('/assets/intro.jpg');
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
border: 1px solid rgba(var(--dark-dividers));
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--palette-primary-500), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -37,9 +52,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.intro-container {
|
||||
.intro {
|
||||
.outro-inner-container {
|
||||
div {
|
||||
background-image: url('/assets/intro-dark.jpg') !important;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
border-color: rgba(var(--light-dividers));
|
||||
}
|
||||
}
|
||||
|
@ -68,7 +68,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
| 'value'
|
||||
>;
|
||||
};
|
||||
public positionsArray: PortfolioPosition[];
|
||||
public routeQueryParams: Subscription;
|
||||
public sectors: {
|
||||
[name: string]: { name: string; value: number };
|
||||
@ -87,9 +86,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,
|
||||
@ -116,9 +112,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the controller
|
||||
*/
|
||||
public ngOnInit() {
|
||||
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||
|
||||
@ -229,7 +222,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
};
|
||||
this.positions = {};
|
||||
this.positionsArray = [];
|
||||
this.sectors = {
|
||||
[UNKNOWN_KEY]: {
|
||||
name: UNKNOWN_KEY,
|
||||
@ -285,7 +277,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
|
||||
|
@ -37,12 +37,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 +66,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 +94,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"
|
||||
@ -129,11 +126,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 +155,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 +183,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 +214,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 +262,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>
|
||||
|
@ -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 {}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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 {}
|
@ -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 });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -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>
|
@ -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 {}
|
@ -0,0 +1,3 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
@ -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))
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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 +=
|
||||
|
@ -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">
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -32,7 +32,7 @@ import {
|
||||
PortfolioSummary,
|
||||
User
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { filterGlobalPermissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
@ -115,12 +115,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;
|
||||
}
|
||||
|
80
apps/client/src/assets/privacy-policy.md
Normal file
80
apps/client/src/assets/privacy-policy.md
Normal 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).
|
@ -1,5 +1,6 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /about/privacy-policy
|
||||
Disallow: /p/*
|
||||
|
||||
Sitemap: https://ghostfol.io/sitemap.xml
|
||||
|
@ -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;
|
||||
|
||||
|
@ -16,5 +16,8 @@
|
||||
"angularCompilerOptions": {
|
||||
"strictInjectionParameters": true,
|
||||
"strictTemplates": false
|
||||
},
|
||||
"compilerOptions": {
|
||||
"target": "es2020"
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
const { getJestProjects } = require('@nrwl/jest');
|
||||
|
||||
module.exports = { projects: getJestProjects() };
|
||||
export default { projects: getJestProjects() };
|
||||
|
3
jest.preset.js
Normal file
3
jest.preset.js
Normal file
@ -0,0 +1,3 @@
|
||||
const nxPreset = require('@nrwl/jest/preset').default;
|
||||
|
||||
module.exports = { ...nxPreset };
|
@ -1,3 +0,0 @@
|
||||
const nxPreset = require('@nrwl/jest/preset');
|
||||
|
||||
module.exports = { ...nxPreset };
|
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
displayName: 'common',
|
||||
|
||||
globals: {
|
||||
@ -9,5 +9,5 @@ module.exports = {
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/libs/common',
|
||||
preset: '../../jest.preset.ts'
|
||||
preset: '../../jest.preset.js'
|
||||
};
|
||||
|
@ -18,4 +18,5 @@ export interface EnhancedSymbolProfile {
|
||||
symbol: string;
|
||||
symbolMapping?: { [key: string]: string };
|
||||
updatedAt: Date;
|
||||
url?: string;
|
||||
}
|
||||
|
@ -10,7 +10,10 @@ export interface PortfolioPublicDetails {
|
||||
| 'currency'
|
||||
| 'markets'
|
||||
| 'name'
|
||||
| 'netPerformancePercent'
|
||||
| 'sectors'
|
||||
| 'symbol'
|
||||
| 'url'
|
||||
| 'value'
|
||||
>;
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
export default {
|
||||
displayName: 'ui',
|
||||
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
@ -18,5 +18,5 @@ module.exports = {
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment'
|
||||
],
|
||||
preset: '../../jest.preset.ts'
|
||||
preset: '../../jest.preset.js'
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
public filterGroups$: Subject<FilterGroup[]> = new BehaviorSubject([]);
|
||||
public filters$: Subject<Filter[]> = new BehaviorSubject([]);
|
||||
public filters: Observable<Filter[]> = this.filters$.asObservable();
|
||||
public searchControl = new FormControl();
|
||||
public searchControl = new FormControl<Filter | string>(undefined);
|
||||
public selectedFilters: Filter[] = [];
|
||||
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
|
||||
@ -50,7 +50,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
public constructor() {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((filterOrSearchTerm: Filter | string) => {
|
||||
.subscribe((filterOrSearchTerm) => {
|
||||
if (filterOrSearchTerm) {
|
||||
const searchTerm =
|
||||
typeof filterOrSearchTerm === 'string'
|
||||
@ -80,7 +80,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
input.value = '';
|
||||
}
|
||||
|
||||
this.searchControl.setValue(null);
|
||||
this.searchControl.setValue(undefined);
|
||||
}
|
||||
|
||||
public onRemoveFilter(aFilter: Filter): void {
|
||||
@ -99,7 +99,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
);
|
||||
this.updateFilters();
|
||||
this.searchInput.nativeElement.value = '';
|
||||
this.searchControl.setValue(null);
|
||||
this.searchControl.setValue(undefined);
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
Output,
|
||||
ViewChild
|
||||
} from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
@ -62,7 +61,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
public isUUID = isUUID;
|
||||
public placeholder = '';
|
||||
public routeQueryParams: Subscription;
|
||||
public searchControl = new FormControl();
|
||||
public searchKeywords: string[] = [];
|
||||
public totalFees: number;
|
||||
public totalValue: number;
|
||||
|
@ -4,7 +4,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { baseCurrency, locale } from '@ghostfolio/common/config';
|
||||
import { locale } from '@ghostfolio/common/config';
|
||||
import { Meta, Story, moduleMetadata } from '@storybook/angular';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
|
||||
@ -42,7 +42,7 @@ const Template: Story<FireCalculatorComponent> = (
|
||||
|
||||
export const Simple = Template.bind({});
|
||||
Simple.args = {
|
||||
currency: baseCurrency,
|
||||
currency: 'USD',
|
||||
fireWealth: 0,
|
||||
locale: locale
|
||||
};
|
||||
|
@ -51,10 +51,10 @@ export class FireCalculatorComponent
|
||||
@ViewChild('chartCanvas') chartCanvas;
|
||||
|
||||
public calculatorForm = this.formBuilder.group({
|
||||
annualInterestRate: new FormControl(),
|
||||
paymentPerPeriod: new FormControl(),
|
||||
principalInvestmentAmount: new FormControl(),
|
||||
time: new FormControl()
|
||||
annualInterestRate: new FormControl<number>(undefined),
|
||||
paymentPerPeriod: new FormControl<number>(undefined),
|
||||
principalInvestmentAmount: new FormControl<number>(undefined),
|
||||
time: new FormControl<number>(undefined)
|
||||
});
|
||||
public chart: Chart;
|
||||
public isLoading = true;
|
||||
@ -62,9 +62,6 @@ export class FireCalculatorComponent
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private fireCalculatorService: FireCalculatorService,
|
||||
@ -261,15 +258,13 @@ export class FireCalculatorComponent
|
||||
this.calculatorForm.get('principalInvestmentAmount').value || 0;
|
||||
|
||||
// Payment per period
|
||||
const PMT: number = parseFloat(
|
||||
this.calculatorForm.get('paymentPerPeriod').value
|
||||
);
|
||||
const PMT = this.calculatorForm.get('paymentPerPeriod').value;
|
||||
|
||||
// Annual interest rate
|
||||
const r: number = this.calculatorForm.get('annualInterestRate').value / 100;
|
||||
|
||||
// Time
|
||||
const t: number = parseFloat(this.calculatorForm.get('time').value);
|
||||
const t = this.calculatorForm.get('time').value;
|
||||
|
||||
for (let year = currentYear; year < currentYear + t; year++) {
|
||||
labels.push(year);
|
||||
|
@ -6,9 +6,6 @@ export class FireCalculatorService {
|
||||
private readonly COMPOUND_PERIOD = 12;
|
||||
private readonly CONTRIBUTION_PERIOD = 12;
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
public constructor() {}
|
||||
|
||||
public calculateCompoundInterest({
|
||||
|
@ -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>;
|
||||
|
1
libs/ui/src/lib/premium-indicator/index.ts
Normal file
1
libs/ui/src/lib/premium-indicator/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './premium-indicator.module';
|
@ -0,0 +1,6 @@
|
||||
<a
|
||||
class="align-items-center d-flex"
|
||||
[ngStyle]="{ 'pointer-events': enableLink ? 'initial' : 'none' }"
|
||||
[routerLink]="['/pricing']"
|
||||
><ion-icon class="text-muted" name="diamond-outline"></ion-icon
|
||||
></a>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user