Compare commits
26 Commits
Author | SHA1 | Date | |
---|---|---|---|
3e16041c16 | |||
5882b7914d | |||
69c9e259b1 | |||
aca37a27f9 | |||
313d2a2f79 | |||
9ac67b0af2 | |||
1e526852a7 | |||
e54638a684 | |||
0179823ad9 | |||
029b7bed9a | |||
635f10e2d0 | |||
cebf879d67 | |||
124bdc028d | |||
d69a69ce18 | |||
15344513ce | |||
b291d9e031 | |||
bee702302f | |||
bb56e09a13 | |||
0873f539c5 | |||
6dcd801d05 | |||
77065dac50 | |||
438484879d | |||
e37a650c70 | |||
6e8c90b3fc | |||
9e1a7fc981 | |||
ff638adf03 |
62
CHANGELOG.md
62
CHANGELOG.md
@ -5,6 +5,68 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## 1.100.0 - 05.01.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the _Top 3_ and _Bottom 3_ performers to the analysis page
|
||||||
|
- Added a blog post
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the routing of the create activity dialog
|
||||||
|
- Fixed the link color in the blog posts
|
||||||
|
|
||||||
|
## 1.99.0 - 01.01.2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Exposed the profile data gathering by symbol as an endpoint
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Improved the portfolio analysis page: show the y-axis and extend the chart in relation to the days in market
|
||||||
|
- Restructured the about page
|
||||||
|
- Start refactoring _transactions_ to _activities_
|
||||||
|
- Refactored the demo user id
|
||||||
|
- Upgraded `angular` from version `13.0.2` to `13.1.1`
|
||||||
|
- Upgraded `chart.js` from version `3.5.0` to `3.7.0`
|
||||||
|
- Upgraded `Nx` from version `13.3.0` to `13.4.1`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Hid the data provider warning while loading
|
||||||
|
- Fixed an exception with the market state caused by a failed data provider request
|
||||||
|
- Fixed an exception in the portfolio position endpoint
|
||||||
|
- Fixed the reload of the position detail dialog (with query parameters)
|
||||||
|
- Fixed the missing mapping for Russia in the data enhancer for symbol profile data via _Trackinsight_
|
||||||
|
|
||||||
|
## 1.98.0 - 29.12.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the date range component to the holdings tab
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Extended the statistics section on the about page (users in Slack community)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed the creation of historical data in the admin control panel (upsert instead of update)
|
||||||
|
- Fixed the scrolling issue in the position detail dialog on mobile
|
||||||
|
|
||||||
|
## 1.97.0 - 28.12.2021
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the transactions to the position detail dialog
|
||||||
|
- Added support for dividend
|
||||||
|
|
||||||
|
### Todo
|
||||||
|
|
||||||
|
- Apply data migration (`yarn database:migrate`)
|
||||||
|
|
||||||
## 1.96.0 - 27.12.2021
|
## 1.96.0 - 27.12.2021
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -96,6 +96,29 @@ export class AdminController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('gather/profile-data/:dataSource/:symbol')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async gatherProfileDataForSymbol(
|
||||||
|
@Param('dataSource') dataSource: DataSource,
|
||||||
|
@Param('symbol') symbol: string
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
!hasPermission(
|
||||||
|
this.request.user.permissions,
|
||||||
|
permissions.accessAdminControl
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new HttpException(
|
||||||
|
getReasonPhrase(StatusCodes.FORBIDDEN),
|
||||||
|
StatusCodes.FORBIDDEN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataGatheringService.gatherProfileData([{ dataSource, symbol }]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
@Post('gather/:dataSource/:symbol')
|
@Post('gather/:dataSource/:symbol')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async gatherSymbol(
|
public async gatherSymbol(
|
||||||
@ -215,7 +238,7 @@ export class AdminController {
|
|||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
|
|
||||||
return this.marketDataService.updateMarketData({
|
return this.marketDataService.updateMarketData({
|
||||||
data,
|
data: { ...data, dataSource },
|
||||||
where: {
|
where: {
|
||||||
date_symbol: {
|
date_symbol: {
|
||||||
date,
|
date,
|
||||||
|
@ -6,7 +6,9 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
import { PropertyService } from '@ghostfolio/api/services/property/property.service';
|
||||||
import {
|
import {
|
||||||
|
DEMO_USER_ID,
|
||||||
PROPERTY_IS_READ_ONLY_MODE,
|
PROPERTY_IS_READ_ONLY_MODE,
|
||||||
|
PROPERTY_SLACK_COMMUNITY_USERS,
|
||||||
PROPERTY_STRIPE_CONFIG,
|
PROPERTY_STRIPE_CONFIG,
|
||||||
PROPERTY_SYSTEM_MESSAGE
|
PROPERTY_SYSTEM_MESSAGE
|
||||||
} from '@ghostfolio/common/config';
|
} from '@ghostfolio/common/config';
|
||||||
@ -22,7 +24,6 @@ import { subDays } from 'date-fns';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class InfoService {
|
export class InfoService {
|
||||||
private static CACHE_KEY_STATISTICS = 'STATISTICS';
|
private static CACHE_KEY_STATISTICS = 'STATISTICS';
|
||||||
private static DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly configurationService: ConfigurationService,
|
private readonly configurationService: ConfigurationService,
|
||||||
@ -187,9 +188,15 @@ export class InfoService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async countSlackCommunityUsers() {
|
||||||
|
return (await this.propertyService.getByKey(
|
||||||
|
PROPERTY_SLACK_COMMUNITY_USERS
|
||||||
|
)) as string;
|
||||||
|
}
|
||||||
|
|
||||||
private getDemoAuthToken() {
|
private getDemoAuthToken() {
|
||||||
return this.jwtService.sign({
|
return this.jwtService.sign({
|
||||||
id: InfoService.DEMO_USER_ID
|
id: DEMO_USER_ID
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,19 +225,19 @@ export class InfoService {
|
|||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
const activeUsers1d = await this.countActiveUsers(1);
|
const activeUsers1d = await this.countActiveUsers(1);
|
||||||
const activeUsers7d = await this.countActiveUsers(7);
|
|
||||||
const activeUsers30d = await this.countActiveUsers(30);
|
const activeUsers30d = await this.countActiveUsers(30);
|
||||||
const newUsers30d = await this.countNewUsers(30);
|
const newUsers30d = await this.countNewUsers(30);
|
||||||
const gitHubContributors = await this.countGitHubContributors();
|
const gitHubContributors = await this.countGitHubContributors();
|
||||||
const gitHubStargazers = await this.countGitHubStargazers();
|
const gitHubStargazers = await this.countGitHubStargazers();
|
||||||
|
const slackCommunityUsers = await this.countSlackCommunityUsers();
|
||||||
|
|
||||||
statistics = {
|
statistics = {
|
||||||
activeUsers1d,
|
activeUsers1d,
|
||||||
activeUsers7d,
|
|
||||||
activeUsers30d,
|
activeUsers30d,
|
||||||
gitHubContributors,
|
gitHubContributors,
|
||||||
gitHubStargazers,
|
gitHubStargazers,
|
||||||
newUsers30d
|
newUsers30d,
|
||||||
|
slackCommunityUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.redisCacheService.set(
|
await this.redisCacheService.set(
|
||||||
|
@ -66,28 +66,21 @@ export class OrderController {
|
|||||||
this.request.user.id
|
this.request.user.id
|
||||||
);
|
);
|
||||||
|
|
||||||
let orders = await this.orderService.orders({
|
let orders = await this.orderService.getOrders({
|
||||||
include: {
|
includeDrafts: true,
|
||||||
Account: {
|
userId: impersonationUserId || this.request.user.id
|
||||||
include: {
|
|
||||||
Platform: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
SymbolProfile: {
|
|
||||||
select: {
|
|
||||||
name: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
orderBy: { date: 'desc' },
|
|
||||||
where: { userId: impersonationUserId || this.request.user.id }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
impersonationUserId ||
|
impersonationUserId ||
|
||||||
this.userService.isRestrictedView(this.request.user)
|
this.userService.isRestrictedView(this.request.user)
|
||||||
) {
|
) {
|
||||||
orders = nullifyValuesInObjects(orders, ['fee', 'quantity', 'unitPrice']);
|
orders = nullifyValuesInObjects(orders, [
|
||||||
|
'fee',
|
||||||
|
'quantity',
|
||||||
|
'unitPrice',
|
||||||
|
'value'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return orders;
|
return orders;
|
||||||
|
@ -3,7 +3,8 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
|
|||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DataSource, Order, Prisma } from '@prisma/client';
|
import { DataSource, Order, Prisma, Type as TypeOfOrder } from '@prisma/client';
|
||||||
|
import Big from 'big.js';
|
||||||
import { endOfToday, isAfter } from 'date-fns';
|
import { endOfToday, isAfter } from 'date-fns';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -82,11 +83,13 @@ export class OrderService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public getOrders({
|
public async getOrders({
|
||||||
includeDrafts = false,
|
includeDrafts = false,
|
||||||
|
types,
|
||||||
userId
|
userId
|
||||||
}: {
|
}: {
|
||||||
includeDrafts?: boolean;
|
includeDrafts?: boolean;
|
||||||
|
types?: TypeOfOrder[];
|
||||||
userId: string;
|
userId: string;
|
||||||
}) {
|
}) {
|
||||||
const where: Prisma.OrderWhereInput = { userId };
|
const where: Prisma.OrderWhereInput = { userId };
|
||||||
@ -95,15 +98,39 @@ export class OrderService {
|
|||||||
where.isDraft = false;
|
where.isDraft = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.orders({
|
if (types) {
|
||||||
where,
|
where.OR = types.map((type) => {
|
||||||
include: {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
type: {
|
||||||
Account: true,
|
equals: type
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
}
|
||||||
SymbolProfile: true
|
};
|
||||||
},
|
});
|
||||||
orderBy: { date: 'asc' }
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
await this.orders({
|
||||||
|
where,
|
||||||
|
include: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
Account: {
|
||||||
|
include: {
|
||||||
|
Platform: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
SymbolProfile: true
|
||||||
|
},
|
||||||
|
orderBy: { date: 'asc' }
|
||||||
|
})
|
||||||
|
).map((order) => {
|
||||||
|
return {
|
||||||
|
...order,
|
||||||
|
value: new Big(order.quantity)
|
||||||
|
.mul(order.unitPrice)
|
||||||
|
.plus(order.fee)
|
||||||
|
.toNumber()
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { AssetClass, AssetSubClass } from '@prisma/client';
|
import { AssetClass, AssetSubClass } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPositionDetail {
|
export interface PortfolioPositionDetail {
|
||||||
@ -16,6 +17,7 @@ export interface PortfolioPositionDetail {
|
|||||||
name: string;
|
name: string;
|
||||||
netPerformance: number;
|
netPerformance: number;
|
||||||
netPerformancePercent: number;
|
netPerformancePercent: number;
|
||||||
|
orders: OrderWithAccount[];
|
||||||
quantity: number;
|
quantity: number;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
|
@ -10,12 +10,12 @@ import { baseCurrency } from '@ghostfolio/common/config';
|
|||||||
import {
|
import {
|
||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
|
PortfolioInvestments,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary
|
PortfolioSummary
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
|
||||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||||
import {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
@ -48,42 +48,6 @@ export class PortfolioController {
|
|||||||
private readonly userService: UserService
|
private readonly userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('investments')
|
|
||||||
@UseGuards(AuthGuard('jwt'))
|
|
||||||
public async findAll(
|
|
||||||
@Headers('impersonation-id') impersonationId: string,
|
|
||||||
@Res() res: Response
|
|
||||||
): Promise<InvestmentItem[]> {
|
|
||||||
if (
|
|
||||||
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
|
||||||
this.request.user.subscription.type === 'Basic'
|
|
||||||
) {
|
|
||||||
res.status(StatusCodes.FORBIDDEN);
|
|
||||||
return <any>res.json([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let investments = await this.portfolioService.getInvestments(
|
|
||||||
impersonationId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
impersonationId ||
|
|
||||||
this.userService.isRestrictedView(this.request.user)
|
|
||||||
) {
|
|
||||||
const maxInvestment = investments.reduce(
|
|
||||||
(investment, item) => Math.max(investment, item.investment),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
|
|
||||||
investments = investments.map((item) => ({
|
|
||||||
date: item.date,
|
|
||||||
investment: item.investment / maxInvestment
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return <any>res.json(investments);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('chart')
|
@Get('chart')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getChart(
|
public async getChart(
|
||||||
@ -200,6 +164,42 @@ export class PortfolioController {
|
|||||||
return <any>res.json({ accounts, hasError, holdings });
|
return <any>res.json({ accounts, hasError, holdings });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get('investments')
|
||||||
|
@UseGuards(AuthGuard('jwt'))
|
||||||
|
public async getInvestments(
|
||||||
|
@Headers('impersonation-id') impersonationId: string,
|
||||||
|
@Res() res: Response
|
||||||
|
): Promise<PortfolioInvestments> {
|
||||||
|
if (
|
||||||
|
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
|
||||||
|
this.request.user.subscription.type === 'Basic'
|
||||||
|
) {
|
||||||
|
res.status(StatusCodes.FORBIDDEN);
|
||||||
|
return <any>res.json({});
|
||||||
|
}
|
||||||
|
|
||||||
|
let investments = await this.portfolioService.getInvestments(
|
||||||
|
impersonationId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
impersonationId ||
|
||||||
|
this.userService.isRestrictedView(this.request.user)
|
||||||
|
) {
|
||||||
|
const maxInvestment = investments.reduce(
|
||||||
|
(investment, item) => Math.max(investment, item.investment),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
investments = investments.map((item) => ({
|
||||||
|
date: item.date,
|
||||||
|
investment: item.investment / maxInvestment
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return <any>res.json({ firstOrderDate: investments[0]?.date, investments });
|
||||||
|
}
|
||||||
|
|
||||||
@Get('performance')
|
@Get('performance')
|
||||||
@UseGuards(AuthGuard('jwt'))
|
@UseGuards(AuthGuard('jwt'))
|
||||||
public async getPerformance(
|
public async getPerformance(
|
||||||
@ -330,6 +330,7 @@ export class PortfolioController {
|
|||||||
'currentGrossPerformance',
|
'currentGrossPerformance',
|
||||||
'currentNetPerformance',
|
'currentNetPerformance',
|
||||||
'currentValue',
|
'currentValue',
|
||||||
|
'dividend',
|
||||||
'fees',
|
'fees',
|
||||||
'netWorth',
|
'netWorth',
|
||||||
'totalBuy',
|
'totalBuy',
|
||||||
@ -360,6 +361,7 @@ export class PortfolioController {
|
|||||||
'grossPerformance',
|
'grossPerformance',
|
||||||
'investment',
|
'investment',
|
||||||
'netPerformance',
|
'netPerformance',
|
||||||
|
'orders',
|
||||||
'quantity',
|
'quantity',
|
||||||
'value'
|
'value'
|
||||||
]);
|
]);
|
||||||
|
@ -55,7 +55,7 @@ import {
|
|||||||
subDays,
|
subDays,
|
||||||
subYears
|
subYears
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty } from 'lodash';
|
import { isEmpty, sortBy } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HistoricalDataContainer,
|
HistoricalDataContainer,
|
||||||
@ -150,12 +150,33 @@ export class PortfolioService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return portfolioCalculator.getInvestments().map((item) => {
|
const investments = portfolioCalculator.getInvestments().map((item) => {
|
||||||
return {
|
return {
|
||||||
date: item.date,
|
date: item.date,
|
||||||
investment: item.investment.toNumber()
|
investment: item.investment.toNumber()
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add investment of today
|
||||||
|
const investmentOfToday = investments.filter((investment) => {
|
||||||
|
return investment.date === format(new Date(), DATE_FORMAT);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (investmentOfToday.length <= 0) {
|
||||||
|
const pastInvestments = investments.filter((investment) => {
|
||||||
|
return isBefore(parseDate(investment.date), new Date());
|
||||||
|
});
|
||||||
|
const lastInvestment = pastInvestments[pastInvestments.length - 1];
|
||||||
|
|
||||||
|
investments.push({
|
||||||
|
date: format(new Date(), DATE_FORMAT),
|
||||||
|
investment: lastInvestment?.investment ?? 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortBy(investments, (investment) => {
|
||||||
|
return investment.date;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChart(
|
public async getChart(
|
||||||
@ -388,6 +409,7 @@ export class PortfolioService {
|
|||||||
name: undefined,
|
name: undefined,
|
||||||
netPerformance: undefined,
|
netPerformance: undefined,
|
||||||
netPerformancePercent: undefined,
|
netPerformancePercent: undefined,
|
||||||
|
orders: [],
|
||||||
quantity: undefined,
|
quantity: undefined,
|
||||||
symbol: aSymbol,
|
symbol: aSymbol,
|
||||||
transactionCount: undefined,
|
transactionCount: undefined,
|
||||||
@ -400,17 +422,21 @@ export class PortfolioService {
|
|||||||
const positionCurrency = orders[0].currency;
|
const positionCurrency = orders[0].currency;
|
||||||
const name = orders[0].SymbolProfile?.name ?? '';
|
const name = orders[0].SymbolProfile?.name ?? '';
|
||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders.map((order) => ({
|
const portfolioOrders: PortfolioOrder[] = orders
|
||||||
currency: order.currency,
|
.filter((order) => {
|
||||||
dataSource: order.dataSource,
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
date: format(order.date, DATE_FORMAT),
|
})
|
||||||
fee: new Big(order.fee),
|
.map((order) => ({
|
||||||
name: order.SymbolProfile?.name,
|
currency: order.currency,
|
||||||
quantity: new Big(order.quantity),
|
dataSource: order.dataSource,
|
||||||
symbol: order.symbol,
|
date: format(order.date, DATE_FORMAT),
|
||||||
type: order.type,
|
fee: new Big(order.fee),
|
||||||
unitPrice: new Big(order.unitPrice)
|
name: order.SymbolProfile?.name,
|
||||||
}));
|
quantity: new Big(order.quantity),
|
||||||
|
symbol: order.symbol,
|
||||||
|
type: order.type,
|
||||||
|
unitPrice: new Big(order.unitPrice)
|
||||||
|
}));
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator(
|
const portfolioCalculator = new PortfolioCalculator(
|
||||||
this.currentRateService,
|
this.currentRateService,
|
||||||
@ -442,17 +468,17 @@ export class PortfolioService {
|
|||||||
// Convert investment, gross and net performance to currency of user
|
// Convert investment, gross and net performance to currency of user
|
||||||
const userCurrency = this.request.user.Settings.currency;
|
const userCurrency = this.request.user.Settings.currency;
|
||||||
const investment = this.exchangeRateDataService.toCurrency(
|
const investment = this.exchangeRateDataService.toCurrency(
|
||||||
position.investment.toNumber(),
|
position.investment?.toNumber(),
|
||||||
currency,
|
currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
const grossPerformance = this.exchangeRateDataService.toCurrency(
|
const grossPerformance = this.exchangeRateDataService.toCurrency(
|
||||||
position.grossPerformance.toNumber(),
|
position.grossPerformance?.toNumber(),
|
||||||
currency,
|
currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
const netPerformance = this.exchangeRateDataService.toCurrency(
|
const netPerformance = this.exchangeRateDataService.toCurrency(
|
||||||
position.netPerformance.toNumber(),
|
position.netPerformance?.toNumber(),
|
||||||
currency,
|
currency,
|
||||||
userCurrency
|
userCurrency
|
||||||
);
|
);
|
||||||
@ -521,6 +547,7 @@ export class PortfolioService {
|
|||||||
minPrice,
|
minPrice,
|
||||||
name,
|
name,
|
||||||
netPerformance,
|
netPerformance,
|
||||||
|
orders,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
averagePrice: averagePrice.toNumber(),
|
averagePrice: averagePrice.toNumber(),
|
||||||
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
grossPerformancePercent: position.grossPerformancePercentage.toNumber(),
|
||||||
@ -578,6 +605,7 @@ export class PortfolioService {
|
|||||||
maxPrice,
|
maxPrice,
|
||||||
minPrice,
|
minPrice,
|
||||||
name,
|
name,
|
||||||
|
orders,
|
||||||
averagePrice: 0,
|
averagePrice: 0,
|
||||||
currency: currentData[aSymbol]?.currency,
|
currency: currentData[aSymbol]?.currency,
|
||||||
firstBuyDate: undefined,
|
firstBuyDate: undefined,
|
||||||
@ -655,7 +683,9 @@ export class PortfolioService {
|
|||||||
grossPerformancePercentage:
|
grossPerformancePercentage:
|
||||||
position.grossPerformancePercentage?.toNumber() ?? null,
|
position.grossPerformancePercentage?.toNumber() ?? null,
|
||||||
investment: new Big(position.investment).toNumber(),
|
investment: new Big(position.investment).toNumber(),
|
||||||
marketState: dataProviderResponses[position.symbol].marketState,
|
marketState:
|
||||||
|
dataProviderResponses[position.symbol]?.marketState ??
|
||||||
|
MarketState.delayed,
|
||||||
name: symbolProfileMap[position.symbol].name,
|
name: symbolProfileMap[position.symbol].name,
|
||||||
netPerformance: position.netPerformance?.toNumber() ?? null,
|
netPerformance: position.netPerformance?.toNumber() ?? null,
|
||||||
netPerformancePercentage:
|
netPerformancePercentage:
|
||||||
@ -726,22 +756,6 @@ export class PortfolioService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFees(orders: OrderWithAccount[], date = new Date(0)) {
|
|
||||||
return orders
|
|
||||||
.filter((order) => {
|
|
||||||
// Filter out all orders before given date
|
|
||||||
return isBefore(date, new Date(order.date));
|
|
||||||
})
|
|
||||||
.map((order) => {
|
|
||||||
return this.exchangeRateDataService.toCurrency(
|
|
||||||
order.fee,
|
|
||||||
order.currency,
|
|
||||||
this.request.user.Settings.currency
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.reduce((previous, current) => previous + current, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
public async getReport(impersonationId: string): Promise<PortfolioReport> {
|
||||||
const currency = this.request.user.Settings.currency;
|
const currency = this.request.user.Settings.currency;
|
||||||
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
const userId = await this.getUserId(impersonationId, this.request.user.id);
|
||||||
@ -822,7 +836,7 @@ export class PortfolioService {
|
|||||||
new FeeRatioInitialInvestment(
|
new FeeRatioInitialInvestment(
|
||||||
this.exchangeRateDataService,
|
this.exchangeRateDataService,
|
||||||
currentPositions.totalInvestment.toNumber(),
|
currentPositions.totalInvestment.toNumber(),
|
||||||
this.getFees(orders)
|
this.getFees(orders).toNumber()
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
{ baseCurrency: currency }
|
{ baseCurrency: currency }
|
||||||
@ -841,8 +855,11 @@ export class PortfolioService {
|
|||||||
userId,
|
userId,
|
||||||
currency
|
currency
|
||||||
);
|
);
|
||||||
const orders = await this.orderService.getOrders({ userId });
|
const orders = await this.orderService.getOrders({
|
||||||
const fees = this.getFees(orders);
|
userId
|
||||||
|
});
|
||||||
|
const dividend = this.getDividend(orders).toNumber();
|
||||||
|
const fees = this.getFees(orders).toNumber();
|
||||||
const firstOrderDate = orders[0]?.date;
|
const firstOrderDate = orders[0]?.date;
|
||||||
|
|
||||||
const totalBuy = this.getTotalByType(orders, currency, 'BUY');
|
const totalBuy = this.getTotalByType(orders, currency, 'BUY');
|
||||||
@ -856,14 +873,17 @@ export class PortfolioService {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...performanceInformation.performance,
|
...performanceInformation.performance,
|
||||||
|
dividend,
|
||||||
fees,
|
fees,
|
||||||
firstOrderDate,
|
firstOrderDate,
|
||||||
netWorth,
|
netWorth,
|
||||||
|
totalBuy,
|
||||||
|
totalSell,
|
||||||
cash: balance,
|
cash: balance,
|
||||||
committedFunds: committedFunds.toNumber(),
|
committedFunds: committedFunds.toNumber(),
|
||||||
ordersCount: orders.length,
|
ordersCount: orders.filter((order) => {
|
||||||
totalBuy: totalBuy,
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
totalSell: totalSell
|
}).length
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -936,6 +956,47 @@ export class PortfolioService {
|
|||||||
return cashPositions;
|
return cashPositions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getDividend(orders: OrderWithAccount[], date = new Date(0)) {
|
||||||
|
return orders
|
||||||
|
.filter((order) => {
|
||||||
|
// Filter out all orders before given date and type dividend
|
||||||
|
return (
|
||||||
|
isBefore(date, new Date(order.date)) &&
|
||||||
|
order.type === TypeOfOrder.DIVIDEND
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((order) => {
|
||||||
|
return this.exchangeRateDataService.toCurrency(
|
||||||
|
new Big(order.quantity).mul(order.unitPrice).toNumber(),
|
||||||
|
order.currency,
|
||||||
|
this.request.user.Settings.currency
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
(previous, current) => new Big(previous).plus(current),
|
||||||
|
new Big(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFees(orders: OrderWithAccount[], date = new Date(0)) {
|
||||||
|
return orders
|
||||||
|
.filter((order) => {
|
||||||
|
// Filter out all orders before given date
|
||||||
|
return isBefore(date, new Date(order.date));
|
||||||
|
})
|
||||||
|
.map((order) => {
|
||||||
|
return this.exchangeRateDataService.toCurrency(
|
||||||
|
order.fee,
|
||||||
|
order.currency,
|
||||||
|
this.request.user.Settings.currency
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
(previous, current) => new Big(previous).plus(current),
|
||||||
|
new Big(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
|
private getStartDate(aDateRange: DateRange, portfolioStart: Date) {
|
||||||
switch (aDateRange) {
|
switch (aDateRange) {
|
||||||
case '1d':
|
case '1d':
|
||||||
@ -964,7 +1025,11 @@ export class PortfolioService {
|
|||||||
transactionPoints: TransactionPoint[];
|
transactionPoints: TransactionPoint[];
|
||||||
orders: OrderWithAccount[];
|
orders: OrderWithAccount[];
|
||||||
}> {
|
}> {
|
||||||
const orders = await this.orderService.getOrders({ includeDrafts, userId });
|
const orders = await this.orderService.getOrders({
|
||||||
|
includeDrafts,
|
||||||
|
userId,
|
||||||
|
types: ['BUY', 'SELL']
|
||||||
|
});
|
||||||
|
|
||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
return { transactionPoints: [], orders: [] };
|
return { transactionPoints: [], orders: [] };
|
||||||
|
@ -7,6 +7,9 @@ const getJSON = bent('json');
|
|||||||
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
||||||
private static baseUrl = 'https://data.trackinsight.com/holdings';
|
private static baseUrl = 'https://data.trackinsight.com/holdings';
|
||||||
private static countries = require('countries-list/dist/countries.json');
|
private static countries = require('countries-list/dist/countries.json');
|
||||||
|
private static countriesMapping = {
|
||||||
|
'Russian Federation': 'Russia'
|
||||||
|
};
|
||||||
private static sectorsMapping = {
|
private static sectorsMapping = {
|
||||||
'Consumer Discretionary': 'Consumer Cyclical',
|
'Consumer Discretionary': 'Consumer Cyclical',
|
||||||
'Consumer Defensive': 'Consumer Staples',
|
'Consumer Defensive': 'Consumer Staples',
|
||||||
@ -45,7 +48,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
|
|||||||
for (const [key, country] of Object.entries<any>(
|
for (const [key, country] of Object.entries<any>(
|
||||||
TrackinsightDataEnhancerService.countries
|
TrackinsightDataEnhancerService.countries
|
||||||
)) {
|
)) {
|
||||||
if (country.name === name) {
|
if (
|
||||||
|
country.name === name ||
|
||||||
|
country.name ===
|
||||||
|
TrackinsightDataEnhancerService.countriesMapping[name]
|
||||||
|
) {
|
||||||
countryCode = key;
|
countryCode = key;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import { UpdateMarketDataDto } from '@ghostfolio/api/app/admin/update-market-data.dto';
|
||||||
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
|
import { DateQuery } from '@ghostfolio/api/app/portfolio/interfaces/date-query.interface';
|
||||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||||
import { resetHours } from '@ghostfolio/common/helper';
|
import { resetHours } from '@ghostfolio/common/helper';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { MarketData, Prisma } from '@prisma/client';
|
import { DataSource, MarketData, Prisma } from '@prisma/client';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MarketDataService {
|
export class MarketDataService {
|
||||||
@ -67,14 +68,20 @@ export class MarketDataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async updateMarketData(params: {
|
public async updateMarketData(params: {
|
||||||
data: Prisma.MarketDataUpdateInput;
|
data: { dataSource: DataSource } & UpdateMarketDataDto;
|
||||||
where: Prisma.MarketDataWhereUniqueInput;
|
where: Prisma.MarketDataWhereUniqueInput;
|
||||||
}): Promise<MarketData> {
|
}): Promise<MarketData> {
|
||||||
const { data, where } = params;
|
const { data, where } = params;
|
||||||
|
|
||||||
return this.prismaService.marketData.update({
|
return this.prismaService.marketData.upsert({
|
||||||
data,
|
where,
|
||||||
where
|
create: {
|
||||||
|
dataSource: data.dataSource,
|
||||||
|
date: where.date_symbol.date,
|
||||||
|
marketPrice: data.marketPrice,
|
||||||
|
symbol: where.date_symbol.symbol
|
||||||
|
},
|
||||||
|
update: { marketPrice: data.marketPrice }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,13 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
import('./pages/about/about-page.module').then((m) => m.AboutPageModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'about/changelog',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/about/changelog/changelog-page.module').then(
|
||||||
|
(m) => m.ChangelogPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'account',
|
path: 'account',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -33,6 +40,11 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
|
import('./pages/auth/auth-page.module').then((m) => m.AuthPageModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'blog',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'de/blog/2021/07/hallo-ghostfolio',
|
path: 'de/blog/2021/07/hallo-ghostfolio',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -47,6 +59,13 @@ const routes: Routes = [
|
|||||||
'./pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module'
|
'./pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.module'
|
||||||
).then((m) => m.HelloGhostfolioPageModule)
|
).then((m) => m.HelloGhostfolioPageModule)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'en/blog/2022/01/ghostfolio-first-months-in-open-source',
|
||||||
|
loadChildren: () =>
|
||||||
|
import(
|
||||||
|
'./pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.module'
|
||||||
|
).then((m) => m.FirstMonthsInOpenSourcePageModule)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'home',
|
path: 'home',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -66,6 +85,13 @@ const routes: Routes = [
|
|||||||
(m) => m.PortfolioPageModule
|
(m) => m.PortfolioPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'portfolio/activities',
|
||||||
|
loadChildren: () =>
|
||||||
|
import('./pages/portfolio/transactions/transactions-page.module').then(
|
||||||
|
(m) => m.TransactionsPageModule
|
||||||
|
)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'portfolio/allocations',
|
path: 'portfolio/allocations',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
@ -87,13 +113,6 @@ const routes: Routes = [
|
|||||||
(m) => m.ReportPageModule
|
(m) => m.ReportPageModule
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'portfolio/transactions',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./pages/portfolio/transactions/transactions-page.module').then(
|
|
||||||
(m) => m.TransactionsPageModule
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'pricing',
|
path: 'pricing',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
@ -5,10 +5,10 @@ import { MatInputModule } from '@angular/material/input';
|
|||||||
import { MatMenuModule } from '@angular/material/menu';
|
import { MatMenuModule } from '@angular/material/menu';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
|
||||||
import { AccountsTableComponent } from './accounts-table.component';
|
import { AccountsTableComponent } from './accounts-table.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@ -43,6 +43,19 @@ export class AdminMarketDataComponent implements OnDestroy, OnInit {
|
|||||||
this.fetchAdminMarketData();
|
this.fetchAdminMarketData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onGatherProfileDataBySymbol({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
this.adminService
|
||||||
|
.gatherProfileDataBySymbol({ dataSource, symbol })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
public onGatherSymbol({
|
public onGatherSymbol({
|
||||||
dataSource,
|
dataSource,
|
||||||
symbol
|
symbol
|
||||||
|
@ -38,6 +38,13 @@
|
|||||||
>
|
>
|
||||||
Gather Data
|
Gather Data
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
i18n
|
||||||
|
mat-menu-item
|
||||||
|
(click)="onGatherProfileDataBySymbol({dataSource: item.dataSource, symbol: item.symbol})"
|
||||||
|
>
|
||||||
|
Gather Profile Data
|
||||||
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<button
|
<button
|
||||||
*ngIf="deviceType === 'mobile'"
|
*ngIf="deviceType === 'mobile'"
|
||||||
|
class="mt-2"
|
||||||
mat-button
|
mat-button
|
||||||
(click)="onClickCloseButton()"
|
(click)="onClickCloseButton()"
|
||||||
>
|
>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-bottom: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import {
|
import {
|
||||||
RANGE,
|
RANGE,
|
||||||
SettingsStorageService
|
SettingsStorageService
|
||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
||||||
import { Position, User } from '@ghostfolio/common/interfaces';
|
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||||
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
@ -19,6 +23,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
||||||
public dateRange: DateRange;
|
public dateRange: DateRange;
|
||||||
|
public dateRangeOptions = defaultDateRangeOptions;
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasPermissionToCreateOrder: boolean;
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public positions: Position[];
|
public positions: Position[];
|
||||||
@ -33,9 +38,20 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
private settingsStorageService: SettingsStorageService,
|
private settingsStorageService: SettingsStorageService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {
|
) {
|
||||||
|
route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (params['positionDetailDialog'] && params['symbol']) {
|
||||||
|
this.openPositionDialog({ symbol: params['symbol'] });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.userService.stateChanged
|
this.userService.stateChanged
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
@ -64,12 +80,48 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public onChangeDateRange(aDateRange: DateRange) {
|
||||||
|
this.dateRange = aDateRange;
|
||||||
|
this.settingsStorageService.setSetting(RANGE, this.dateRange);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openPositionDialog({ symbol }: { symbol: string }) {
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: {
|
||||||
|
symbol,
|
||||||
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private update() {
|
private update() {
|
||||||
|
this.positions = undefined;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositions({ range: this.dateRange })
|
.fetchPositions({ range: this.dateRange })
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
@ -1,4 +1,12 @@
|
|||||||
<div class="container justify-content-center pb-3 px-3">
|
<div class="container justify-content-center p-3">
|
||||||
|
<div class="mb-3 text-center">
|
||||||
|
<gf-toggle
|
||||||
|
[defaultValue]="dateRange"
|
||||||
|
[isLoading]="positions === undefined"
|
||||||
|
[options]="dateRangeOptions"
|
||||||
|
(change)="onChangeDateRange($event.value)"
|
||||||
|
></gf-toggle>
|
||||||
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
<div class="align-items-center col-xs-12 col-md-8 offset-md-2">
|
||||||
<mat-card class="p-0">
|
<mat-card class="p-0">
|
||||||
@ -6,6 +14,7 @@
|
|||||||
<gf-positions
|
<gf-positions
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="positions"
|
[positions]="positions"
|
||||||
[range]="dateRange"
|
[range]="dateRange"
|
||||||
@ -17,8 +26,8 @@
|
|||||||
class="mt-3"
|
class="mt-3"
|
||||||
i18n
|
i18n
|
||||||
mat-button
|
mat-button
|
||||||
[routerLink]="['/portfolio', 'transactions']"
|
[routerLink]="['/portfolio', 'activities']"
|
||||||
>Manage Transactions...</a
|
>Manage Activities...</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,7 +3,9 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
||||||
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';
|
||||||
|
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
|
||||||
|
|
||||||
import { HomeHoldingsComponent } from './home-holdings.component';
|
import { HomeHoldingsComponent } from './home-holdings.component';
|
||||||
|
|
||||||
@ -12,7 +14,9 @@ import { HomeHoldingsComponent } from './home-holdings.component';
|
|||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfPositionDetailDialogModule,
|
||||||
GfPositionsModule,
|
GfPositionsModule,
|
||||||
|
GfToggleModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule,
|
MatCardModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import {
|
import {
|
||||||
@ -7,7 +6,9 @@ import {
|
|||||||
SettingsStorageService
|
SettingsStorageService
|
||||||
} from '@ghostfolio/client/services/settings-storage.service';
|
} from '@ghostfolio/client/services/settings-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { defaultDateRangeOptions } from '@ghostfolio/common/config';
|
||||||
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces';
|
import { PortfolioPerformance, User } from '@ghostfolio/common/interfaces';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
@ -21,16 +22,11 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
})
|
})
|
||||||
export class HomeOverviewComponent implements OnDestroy, OnInit {
|
export class HomeOverviewComponent implements OnDestroy, OnInit {
|
||||||
public dateRange: DateRange;
|
public dateRange: DateRange;
|
||||||
public dateRangeOptions: ToggleOption[] = [
|
public dateRangeOptions = defaultDateRangeOptions;
|
||||||
{ label: 'Today', value: '1d' },
|
|
||||||
{ label: 'YTD', value: 'ytd' },
|
|
||||||
{ label: '1Y', value: '1y' },
|
|
||||||
{ label: '5Y', value: '5y' },
|
|
||||||
{ label: 'Max', value: 'max' }
|
|
||||||
];
|
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasError: boolean;
|
public hasError: boolean;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public historicalDataItems: LineChartItem[];
|
public historicalDataItems: LineChartItem[];
|
||||||
public isAllTimeHigh: boolean;
|
public isAllTimeHigh: boolean;
|
||||||
public isAllTimeLow: boolean;
|
public isAllTimeLow: boolean;
|
||||||
@ -57,6 +53,11 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToCreateOrder = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.createOrder
|
||||||
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
[showYAxis]="false"
|
[showYAxis]="false"
|
||||||
></gf-line-chart>
|
></gf-line-chart>
|
||||||
<div
|
<div
|
||||||
*ngIf="historicalDataItems?.length === 0"
|
*ngIf="hasPermissionToCreateOrder&& historicalDataItems?.length === 0"
|
||||||
class="align-items-center d-flex h-100 justify-content-center w-100"
|
class="align-items-center d-flex h-100 justify-content-center w-100"
|
||||||
>
|
>
|
||||||
<div class="d-flex justify-content-center">
|
<div class="d-flex justify-content-center">
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { primaryColorRgb } from '@ghostfolio/common/config';
|
import { primaryColorRgb } from '@ghostfolio/common/config';
|
||||||
|
import { parseDate } from '@ghostfolio/common/helper';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
@ -19,7 +20,7 @@ import {
|
|||||||
PointElement,
|
PointElement,
|
||||||
TimeScale
|
TimeScale
|
||||||
} from 'chart.js';
|
} from 'chart.js';
|
||||||
import { addMonths, isAfter, parseISO, subMonths } from 'date-fns';
|
import { addDays, isAfter, parseISO, subDays } from 'date-fns';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-investment-chart',
|
selector: 'gf-investment-chart',
|
||||||
@ -27,8 +28,10 @@ import { addMonths, isAfter, parseISO, subMonths } from 'date-fns';
|
|||||||
templateUrl: './investment-chart.component.html',
|
templateUrl: './investment-chart.component.html',
|
||||||
styleUrls: ['./investment-chart.component.scss']
|
styleUrls: ['./investment-chart.component.scss']
|
||||||
})
|
})
|
||||||
export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
||||||
|
@Input() daysInMarket: number;
|
||||||
@Input() investments: InvestmentItem[];
|
@Input() investments: InvestmentItem[];
|
||||||
|
@Input() isInPercent = false;
|
||||||
|
|
||||||
@ViewChild('chartCanvas') chartCanvas;
|
@ViewChild('chartCanvas') chartCanvas;
|
||||||
|
|
||||||
@ -45,8 +48,6 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
if (this.investments) {
|
if (this.investments) {
|
||||||
this.initialize();
|
this.initialize();
|
||||||
@ -61,19 +62,25 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
if (this.investments?.length > 0) {
|
if (this.investments?.length > 0) {
|
||||||
// Extend chart by three months (before)
|
// Extend chart by 5% of days in market (before)
|
||||||
const firstItem = this.investments[0];
|
const firstItem = this.investments[0];
|
||||||
this.investments.unshift({
|
this.investments.unshift({
|
||||||
...firstItem,
|
...firstItem,
|
||||||
date: subMonths(parseISO(firstItem.date), 3).toISOString(),
|
date: subDays(
|
||||||
|
parseISO(firstItem.date),
|
||||||
|
this.daysInMarket * 0.05 || 90
|
||||||
|
).toISOString(),
|
||||||
investment: 0
|
investment: 0
|
||||||
});
|
});
|
||||||
|
|
||||||
// Extend chart by three months (after)
|
// Extend chart by 5% of days in market (after)
|
||||||
const lastItem = this.investments[this.investments.length - 1];
|
const lastItem = this.investments[this.investments.length - 1];
|
||||||
this.investments.push({
|
this.investments.push({
|
||||||
...lastItem,
|
...lastItem,
|
||||||
date: addMonths(new Date(), 3).toISOString()
|
date: addDays(
|
||||||
|
parseDate(lastItem.date),
|
||||||
|
this.daysInMarket * 0.05 || 90
|
||||||
|
).toISOString()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,12 +143,26 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
display: false,
|
display: !this.isInPercent,
|
||||||
grid: {
|
grid: {
|
||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
display: false
|
display: true,
|
||||||
|
callback: (tickValue, index, ticks) => {
|
||||||
|
if (index === 0 || index === ticks.length - 1) {
|
||||||
|
// Only print last and first legend entry
|
||||||
|
if (typeof tickValue === 'number') {
|
||||||
|
return tickValue.toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tickValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
mirror: true,
|
||||||
|
z: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ import { InvestmentChartComponent } from './investment-chart.component';
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [InvestmentChartComponent],
|
declarations: [InvestmentChartComponent],
|
||||||
exports: [InvestmentChartComponent],
|
exports: [InvestmentChartComponent],
|
||||||
imports: [CommonModule, NgxSkeletonLoaderModule],
|
imports: [CommonModule, NgxSkeletonLoaderModule]
|
||||||
providers: []
|
|
||||||
})
|
})
|
||||||
export class GfInvestmentChartModule {}
|
export class GfInvestmentChartModule {}
|
||||||
|
@ -3,12 +3,15 @@
|
|||||||
<div
|
<div
|
||||||
class="flex-grow-1 status text-muted text-right"
|
class="flex-grow-1 status text-muted text-right"
|
||||||
[title]="
|
[title]="
|
||||||
hasError
|
hasError && !isLoading
|
||||||
? 'Sorry! Our data provider partner is experiencing the hiccups.'
|
? 'Sorry! Our data provider partner is experiencing the hiccups.'
|
||||||
: ''
|
: ''
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ion-icon *ngIf="hasError" name="alert-circle-outline"></ion-icon>
|
<ion-icon
|
||||||
|
*ngIf="hasError && !isLoading"
|
||||||
|
name="alert-circle-outline"
|
||||||
|
></ion-icon>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="isLoading" class="align-items-center d-flex">
|
<div *ngIf="isLoading" class="align-items-center d-flex">
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
|
@ -169,4 +169,18 @@
|
|||||||
></gf-value>
|
></gf-value>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col"><hr /></div>
|
||||||
|
</div>
|
||||||
|
<div class="row px-3 py-1">
|
||||||
|
<div class="d-flex flex-grow-1" i18n>Dividend</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
[currency]="baseCurrency"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : summary?.dividend"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,11 +3,13 @@ import {
|
|||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
Component,
|
Component,
|
||||||
Inject,
|
Inject,
|
||||||
OnDestroy
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT } from '@ghostfolio/common/helper';
|
||||||
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface';
|
||||||
import { AssetSubClass } from '@prisma/client';
|
import { AssetSubClass } from '@prisma/client';
|
||||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||||
@ -23,7 +25,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces';
|
|||||||
templateUrl: 'position-detail-dialog.html',
|
templateUrl: 'position-detail-dialog.html',
|
||||||
styleUrls: ['./position-detail-dialog.component.scss']
|
styleUrls: ['./position-detail-dialog.component.scss']
|
||||||
})
|
})
|
||||||
export class PositionDetailDialog implements OnDestroy {
|
export class PositionDetailDialog implements OnDestroy, OnInit {
|
||||||
public assetSubClass: AssetSubClass;
|
public assetSubClass: AssetSubClass;
|
||||||
public averagePrice: number;
|
public averagePrice: number;
|
||||||
public benchmarkDataItems: LineChartItem[];
|
public benchmarkDataItems: LineChartItem[];
|
||||||
@ -39,6 +41,7 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
public name: string;
|
public name: string;
|
||||||
public netPerformance: number;
|
public netPerformance: number;
|
||||||
public netPerformancePercent: number;
|
public netPerformancePercent: number;
|
||||||
|
public orders: OrderWithAccount[];
|
||||||
public quantity: number;
|
public quantity: number;
|
||||||
public quantityPrecision = 2;
|
public quantityPrecision = 2;
|
||||||
public symbol: string;
|
public symbol: string;
|
||||||
@ -52,9 +55,11 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
public dialogRef: MatDialogRef<PositionDetailDialog>,
|
public dialogRef: MatDialogRef<PositionDetailDialog>,
|
||||||
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams
|
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams
|
||||||
) {
|
) {}
|
||||||
|
|
||||||
|
public ngOnInit(): void {
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPositionDetail(data.symbol)
|
.fetchPositionDetail(this.data.symbol)
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
({
|
({
|
||||||
@ -72,6 +77,7 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
name,
|
name,
|
||||||
netPerformance,
|
netPerformance,
|
||||||
netPerformancePercent,
|
netPerformancePercent,
|
||||||
|
orders,
|
||||||
quantity,
|
quantity,
|
||||||
symbol,
|
symbol,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
@ -104,6 +110,7 @@ export class PositionDetailDialog implements OnDestroy {
|
|||||||
this.name = name;
|
this.name = name;
|
||||||
this.netPerformance = netPerformance;
|
this.netPerformance = netPerformance;
|
||||||
this.netPerformancePercent = netPerformancePercent;
|
this.netPerformancePercent = netPerformancePercent;
|
||||||
|
this.orders = orders;
|
||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
this.symbol = symbol;
|
this.symbol = symbol;
|
||||||
this.transactionCount = transactionCount;
|
this.transactionCount = transactionCount;
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<div class="col-12 d-flex justify-content-center mb-3">
|
<div class="col-12 d-flex justify-content-center mb-3">
|
||||||
<gf-value
|
<gf-value
|
||||||
size="large"
|
size="large"
|
||||||
[currency]="data.baseCurrency"
|
[currency]="currency"
|
||||||
[locale]="data.locale"
|
[locale]="data.locale"
|
||||||
[value]="value"
|
[value]="value"
|
||||||
></gf-value>
|
></gf-value>
|
||||||
@ -124,6 +124,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<gf-activities-table
|
||||||
|
*ngIf="orders?.length > 0"
|
||||||
|
[activities]="orders"
|
||||||
|
[baseCurrency]="data.baseCurrency"
|
||||||
|
[deviceType]="data.deviceType"
|
||||||
|
[hasPermissionToCreateActivity]="false"
|
||||||
|
[hasPermissionToFilter]="false"
|
||||||
|
[hasPermissionToImportActivities]="false"
|
||||||
|
[hasPermissionToOpenDetails]="false"
|
||||||
|
[locale]="data.locale"
|
||||||
|
[showActions]="false"
|
||||||
|
[showSymbolColumn]="false"
|
||||||
|
></gf-activities-table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gf-dialog-footer
|
<gf-dialog-footer
|
||||||
|
@ -2,12 +2,13 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
|
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||||
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfDialogFooterModule } from '../../dialog-footer/dialog-footer.module';
|
|
||||||
import { GfDialogHeaderModule } from '../../dialog-header/dialog-header.module';
|
|
||||||
import { PositionDetailDialog } from './position-detail-dialog.component';
|
import { PositionDetailDialog } from './position-detail-dialog.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@ -15,6 +16,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
|||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfActivitiesTableModule,
|
||||||
GfDialogFooterModule,
|
GfDialogFooterModule,
|
||||||
GfDialogHeaderModule,
|
GfDialogHeaderModule,
|
||||||
GfLineChartModule,
|
GfLineChartModule,
|
||||||
|
@ -5,14 +5,9 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit
|
OnInit
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||||
import { Position } from '@ghostfolio/common/interfaces';
|
import { Position } from '@ghostfolio/common/interfaces';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { PositionDetailDialog } from './position-detail-dialog/position-detail-dialog.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-position',
|
selector: 'gf-position',
|
||||||
@ -32,23 +27,7 @@ export class PositionComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor() {}
|
||||||
private dialog: MatDialog,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router
|
|
||||||
) {
|
|
||||||
route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (
|
|
||||||
params['positionDetailDialog'] &&
|
|
||||||
params['symbol'] &&
|
|
||||||
params['symbol'] === this.position?.symbol
|
|
||||||
) {
|
|
||||||
this.openDialog();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
@ -56,25 +35,4 @@ export class PositionComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
private openDialog(): void {
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: {
|
|
||||||
baseCurrency: this.baseCurrency,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
locale: this.locale,
|
|
||||||
symbol: this.position?.symbol
|
|
||||||
},
|
|
||||||
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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -123,7 +123,12 @@
|
|||||||
}"
|
}"
|
||||||
></ngx-skeleton-loader>
|
></ngx-skeleton-loader>
|
||||||
|
|
||||||
<div *ngIf="dataSource.data.length === 0 && !isLoading" class="p-3 text-center">
|
<div
|
||||||
|
*ngIf="
|
||||||
|
dataSource.data.length === 0 && hasPermissionToCreateOrder && !isLoading
|
||||||
|
"
|
||||||
|
class="p-3 text-center"
|
||||||
|
>
|
||||||
<gf-no-transactions-info-indicator
|
<gf-no-transactions-info-indicator
|
||||||
[hasBorder]="false"
|
[hasBorder]="false"
|
||||||
></gf-no-transactions-info-indicator>
|
></gf-no-transactions-info-indicator>
|
||||||
|
@ -9,17 +9,13 @@ import {
|
|||||||
Output,
|
Output,
|
||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { MatPaginator } from '@angular/material/paginator';
|
import { MatPaginator } from '@angular/material/paginator';
|
||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
import { PortfolioPosition } from '@ghostfolio/common/interfaces';
|
||||||
import { AssetClass, Order as OrderModel } from '@prisma/client';
|
import { AssetClass, Order as OrderModel } from '@prisma/client';
|
||||||
import { Subject, Subscription } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-positions-table',
|
selector: 'gf-positions-table',
|
||||||
@ -30,6 +26,7 @@ import { PositionDetailDialog } from '../position/position-detail-dialog/positio
|
|||||||
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
|
@Input() hasPermissionToCreateOrder: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() positions: PortfolioPosition[];
|
@Input() positions: PortfolioPosition[];
|
||||||
|
|
||||||
@ -49,21 +46,7 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(private router: Router) {}
|
||||||
private dialog: MatDialog,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router
|
|
||||||
) {
|
|
||||||
this.routeQueryParams = route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (params['positionDetailDialog'] && params['symbol']) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnInit() {}
|
public ngOnInit() {}
|
||||||
|
|
||||||
@ -106,27 +89,6 @@ export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public openPositionDialog({ symbol }: { symbol: string }): void {
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: {
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.baseCurrency,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
locale: this.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 });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
|
@ -7,12 +7,12 @@ import { MatPaginatorModule } from '@angular/material/paginator';
|
|||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module';
|
|
||||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
||||||
import { PositionsTableComponent } from './positions-table.component';
|
import { PositionsTableComponent } from './positions-table.component';
|
||||||
|
|
||||||
|
@ -23,7 +23,10 @@
|
|||||||
[range]="range"
|
[range]="range"
|
||||||
></gf-position>
|
></gf-position>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div *ngIf="!hasPositions" class="p-3 text-center">
|
<div
|
||||||
|
*ngIf="hasPermissionToCreateOrder && !hasPositions"
|
||||||
|
class="p-3 text-center"
|
||||||
|
>
|
||||||
<gf-no-transactions-info-indicator
|
<gf-no-transactions-info-indicator
|
||||||
[hasBorder]="false"
|
[hasBorder]="false"
|
||||||
></gf-no-transactions-info-indicator>
|
></gf-no-transactions-info-indicator>
|
||||||
|
@ -17,6 +17,7 @@ import { Position } from '@ghostfolio/common/interfaces';
|
|||||||
export class PositionsComponent implements OnChanges, OnInit {
|
export class PositionsComponent implements OnChanges, OnInit {
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
|
@Input() hasPermissionToCreateOrder: boolean;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() positions: Position[];
|
@Input() positions: Position[];
|
||||||
@Input() range: string;
|
@Input() range: string;
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
<div class="container p-0">
|
<div class="container p-0">
|
||||||
<div class="row no-gutters">
|
<div class="row no-gutters">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<mat-card *ngIf="rules === null" class="my-2 text-center">
|
<mat-card
|
||||||
|
*ngIf="hasPermissionToCreateOrder && rules === null"
|
||||||
|
class="my-2 text-center"
|
||||||
|
>
|
||||||
<gf-no-transactions-info-indicator
|
<gf-no-transactions-info-indicator
|
||||||
[hasBorder]="false"
|
[hasBorder]="false"
|
||||||
></gf-no-transactions-info-indicator>
|
></gf-no-transactions-info-indicator>
|
||||||
|
@ -8,6 +8,7 @@ import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
|||||||
styleUrls: ['./rules.component.scss']
|
styleUrls: ['./rules.component.scss']
|
||||||
})
|
})
|
||||||
export class RulesComponent {
|
export class RulesComponent {
|
||||||
|
@Input() hasPermissionToCreateOrder: boolean;
|
||||||
@Input() rules: PortfolioReportRule;
|
@Input() rules: PortfolioReportRule;
|
||||||
|
|
||||||
public constructor() {}
|
public constructor() {}
|
||||||
|
@ -8,8 +8,7 @@ import {
|
|||||||
Output
|
Output
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { ToggleOption } from '@ghostfolio/common/types';
|
||||||
import { ToggleOption } from './interfaces/toggle-option.type';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-toggle',
|
selector: 'gf-toggle',
|
||||||
|
@ -16,6 +16,8 @@ import { UserService } from '../services/user/user.service';
|
|||||||
export class AuthGuard implements CanActivate {
|
export class AuthGuard implements CanActivate {
|
||||||
private static PUBLIC_PAGE_ROUTES = [
|
private static PUBLIC_PAGE_ROUTES = [
|
||||||
'/about',
|
'/about',
|
||||||
|
'/about/changelog',
|
||||||
|
'/blog',
|
||||||
'/de/blog',
|
'/de/blog',
|
||||||
'/en/blog',
|
'/en/blog',
|
||||||
'/p',
|
'/p',
|
||||||
|
@ -35,8 +35,8 @@
|
|||||||
new feature, please join the Ghostfolio
|
new feature, please join the Ghostfolio
|
||||||
<a
|
<a
|
||||||
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
title="Join the Ghostfolio Slack channel"
|
title="Join the Ghostfolio Slack community"
|
||||||
>Slack channel</a
|
>Slack community</a
|
||||||
>, tweet to
|
>, tweet to
|
||||||
<a
|
<a
|
||||||
href="https://twitter.com/ghostfolio_"
|
href="https://twitter.com/ghostfolio_"
|
||||||
@ -108,12 +108,7 @@
|
|||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<h3
|
<h3 class="mb-0">{{ statistics?.activeUsers1d || '-' }}</h3>
|
||||||
class="mb-0"
|
|
||||||
[hidden]="statistics?.activeUsers1d === undefined"
|
|
||||||
>
|
|
||||||
{{ statistics?.activeUsers1d || '-' }}
|
|
||||||
</h3>
|
|
||||||
<div class="h6 mb-0">
|
<div class="h6 mb-0">
|
||||||
<span i18n>Active Users</span> <small class="text-muted"
|
<span i18n>Active Users</span> <small class="text-muted"
|
||||||
>(Last 24 hours)</small
|
>(Last 24 hours)</small
|
||||||
@ -121,35 +116,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<h3
|
<h3 class="mb-0">{{ statistics?.newUsers30d ?? '-' }}</h3>
|
||||||
class="mb-0"
|
|
||||||
[hidden]="statistics?.activeUsers7d === undefined"
|
|
||||||
>
|
|
||||||
{{ statistics?.activeUsers7d ?? '-' }}
|
|
||||||
</h3>
|
|
||||||
<div class="h6 mb-0">
|
|
||||||
<span i18n>Active Users</span> <small class="text-muted"
|
|
||||||
>(Last 7 days)</small
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
|
||||||
<h3
|
|
||||||
class="mb-0"
|
|
||||||
[hidden]="statistics?.activeUsers30d === undefined"
|
|
||||||
>
|
|
||||||
{{ statistics?.activeUsers30d ?? '-' }}
|
|
||||||
</h3>
|
|
||||||
<div class="h6 mb-0">
|
|
||||||
<span i18n>Active Users</span> <small class="text-muted"
|
|
||||||
>(Last 30 days)</small
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
|
||||||
<h3 class="mb-0" [hidden]="statistics?.newUsers30d === undefined">
|
|
||||||
{{ statistics?.newUsers30d ?? '-' }}
|
|
||||||
</h3>
|
|
||||||
<div class="h6 mb-0">
|
<div class="h6 mb-0">
|
||||||
<span i18n>New Users</span> <small class="text-muted"
|
<span i18n>New Users</span> <small class="text-muted"
|
||||||
>(Last 30 days)</small
|
>(Last 30 days)</small
|
||||||
@ -157,21 +124,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<h3
|
<h3 class="mb-0">{{ statistics?.activeUsers30d ?? '-' }}</h3>
|
||||||
class="mb-0"
|
<div class="h6 mb-0">
|
||||||
[hidden]="statistics?.gitHubContributors === undefined"
|
<span i18n>Active Users</span> <small class="text-muted"
|
||||||
>
|
>(Last 30 days)</small
|
||||||
{{ statistics?.gitHubContributors ?? '-' }}
|
>
|
||||||
</h3>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<h3 class="mb-0">{{ statistics?.slackCommunityUsers ?? '-' }}</h3>
|
||||||
|
<div class="h6 mb-0" i18n>Users in Slack community</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
|
<h3 class="mb-0">{{ statistics?.gitHubContributors ?? '-' }}</h3>
|
||||||
<div class="h6 mb-0" i18n>Contributors on GitHub</div>
|
<div class="h6 mb-0" i18n>Contributors on GitHub</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xs-12 col-md-4 my-2">
|
<div class="col-xs-12 col-md-4 my-2">
|
||||||
<h3
|
<h3 class="mb-0">{{ statistics?.gitHubStargazers ?? '-' }}</h3>
|
||||||
class="mb-0"
|
|
||||||
[hidden]="statistics?.gitHubStargazers === undefined"
|
|
||||||
>
|
|
||||||
{{ statistics?.gitHubStargazers ?? '-' }}
|
|
||||||
</h3>
|
|
||||||
<div class="h6 mb-0" i18n>Stars on GitHub</div>
|
<div class="h6 mb-0" i18n>Stars on GitHub</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -180,73 +149,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngIf="hasPermissionForBlog" class="mb-5 row">
|
|
||||||
<div class="col">
|
|
||||||
<h3 class="mb-3 text-center" i18n>Blog</h3>
|
|
||||||
<mat-card class="blog-container">
|
|
||||||
<mat-card-content>
|
|
||||||
<div class="container p-0">
|
|
||||||
<div class="flex-nowrap mb-3 no-gutters row">
|
|
||||||
<a
|
|
||||||
class="d-flex w-100"
|
|
||||||
[routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
|
|
||||||
>
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="h6 m-0 text-truncate">Hello Ghostfolio</div>
|
|
||||||
<div class="d-flex text-muted">31.07.2021</div>
|
|
||||||
</div>
|
|
||||||
<div class="align-items-center d-flex">
|
|
||||||
<ion-icon
|
|
||||||
class="chevron text-muted"
|
|
||||||
name="chevron-forward-outline"
|
|
||||||
size="small"
|
|
||||||
></ion-icon>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="flex-nowrap no-gutters row">
|
|
||||||
<a
|
|
||||||
class="d-flex w-100"
|
|
||||||
[routerLink]="['/de', 'blog', '2021', '07', 'hallo-ghostfolio']"
|
|
||||||
>
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="h6 m-0 text-truncate">Hallo Ghostfolio</div>
|
|
||||||
<div class="d-flex text-muted">31.07.2021</div>
|
|
||||||
</div>
|
|
||||||
<div class="align-items-center d-flex">
|
|
||||||
<ion-icon
|
|
||||||
class="chevron text-muted"
|
|
||||||
name="chevron-forward-outline"
|
|
||||||
size="small"
|
|
||||||
></ion-icon>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-5 row">
|
|
||||||
<div class="col">
|
|
||||||
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
|
||||||
<mat-card class="changelog">
|
|
||||||
<mat-card-content>
|
|
||||||
<markdown [src]="'assets/CHANGELOG.md'"></markdown>
|
|
||||||
</mat-card-content>
|
|
||||||
</mat-card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div *ngIf="hasPermissionForBlog" class="col-md-6 col-xs-12 my-2">
|
||||||
<h3 class="mb-3 text-center" i18n>License</h3>
|
<a class="py-2 w-100" i18n mat-stroked-button [routerLink]="['/blog']"
|
||||||
<mat-card>
|
>Blog</a
|
||||||
<mat-card-content>
|
>
|
||||||
<markdown [src]="'assets/LICENSE'"></markdown>
|
</div>
|
||||||
</mat-card-content>
|
<div
|
||||||
</mat-card>
|
class="col-md-6 col-xs-12 my-2"
|
||||||
|
[ngClass]="{ 'offset-md-3': !hasPermissionForBlog }"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="py-2 w-100"
|
||||||
|
i18n
|
||||||
|
mat-stroked-button
|
||||||
|
[routerLink]="['/about', 'changelog']"
|
||||||
|
>Changelog & License</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,6 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { MarkdownModule } from 'ngx-markdown';
|
|
||||||
|
|
||||||
import { AboutPageRoutingModule } from './about-page-routing.module';
|
import { AboutPageRoutingModule } from './about-page-routing.module';
|
||||||
import { AboutPageComponent } from './about-page.component';
|
import { AboutPageComponent } from './about-page.component';
|
||||||
@ -13,7 +12,6 @@ import { AboutPageComponent } from './about-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
AboutPageRoutingModule,
|
AboutPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
MarkdownModule.forChild(),
|
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatCardModule
|
MatCardModule
|
||||||
],
|
],
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { ChangelogPageComponent } from './changelog-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: ChangelogPageComponent, canActivate: [AuthGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class ChangelogPageRoutingModule {}
|
@ -0,0 +1,22 @@
|
|||||||
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'mb-5' },
|
||||||
|
selector: 'gf-changelog-page',
|
||||||
|
styleUrls: ['./changelog-page.scss'],
|
||||||
|
templateUrl: './changelog-page.html'
|
||||||
|
})
|
||||||
|
export class ChangelogPageComponent implements OnDestroy {
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="mb-5 row">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="mb-3 text-center" i18n>Changelog</h3>
|
||||||
|
<mat-card class="changelog">
|
||||||
|
<mat-card-content>
|
||||||
|
<markdown [src]="'assets/CHANGELOG.md'"></markdown>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="mb-3 text-center" i18n>License</h3>
|
||||||
|
<mat-card>
|
||||||
|
<mat-card-content>
|
||||||
|
<markdown [src]="'assets/LICENSE'"></markdown>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,19 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
import { MarkdownModule } from 'ngx-markdown';
|
||||||
|
|
||||||
|
import { ChangelogPageRoutingModule } from './changelog-page-routing.module';
|
||||||
|
import { ChangelogPageComponent } from './changelog-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [ChangelogPageComponent],
|
||||||
|
imports: [
|
||||||
|
ChangelogPageRoutingModule,
|
||||||
|
CommonModule,
|
||||||
|
MarkdownModule.forChild(),
|
||||||
|
MatCardModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class ChangelogPageModule {}
|
@ -0,0 +1,44 @@
|
|||||||
|
:host {
|
||||||
|
color: rgb(var(--dark-primary-text));
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.mat-card {
|
||||||
|
&.changelog {
|
||||||
|
a {
|
||||||
|
color: rgba(var(--palette-primary-500), 1);
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: rgba(var(--palette-primary-300), 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.changelog {
|
||||||
|
::ng-deep {
|
||||||
|
markdown {
|
||||||
|
h1,
|
||||||
|
p {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
color: rgb(var(--light-primary-text));
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<div class="blog container">
|
<div class="blog container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col-md-8 offset-md-2">
|
||||||
<article>
|
<article>
|
||||||
<div class="mb-4 text-center">
|
<div class="mb-4 text-center">
|
||||||
<h1 class="mb-1" i18n>Hallo Ghostfolio 👋</h1>
|
<h1 class="mb-1" i18n>Hallo Ghostfolio 👋</h1>
|
||||||
@ -141,58 +141,59 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
<li class="h5">
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Aktie</span>
|
||||||
>Aktie</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Altersvorsorge</span>
|
||||||
>Altersvorsorge</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Anlage</span>
|
||||||
>Anlage</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2">App</span>
|
<span class="badge badge-light">App</span>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
</li>
|
||||||
>Cryptocurrency</span
|
<li class="list-inline-item">
|
||||||
>
|
<span class="badge badge-light">Cryptocurrency</span>
|
||||||
<span class="badge badge-light font-weight-normal mr-2">ETF</span>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Feedback</span
|
<span class="badge badge-light">Feedback</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Fintech</span
|
<span class="badge badge-light">Fintech</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Ghostfolio</span
|
<span class="badge badge-light">Ghostfolio</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Investition</span
|
<span class="badge badge-light">Investition</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Open Source</span
|
<span class="badge badge-light">Open Source</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2">OSS</span>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">OSS</span>
|
||||||
>Portfolio</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Portfolio</span>
|
||||||
>Software</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Software</span>
|
||||||
>Strategie</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Strategie</span>
|
||||||
>Trading</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Trading</span>
|
||||||
>TypeScript</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">TypeScript</span>
|
||||||
>Vermögen</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Vermögen</span>
|
||||||
>Wealth Management</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Wealth Management</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
@ -7,9 +7,7 @@ import { HalloGhostfolioPageComponent } from './hallo-ghostfolio-page.component'
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [HalloGhostfolioPageComponent],
|
declarations: [HalloGhostfolioPageComponent],
|
||||||
exports: [],
|
|
||||||
imports: [CommonModule, HalloGhostfolioPageRoutingModule, RouterModule],
|
imports: [CommonModule, HalloGhostfolioPageRoutingModule, RouterModule],
|
||||||
providers: [],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class HalloGhostfolioPageModule {}
|
export class HalloGhostfolioPageModule {}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div class="blog container">
|
<div class="blog container">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col">
|
<div class="col-md-8 offset-md-2">
|
||||||
<article>
|
<article>
|
||||||
<div class="mb-4 text-center">
|
<div class="mb-4 text-center">
|
||||||
<h1 class="mb-1" i18n>Hello Ghostfolio 👋</h1>
|
<h1 class="mb-1" i18n>Hello Ghostfolio 👋</h1>
|
||||||
@ -136,42 +136,44 @@
|
|||||||
</section>
|
</section>
|
||||||
<section class="mb-4">
|
<section class="mb-4">
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
<li class="h5">
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<span class="badge badge-light">Cryptocurrency</span>
|
||||||
>Cryptocurrency</span
|
</li>
|
||||||
>
|
<li class="list-inline-item">
|
||||||
<span class="badge badge-light font-weight-normal mr-2">ETF</span>
|
<span class="badge badge-light">ETF</span>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
</li>
|
||||||
>Fintech</span
|
<li class="list-inline-item">
|
||||||
>
|
<span class="badge badge-light">Fintech</span>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
</li>
|
||||||
>Ghostfolio</span
|
<li class="list-inline-item">
|
||||||
>
|
<span class="badge badge-light">Ghostfolio</span>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
</li>
|
||||||
>Investment</span
|
<li class="list-inline-item">
|
||||||
>
|
<span class="badge badge-light">Investment</span>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
</li>
|
||||||
>Open Source</span
|
<li class="list-inline-item">
|
||||||
>
|
<span class="badge badge-light">Open Source</span>
|
||||||
<span class="badge badge-light font-weight-normal mr-2">OSS</span>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Portfolio</span
|
<span class="badge badge-light">OSS</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Software</span
|
<span class="badge badge-light">Portfolio</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Stock</span
|
<span class="badge badge-light">Software</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Strategy</span
|
<span class="badge badge-light">Stock</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Wealth</span
|
<span class="badge badge-light">Strategy</span>
|
||||||
>
|
</li>
|
||||||
<span class="badge badge-light font-weight-normal mr-2"
|
<li class="list-inline-item">
|
||||||
>Wealth Management</span
|
<span class="badge badge-light">Wealth</span>
|
||||||
>
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Wealth Management</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
@ -7,9 +7,7 @@ import { HelloGhostfolioPageComponent } from './hello-ghostfolio-page.component'
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [HelloGhostfolioPageComponent],
|
declarations: [HelloGhostfolioPageComponent],
|
||||||
exports: [],
|
|
||||||
imports: [CommonModule, HelloGhostfolioPageRoutingModule, RouterModule],
|
imports: [CommonModule, HelloGhostfolioPageRoutingModule, RouterModule],
|
||||||
providers: [],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class HelloGhostfolioPageModule {}
|
export class HelloGhostfolioPageModule {}
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-source-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: FirstMonthsInOpenSourcePageComponent,
|
||||||
|
canActivate: [AuthGuard]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class FirstMonthsInOpenSourceRoutingModule {}
|
@ -0,0 +1,9 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'mb-5' },
|
||||||
|
selector: 'gf-first-months-in-open-source-page',
|
||||||
|
styleUrls: ['./first-months-in-open-source-page.scss'],
|
||||||
|
templateUrl: './first-months-in-open-source-page.html'
|
||||||
|
})
|
||||||
|
export class FirstMonthsInOpenSourcePageComponent {}
|
@ -0,0 +1,185 @@
|
|||||||
|
<div class="blog container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8 offset-md-2">
|
||||||
|
<article>
|
||||||
|
<div class="mb-4 text-center">
|
||||||
|
<h1 class="mb-1" i18n>
|
||||||
|
👻 Ghostfolio –
|
||||||
|
<span class="text-nowrap">First months in Open Source</span>
|
||||||
|
</h1>
|
||||||
|
<div class="text-muted"><small>05.01.2022</small></div>
|
||||||
|
</div>
|
||||||
|
<section class="mb-4">
|
||||||
|
<p>
|
||||||
|
In this article I would like to recap the first months running the
|
||||||
|
open source project <a href="https://ghostfol.io">Ghostfolio</a>, a
|
||||||
|
web-based personal finance management software.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<h2 class="h4">From 1* to 100 stars on GitHub</h2>
|
||||||
|
<p>
|
||||||
|
When I decided to
|
||||||
|
<a [routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
|
||||||
|
>publish</a
|
||||||
|
>
|
||||||
|
the project as
|
||||||
|
<a href="https://github.com/ghostfolio/ghostfolio"
|
||||||
|
>open source software</a
|
||||||
|
>
|
||||||
|
(OSS), I did not know what exactly to expect. In the worst case,
|
||||||
|
nobody would care. And in the best case, the repository would be
|
||||||
|
overrun with contributions. The truth is probably somewhere in
|
||||||
|
between.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In the beginning, it felt quite weird to develop in public where
|
||||||
|
anyone can observe the progress. Stupid mistakes remain visible
|
||||||
|
forever. But this feeling, fortunately, quickly settled. I believe
|
||||||
|
the benefits like all the learning clearly outweigh the drawbacks
|
||||||
|
when you just do it.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
At the end of 2021, Ghostfolio reached an important milestone:
|
||||||
|
<a href="https://twitter.com/ghostfolio_/status/1470075774640218121"
|
||||||
|
>100 stars</a
|
||||||
|
>
|
||||||
|
on GitHub. This is really exciting with almost no marketing. I am a
|
||||||
|
technical founder, so I prefer writing code over anything else. But
|
||||||
|
there is so much more to make this project happen: writing
|
||||||
|
documentation, maintaining bug reports and feature requests,
|
||||||
|
supporting users and managing the community, keeping the SaaS
|
||||||
|
running, etc.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Reaching 100 stars will not only attract very early adopters, but
|
||||||
|
also the early adopters. At the same time, the demands and
|
||||||
|
expectations are also increasing.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<h2 class="h4">What is new?</h2>
|
||||||
|
<p>
|
||||||
|
During the last months, Ghostfolio has transformed from a one man
|
||||||
|
project into a prospering wealth management application with 9
|
||||||
|
contributors and counting. User feedback has directly shaped the
|
||||||
|
direction of the product development.
|
||||||
|
</p>
|
||||||
|
<p>These are some selected key features:</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Simplified self-hosting with an
|
||||||
|
<a href="https://hub.docker.com/r/ghostfolio/ghostfolio"
|
||||||
|
>official Ghostfolio docker image</a
|
||||||
|
>
|
||||||
|
on Docker Hub
|
||||||
|
</li>
|
||||||
|
<li>Improved import for activities (transactions and dividend)</li>
|
||||||
|
<li>Enriched market data for ETFs (region and industries)</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<h2 class="h4">What is coming?</h2>
|
||||||
|
<p>Here is a brief overview of what I am planning in 2022.</p>
|
||||||
|
<p>
|
||||||
|
The goal remains to offer a simple and solid software to manage
|
||||||
|
personal finances. Thus, the main focus is on the core
|
||||||
|
functionality.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
My personal goal is to reach break-even with the Saas offering (<a
|
||||||
|
[routerLink]="['/pricing']"
|
||||||
|
>Ghostfolio Premium</a
|
||||||
|
>) and regularly report about the progress and my learnings on this
|
||||||
|
exciting journey.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I have already started to build a
|
||||||
|
<a
|
||||||
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
|
||||||
|
>community</a
|
||||||
|
>
|
||||||
|
of users. In the future, I would like to involve more contributors
|
||||||
|
to further extend the functionality of Ghostfolio (e.g. with new
|
||||||
|
reports). Get in touch with me by email at
|
||||||
|
<a href="mailto:hi@ghostfol.io">hi@ghostfol.io</a> or on Twitter
|
||||||
|
<a href="https://twitter.com/ghostfolio_">@ghostfolio_</a> if you
|
||||||
|
are interested, I’m happy to discuss ideas.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
I would like to say thank you for all your feedback and support
|
||||||
|
during the last months.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Have a great start into the new year and happy investing<br />
|
||||||
|
Thomas from Ghostfolio
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<p>* Pro Tip: add the first star to your own open source project</p>
|
||||||
|
</section>
|
||||||
|
<section class="mb-4">
|
||||||
|
<ul class="list-inline">
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">BuildInPublic</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Community</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Cryptocurrency</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Docker</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">ETF</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Fintech</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Ghostfolio</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Image</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Investment</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Open Source</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">OSS</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Portfolio</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Progress</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">SaaS</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Software</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Stock</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Strategy</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Wealth</span>
|
||||||
|
</li>
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<span class="badge badge-light">Wealth Management</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,13 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { FirstMonthsInOpenSourceRoutingModule } from './first-months-in-open-source-page-routing.module';
|
||||||
|
import { FirstMonthsInOpenSourcePageComponent } from './first-months-in-open-source-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [FirstMonthsInOpenSourcePageComponent],
|
||||||
|
imports: [CommonModule, FirstMonthsInOpenSourceRoutingModule, RouterModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class FirstMonthsInOpenSourcePageModule {}
|
@ -0,0 +1,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
15
apps/client/src/app/pages/blog/blog-page-routing.module.ts
Normal file
15
apps/client/src/app/pages/blog/blog-page-routing.module.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
|
||||||
|
|
||||||
|
import { BlogPageComponent } from './blog-page.component';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{ path: '', component: BlogPageComponent, canActivate: [AuthGuard] }
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class BlogPageRoutingModule {}
|
22
apps/client/src/app/pages/blog/blog-page.component.ts
Normal file
22
apps/client/src/app/pages/blog/blog-page.component.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Component, OnDestroy } from '@angular/core';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
host: { class: 'mb-5' },
|
||||||
|
selector: 'gf-blog-page',
|
||||||
|
styleUrls: ['./blog-page.scss'],
|
||||||
|
templateUrl: './blog-page.html'
|
||||||
|
})
|
||||||
|
export class BlogPageComponent implements OnDestroy {
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
}
|
69
apps/client/src/app/pages/blog/blog-page.html
Normal file
69
apps/client/src/app/pages/blog/blog-page.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="mb-5 row">
|
||||||
|
<div class="col">
|
||||||
|
<h3 class="mb-3 text-center" i18n>Blog</h3>
|
||||||
|
<mat-card class="blog-container">
|
||||||
|
<mat-card-content>
|
||||||
|
<div class="container p-0">
|
||||||
|
<div class="flex-nowrap mb-3 no-gutters row">
|
||||||
|
<a
|
||||||
|
class="d-flex w-100"
|
||||||
|
[routerLink]="['/en', 'blog', '2022', '01', 'ghostfolio-first-months-in-open-source']"
|
||||||
|
>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="h6 m-0 text-truncate">
|
||||||
|
First months in Open Source
|
||||||
|
</div>
|
||||||
|
<div class="d-flex text-muted">05.01.2021</div>
|
||||||
|
</div>
|
||||||
|
<div class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="chevron text-muted"
|
||||||
|
name="chevron-forward-outline"
|
||||||
|
size="small"
|
||||||
|
></ion-icon>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex-nowrap mb-3 no-gutters row">
|
||||||
|
<a
|
||||||
|
class="d-flex w-100"
|
||||||
|
[routerLink]="['/en', 'blog', '2021', '07', 'hello-ghostfolio']"
|
||||||
|
>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="h6 m-0 text-truncate">Hello Ghostfolio</div>
|
||||||
|
<div class="d-flex text-muted">31.07.2021</div>
|
||||||
|
</div>
|
||||||
|
<div class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="chevron text-muted"
|
||||||
|
name="chevron-forward-outline"
|
||||||
|
size="small"
|
||||||
|
></ion-icon>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="flex-nowrap no-gutters row">
|
||||||
|
<a
|
||||||
|
class="d-flex w-100"
|
||||||
|
[routerLink]="['/de', 'blog', '2021', '07', 'hallo-ghostfolio']"
|
||||||
|
>
|
||||||
|
<div class="flex-grow-1">
|
||||||
|
<div class="h6 m-0 text-truncate">Hallo Ghostfolio</div>
|
||||||
|
<div class="d-flex text-muted">31.07.2021</div>
|
||||||
|
</div>
|
||||||
|
<div class="align-items-center d-flex">
|
||||||
|
<ion-icon
|
||||||
|
class="chevron text-muted"
|
||||||
|
name="chevron-forward-outline"
|
||||||
|
size="small"
|
||||||
|
></ion-icon>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
13
apps/client/src/app/pages/blog/blog-page.module.ts
Normal file
13
apps/client/src/app/pages/blog/blog-page.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { MatCardModule } from '@angular/material/card';
|
||||||
|
|
||||||
|
import { BlogPageRoutingModule } from './blog-page-routing.module';
|
||||||
|
import { BlogPageComponent } from './blog-page.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [BlogPageComponent],
|
||||||
|
imports: [BlogPageRoutingModule, CommonModule, MatCardModule],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class BlogPageModule {}
|
8
apps/client/src/app/pages/blog/blog-page.scss
Normal file
8
apps/client/src/app/pages/blog/blog-page.scss
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
:host {
|
||||||
|
color: rgb(var(--dark-primary-text));
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
:host-context(.is-dark-theme) {
|
||||||
|
color: rgb(var(--light-primary-text));
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
@ -10,9 +12,11 @@ import {
|
|||||||
PortfolioPosition,
|
PortfolioPosition,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
|
import { ToggleOption } from '@ghostfolio/common/types';
|
||||||
import { AssetClass } from '@prisma/client';
|
import { AssetClass } from '@prisma/client';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -33,6 +37,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
};
|
};
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
|
public hasPermissionToCreateOrder: boolean;
|
||||||
public period = 'current';
|
public period = 'current';
|
||||||
public periodOptions: ToggleOption[] = [
|
public periodOptions: ToggleOption[] = [
|
||||||
{ label: 'Initial', value: 'original' },
|
{ label: 'Initial', value: 'original' },
|
||||||
@ -51,6 +56,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
public positionsArray: PortfolioPosition[];
|
public positionsArray: PortfolioPosition[];
|
||||||
|
public routeQueryParams: Subscription;
|
||||||
public sectors: {
|
public sectors: {
|
||||||
[name: string]: { name: string; value: number };
|
[name: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
@ -69,9 +75,22 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private deviceService: DeviceDetectorService,
|
private deviceService: DeviceDetectorService,
|
||||||
|
private dialog: MatDialog,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private impersonationStorageService: ImpersonationStorageService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {
|
||||||
|
this.routeQueryParams = route.queryParams
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((params) => {
|
||||||
|
if (params['positionDetailDialog'] && params['symbol']) {
|
||||||
|
this.openPositionDialog({
|
||||||
|
symbol: params['symbol']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initializes the controller
|
* Initializes the controller
|
||||||
@ -103,6 +122,11 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
if (state?.user) {
|
if (state?.user) {
|
||||||
this.user = state.user;
|
this.user = state.user;
|
||||||
|
|
||||||
|
this.hasPermissionToCreateOrder = hasPermission(
|
||||||
|
this.user.permissions,
|
||||||
|
permissions.createOrder
|
||||||
|
);
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -266,4 +290,32 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openPositionDialog({ symbol }: { symbol: string }) {
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: {
|
||||||
|
symbol,
|
||||||
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -197,6 +197,7 @@
|
|||||||
<gf-positions-table
|
<gf-positions-table
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[positions]="positionsArray"
|
[positions]="positionsArray"
|
||||||
></gf-positions-table>
|
></gf-positions-table>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ToggleOption } from '@ghostfolio/client/components/toggle/interfaces/toggle-option.type';
|
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { PortfolioPosition, User } from '@ghostfolio/common/interfaces';
|
import { Position, User } from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
|
import { differenceInDays } from 'date-fns';
|
||||||
|
import { sortBy } from 'lodash';
|
||||||
import { DeviceDetectorService } from 'ngx-device-detector';
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -16,28 +17,12 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './analysis-page.html'
|
templateUrl: './analysis-page.html'
|
||||||
})
|
})
|
||||||
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
export class AnalysisPageComponent implements OnDestroy, OnInit {
|
||||||
public accounts: {
|
public bottom3: Position[];
|
||||||
[symbol: string]: Pick<PortfolioPosition, 'name'> & { value: number };
|
public daysInMarket: number;
|
||||||
};
|
|
||||||
public continents: {
|
|
||||||
[code: string]: { name: string; value: number };
|
|
||||||
};
|
|
||||||
public countries: {
|
|
||||||
[code: string]: { name: string; value: number };
|
|
||||||
};
|
|
||||||
public deviceType: string;
|
public deviceType: string;
|
||||||
public hasImpersonationId: boolean;
|
public hasImpersonationId: boolean;
|
||||||
public period = 'current';
|
|
||||||
public periodOptions: ToggleOption[] = [
|
|
||||||
{ label: 'Initial', value: 'original' },
|
|
||||||
{ label: 'Current', value: 'current' }
|
|
||||||
];
|
|
||||||
public investments: InvestmentItem[];
|
public investments: InvestmentItem[];
|
||||||
public portfolioPositions: { [symbol: string]: PortfolioPosition };
|
public top3: Position[];
|
||||||
public positions: { [symbol: string]: any };
|
|
||||||
public sectors: {
|
|
||||||
[name: string]: { name: string; value: number };
|
|
||||||
};
|
|
||||||
public user: User;
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
@ -69,8 +54,29 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
|
|||||||
this.dataService
|
this.dataService
|
||||||
.fetchInvestments()
|
.fetchInvestments()
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((response) => {
|
.subscribe(({ firstOrderDate, investments }) => {
|
||||||
this.investments = response;
|
this.daysInMarket = differenceInDays(new Date(), firstOrderDate);
|
||||||
|
this.investments = investments;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dataService
|
||||||
|
.fetchPositions({ range: 'max' })
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(({ positions }) => {
|
||||||
|
const positionsSorted = sortBy(
|
||||||
|
positions,
|
||||||
|
'netPerformancePercentage'
|
||||||
|
).reverse();
|
||||||
|
|
||||||
|
this.top3 = positionsSorted.slice(0, 3);
|
||||||
|
|
||||||
|
if (positions?.length > 3) {
|
||||||
|
this.bottom3 = positionsSorted.slice(-3).reverse();
|
||||||
|
} else {
|
||||||
|
this.bottom3 = [];
|
||||||
|
}
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
@ -5,16 +5,96 @@
|
|||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<mat-card-header>
|
<mat-card-header>
|
||||||
<mat-card-title class="align-items-center d-flex" i18n
|
<mat-card-title class="align-items-center d-flex" i18n
|
||||||
>Timeline</mat-card-title
|
>Investment Timeline</mat-card-title
|
||||||
>
|
>
|
||||||
</mat-card-header>
|
</mat-card-header>
|
||||||
<mat-card-content>
|
<mat-card-content>
|
||||||
<gf-investment-chart
|
<gf-investment-chart
|
||||||
class="h-100"
|
class="h-100"
|
||||||
|
[daysInMarket]="daysInMarket"
|
||||||
|
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
|
||||||
[investments]="investments"
|
[investments]="investments"
|
||||||
></gf-investment-chart>
|
></gf-investment-chart>
|
||||||
</mat-card-content>
|
</mat-card-content>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title class="align-items-center d-flex" i18n
|
||||||
|
>Top 3</mat-card-title
|
||||||
|
>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<div *ngFor="let position of top3; let i = index" class="d-flex py-1">
|
||||||
|
<div class="flex-grow-1 mr-2 text-truncate">
|
||||||
|
{{ i + 1 }}. {{ position.name }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[value]="position.netPerformancePercentage"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="!top3"
|
||||||
|
animation="pulse"
|
||||||
|
[theme]="{
|
||||||
|
height: '1.5rem',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<mat-card class="mb-3">
|
||||||
|
<mat-card-header>
|
||||||
|
<mat-card-title class="align-items-center d-flex" i18n
|
||||||
|
>Bottom 3</mat-card-title
|
||||||
|
>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<div
|
||||||
|
*ngFor="let position of bottom3; let i = index"
|
||||||
|
class="d-flex py-1"
|
||||||
|
>
|
||||||
|
<div class="flex-grow-1 mr-2 text-truncate">
|
||||||
|
{{ i + 1 }}. {{ position.name }}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
class="justify-content-end"
|
||||||
|
position="end"
|
||||||
|
[colorizeSign]="true"
|
||||||
|
[isPercent]="true"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
[value]="position.netPerformancePercentage"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="!bottom3"
|
||||||
|
animation="pulse"
|
||||||
|
[theme]="{
|
||||||
|
height: '1.5rem',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
</div>
|
||||||
|
</mat-card-content>
|
||||||
|
</mat-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
import { MatCardModule } from '@angular/material/card';
|
import { MatCardModule } from '@angular/material/card';
|
||||||
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
|
||||||
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
|
import { AnalysisPageRoutingModule } from './analysis-page-routing.module';
|
||||||
import { AnalysisPageComponent } from './analysis-page.component';
|
import { AnalysisPageComponent } from './analysis-page.component';
|
||||||
@ -13,7 +15,9 @@ import { AnalysisPageComponent } from './analysis-page.component';
|
|||||||
AnalysisPageRoutingModule,
|
AnalysisPageRoutingModule,
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfInvestmentChartModule,
|
GfInvestmentChartModule,
|
||||||
MatCardModule
|
GfValueModule,
|
||||||
|
MatCardModule,
|
||||||
|
NgxSkeletonLoaderModule
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
@ -3,16 +3,16 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12 col-md-6">
|
<div class="col-xs-12 col-md-6">
|
||||||
<mat-card class="mb-3">
|
<mat-card class="mb-3">
|
||||||
<h4 i18n>Transactions</h4>
|
<h4 i18n>Activities</h4>
|
||||||
<p class="mb-0">Manage your transactions.</p>
|
<p class="mb-0">Manage your activities.</p>
|
||||||
<p class="text-right">
|
<p class="text-right">
|
||||||
<a
|
<a
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-button
|
mat-button
|
||||||
[routerLink]="['/portfolio', 'transactions']"
|
[routerLink]="['/portfolio', 'activities']"
|
||||||
>
|
>
|
||||||
Open Transactions →
|
<span i18n>Open Activities</span>
|
||||||
|
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -31,12 +31,12 @@
|
|||||||
<p class="text-right">
|
<p class="text-right">
|
||||||
<a
|
<a
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-button
|
mat-button
|
||||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||||
[routerLink]="['/portfolio', 'allocations']"
|
[routerLink]="['/portfolio', 'allocations']"
|
||||||
>
|
>
|
||||||
Open Allocations →
|
<span i18n>Open Allocations</span>
|
||||||
|
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -57,12 +57,12 @@
|
|||||||
<p class="text-right">
|
<p class="text-right">
|
||||||
<a
|
<a
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-button
|
mat-button
|
||||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||||
[routerLink]="['/portfolio', 'analysis']"
|
[routerLink]="['/portfolio', 'analysis']"
|
||||||
>
|
>
|
||||||
Open Analysis →
|
<span i18n>Open Analysis</span>
|
||||||
|
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
@ -84,12 +84,12 @@
|
|||||||
<p class="text-right">
|
<p class="text-right">
|
||||||
<a
|
<a
|
||||||
color="primary"
|
color="primary"
|
||||||
i18n
|
|
||||||
mat-button
|
mat-button
|
||||||
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
[disabled]="hasPermissionForSubscription && user?.settings?.viewMode !== 'DEFAULT'"
|
||||||
[routerLink]="['/portfolio', 'report']"
|
[routerLink]="['/portfolio', 'report']"
|
||||||
>
|
>
|
||||||
Open X-ray →
|
<span i18n>Open X-ray</span>
|
||||||
|
<ion-icon class="ml-1" name="arrow-forward-outline"></ion-icon>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</mat-card>
|
</mat-card>
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { PortfolioReportRule } from '@ghostfolio/common/interfaces';
|
import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
|
import { PortfolioReportRule, User } from '@ghostfolio/common/interfaces';
|
||||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -14,6 +16,8 @@ export class ReportPageComponent implements OnDestroy, OnInit {
|
|||||||
public accountClusterRiskRules: PortfolioReportRule[];
|
public accountClusterRiskRules: PortfolioReportRule[];
|
||||||
public currencyClusterRiskRules: PortfolioReportRule[];
|
public currencyClusterRiskRules: PortfolioReportRule[];
|
||||||
public feeRules: PortfolioReportRule[];
|
public feeRules: PortfolioReportRule[];
|
||||||
|
public hasPermissionToCreateOrder: boolean;
|
||||||
|
public user: User;
|
||||||
|
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
@ -22,7 +26,8 @@ export class ReportPageComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService
|
private dataService: DataService,
|
||||||
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,6 +46,21 @@ export class ReportPageComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
|
@ -15,15 +15,24 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="m-0" i18n>Currency Cluster Risks</h4>
|
<h4 class="m-0" i18n>Currency Cluster Risks</h4>
|
||||||
<gf-rules [rules]="currencyClusterRiskRules"></gf-rules>
|
<gf-rules
|
||||||
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
|
[rules]="currencyClusterRiskRules"
|
||||||
|
></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="m-0" i18n>Account Cluster Risks</h4>
|
<h4 class="m-0" i18n>Account Cluster Risks</h4>
|
||||||
<gf-rules [rules]="accountClusterRiskRules"></gf-rules>
|
<gf-rules
|
||||||
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
|
[rules]="accountClusterRiskRules"
|
||||||
|
></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 class="m-0" i18n>Fees</h4>
|
<h4 class="m-0" i18n>Fees</h4>
|
||||||
<gf-rules [rules]="feeRules"></gf-rules>
|
<gf-rules
|
||||||
|
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
||||||
|
[rules]="feeRules"
|
||||||
|
></gf-rules>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
|
<form #addTransactionForm="ngForm" class="d-flex flex-column h-100">
|
||||||
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update transaction</h1>
|
<h1 *ngIf="data.transaction.id" mat-dialog-title i18n>Update activity</h1>
|
||||||
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add transaction</h1>
|
<h1 *ngIf="!data.transaction.id" mat-dialog-title i18n>Add activity</h1>
|
||||||
<div class="flex-grow-1" mat-dialog-content>
|
<div class="flex-grow-1" mat-dialog-content>
|
||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
@ -52,8 +52,9 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Type</mat-label>
|
<mat-label i18n>Type</mat-label>
|
||||||
<mat-select name="type" required [(value)]="data.transaction.type">
|
<mat-select name="type" required [(value)]="data.transaction.type">
|
||||||
<mat-option value="BUY" i18n> BUY </mat-option>
|
<mat-option value="BUY" i18n>BUY</mat-option>
|
||||||
<mat-option value="SELL" i18n> SELL </mat-option>
|
<mat-option value="DIVIDEND" i18n>DIVIDEND</mat-option>
|
||||||
|
<mat-option value="SELL" i18n>SELL</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
@ -141,7 +142,7 @@
|
|||||||
[(ngModel)]="data.transaction.unitPrice"
|
[(ngModel)]="data.transaction.unitPrice"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
*ngIf="currentMarketPrice"
|
*ngIf="currentMarketPrice && (data.transaction.type === 'BUY' || data.transaction.type === 'SELL')"
|
||||||
mat-icon-button
|
mat-icon-button
|
||||||
matSuffix
|
matSuffix
|
||||||
title="Apply current market price"
|
title="Apply current market price"
|
||||||
|
@ -4,6 +4,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
|
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
|
||||||
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
||||||
@ -61,7 +62,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.routeQueryParams = route.queryParams
|
this.routeQueryParams = route.queryParams
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((params) => {
|
.subscribe((params) => {
|
||||||
if (params['createDialog'] && this.hasPermissionToCreateOrder) {
|
if (params['createDialog']) {
|
||||||
this.openCreateTransactionDialog();
|
this.openCreateTransactionDialog();
|
||||||
} else if (params['editDialog']) {
|
} else if (params['editDialog']) {
|
||||||
if (this.transactions) {
|
if (this.transactions) {
|
||||||
@ -73,6 +74,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
}
|
}
|
||||||
|
} else if (params['positionDetailDialog'] && params['symbol']) {
|
||||||
|
this.openPositionDialog({
|
||||||
|
symbol: params['symbol']
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -386,4 +391,32 @@ export class TransactionsPageComponent implements OnDestroy, OnInit {
|
|||||||
this.router.navigate(['.'], { relativeTo: this.route });
|
this.router.navigate(['.'], { relativeTo: this.route });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private openPositionDialog({ symbol }: { symbol: string }) {
|
||||||
|
this.userService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe((user) => {
|
||||||
|
this.user = user;
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
||||||
|
autoFocus: false,
|
||||||
|
data: {
|
||||||
|
symbol,
|
||||||
|
baseCurrency: this.user?.settings?.baseCurrency,
|
||||||
|
deviceType: this.deviceType,
|
||||||
|
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 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,21 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>Transactions</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>Activities</h3>
|
||||||
<gf-transactions-table
|
<gf-activities-table
|
||||||
|
[activities]="transactions"
|
||||||
[baseCurrency]="user?.settings?.baseCurrency"
|
[baseCurrency]="user?.settings?.baseCurrency"
|
||||||
[deviceType]="deviceType"
|
[deviceType]="deviceType"
|
||||||
[hasPermissionToCreateOrder]="hasPermissionToCreateOrder"
|
[hasPermissionToCreateActivity]="hasPermissionToCreateOrder"
|
||||||
[hasPermissionToImportOrders]="hasPermissionToImportOrders"
|
[hasPermissionToImportActivities]="hasPermissionToImportOrders"
|
||||||
[locale]="user?.settings?.locale"
|
[locale]="user?.settings?.locale"
|
||||||
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"
|
[showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView"
|
||||||
[transactions]="transactions"
|
(activityDeleted)="onDeleteTransaction($event)"
|
||||||
|
(activityToClone)="onCloneTransaction($event)"
|
||||||
|
(activityToUpdate)="onUpdateTransaction($event)"
|
||||||
(export)="onExport()"
|
(export)="onExport()"
|
||||||
(import)="onImport()"
|
(import)="onImport()"
|
||||||
(transactionDeleted)="onDeleteTransaction($event)"
|
></gf-activities-table>
|
||||||
(transactionToClone)="onCloneTransaction($event)"
|
|
||||||
(transactionToUpdate)="onUpdateTransaction($event)"
|
|
||||||
></gf-transactions-table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -3,8 +3,8 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { GfTransactionsTableModule } from '@ghostfolio/client/components/transactions-table/transactions-table.module';
|
|
||||||
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service';
|
||||||
|
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
|
||||||
|
|
||||||
import { GfCreateOrUpdateTransactionDialogModule } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.module';
|
import { GfCreateOrUpdateTransactionDialogModule } from './create-or-update-transaction-dialog/create-or-update-transaction-dialog.module';
|
||||||
import { GfImportTransactionDialogModule } from './import-transaction-dialog/import-transaction-dialog.module';
|
import { GfImportTransactionDialogModule } from './import-transaction-dialog/import-transaction-dialog.module';
|
||||||
@ -16,9 +16,9 @@ import { TransactionsPageComponent } from './transactions-page.component';
|
|||||||
exports: [],
|
exports: [],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
GfActivitiesTableModule,
|
||||||
GfCreateOrUpdateTransactionDialogModule,
|
GfCreateOrUpdateTransactionDialogModule,
|
||||||
GfImportTransactionDialogModule,
|
GfImportTransactionDialogModule,
|
||||||
GfTransactionsTableModule,
|
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
MatSnackBarModule,
|
MatSnackBarModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
|
@ -20,6 +20,19 @@ export class AdminService {
|
|||||||
return this.http.post<void>(`/api/admin/gather/profile-data`, {});
|
return this.http.post<void>(`/api/admin/gather/profile-data`, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public gatherProfileDataBySymbol({
|
||||||
|
dataSource,
|
||||||
|
symbol
|
||||||
|
}: {
|
||||||
|
dataSource: DataSource;
|
||||||
|
symbol: string;
|
||||||
|
}) {
|
||||||
|
return this.http.post<void>(
|
||||||
|
`/api/admin/gather/profile-data/${dataSource}/${symbol}`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public gatherSymbol({
|
public gatherSymbol({
|
||||||
dataSource,
|
dataSource,
|
||||||
date,
|
date,
|
||||||
|
@ -5,7 +5,6 @@ import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto
|
|||||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||||
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
|
||||||
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
|
|
||||||
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
|
import { PortfolioPositions } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-positions.interface';
|
||||||
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
|
||||||
@ -23,13 +22,13 @@ import {
|
|||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
|
PortfolioInvestments,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
PortfolioPublicDetails,
|
PortfolioPublicDetails,
|
||||||
PortfolioReport,
|
PortfolioReport,
|
||||||
PortfolioSummary,
|
PortfolioSummary,
|
||||||
User
|
User
|
||||||
} from '@ghostfolio/common/interfaces';
|
} from '@ghostfolio/common/interfaces';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
|
||||||
import { permissions } from '@ghostfolio/common/permissions';
|
import { permissions } from '@ghostfolio/common/permissions';
|
||||||
import { DateRange } from '@ghostfolio/common/types';
|
import { DateRange } from '@ghostfolio/common/types';
|
||||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||||
@ -124,6 +123,18 @@ export class DataService {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public fetchInvestments(): Observable<PortfolioInvestments> {
|
||||||
|
return this.http.get<any>('/api/portfolio/investments').pipe(
|
||||||
|
map((response) => {
|
||||||
|
if (response.firstOrderDate) {
|
||||||
|
response.firstOrderDate = parseISO(response.firstOrderDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public fetchSymbolItem({
|
public fetchSymbolItem({
|
||||||
dataSource,
|
dataSource,
|
||||||
includeHistoricalData = false,
|
includeHistoricalData = false,
|
||||||
@ -170,10 +181,6 @@ export class DataService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public fetchInvestments() {
|
|
||||||
return this.http.get<InvestmentItem[]>('/api/portfolio/investments');
|
|
||||||
}
|
|
||||||
|
|
||||||
public fetchPortfolioDetails(aParams: { [param: string]: any }) {
|
public fetchPortfolioDetails(aParams: { [param: string]: any }) {
|
||||||
return this.http.get<PortfolioDetails>('/api/portfolio/details', {
|
return this.http.get<PortfolioDetails>('/api/portfolio/details', {
|
||||||
params: aParams
|
params: aParams
|
||||||
@ -212,8 +219,17 @@ export class DataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public fetchPositionDetail(aSymbol: string) {
|
public fetchPositionDetail(aSymbol: string) {
|
||||||
return this.http.get<PortfolioPositionDetail>(
|
return this.http.get<any>(`/api/portfolio/position/${aSymbol}`).pipe(
|
||||||
`/api/portfolio/position/${aSymbol}`
|
map((data) => {
|
||||||
|
if (data.orders) {
|
||||||
|
for (const order of data.orders) {
|
||||||
|
order.createdAt = parseISO(order.createdAt);
|
||||||
|
order.date = parseISO(order.date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,10 +214,15 @@ export class ImportTransactionsService {
|
|||||||
|
|
||||||
for (const key of ImportTransactionsService.TYPE_KEYS) {
|
for (const key of ImportTransactionsService.TYPE_KEYS) {
|
||||||
if (item[key]) {
|
if (item[key]) {
|
||||||
if (item[key].toLowerCase() === 'buy') {
|
switch (item[key].toLowerCase()) {
|
||||||
return Type.BUY;
|
case 'buy':
|
||||||
} else if (item[key].toLowerCase() === 'sell') {
|
return Type.BUY;
|
||||||
return Type.SELL;
|
case 'dividend':
|
||||||
|
return Type.DIVIDEND;
|
||||||
|
case 'sell':
|
||||||
|
return Type.SELL;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,30 +6,42 @@
|
|||||||
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io</loc>
|
<loc>https://ghostfol.io</loc>
|
||||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/about</loc>
|
<loc>https://ghostfol.io/about</loc>
|
||||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/about/changelog</loc>
|
||||||
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/blog</loc>
|
||||||
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
<loc>https://ghostfol.io/de/blog/2021/07/hallo-ghostfolio</loc>
|
||||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
<loc>https://ghostfol.io/en/blog/2021/07/hello-ghostfolio</loc>
|
||||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>https://ghostfol.io/en/blog/2022/01/ghostfolio-first-months-in-open-source</loc>
|
||||||
|
<lastmod>2022-01-05T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/pricing</loc>
|
<loc>https://ghostfol.io/pricing</loc>
|
||||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/register</loc>
|
<loc>https://ghostfol.io/register</loc>
|
||||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
<url>
|
<url>
|
||||||
<loc>https://ghostfol.io/resources</loc>
|
<loc>https://ghostfol.io/resources</loc>
|
||||||
<lastmod>2021-07-31T00:00:00+00:00</lastmod>
|
<lastmod>2022-01-01T00:00:00+00:00</lastmod>
|
||||||
</url>
|
</url>
|
||||||
</urlset>
|
</urlset>
|
||||||
|
@ -38,7 +38,7 @@ body {
|
|||||||
|
|
||||||
.blog {
|
.blog {
|
||||||
a {
|
a {
|
||||||
color: rgba(var(--palette-primary-500), 1);
|
color: rgba(var(--palette-primary-500), 1) !important;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -1,5 +1,17 @@
|
|||||||
|
import { ToggleOption } from './types';
|
||||||
|
|
||||||
export const baseCurrency = 'USD';
|
export const baseCurrency = 'USD';
|
||||||
|
|
||||||
|
export const defaultDateRangeOptions: ToggleOption[] = [
|
||||||
|
{ label: 'Today', value: '1d' },
|
||||||
|
{ label: 'YTD', value: 'ytd' },
|
||||||
|
{ label: '1Y', value: '1y' },
|
||||||
|
{ label: '5Y', value: '5y' },
|
||||||
|
{ label: 'Max', value: 'max' }
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
||||||
|
|
||||||
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
|
export const ghostfolioScraperApiSymbolPrefix = '_GF_';
|
||||||
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
|
export const ghostfolioCashSymbol = `${ghostfolioScraperApiSymbolPrefix}CASH`;
|
||||||
export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
|
export const ghostfolioFearAndGreedIndexSymbol = `${ghostfolioScraperApiSymbolPrefix}FEAR_AND_GREED_INDEX`;
|
||||||
@ -35,6 +47,7 @@ export const PROPERTY_CURRENCIES = 'CURRENCIES';
|
|||||||
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
|
export const PROPERTY_IS_READ_ONLY_MODE = 'IS_READ_ONLY_MODE';
|
||||||
export const PROPERTY_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING';
|
export const PROPERTY_LAST_DATA_GATHERING = 'LAST_DATA_GATHERING';
|
||||||
export const PROPERTY_LOCKED_DATA_GATHERING = 'LOCKED_DATA_GATHERING';
|
export const PROPERTY_LOCKED_DATA_GATHERING = 'LOCKED_DATA_GATHERING';
|
||||||
|
export const PROPERTY_SLACK_COMMUNITY_USERS = 'SLACK_COMMUNITY_USERS';
|
||||||
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';
|
export const PROPERTY_STRIPE_CONFIG = 'STRIPE_CONFIG';
|
||||||
export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE';
|
export const PROPERTY_SYSTEM_MESSAGE = 'SYSTEM_MESSAGE';
|
||||||
|
|
||||||
|
@ -3,8 +3,6 @@ import { getDate, getMonth, getYear, parse, subDays } from 'date-fns';
|
|||||||
|
|
||||||
import { ghostfolioScraperApiSymbolPrefix } from './config';
|
import { ghostfolioScraperApiSymbolPrefix } from './config';
|
||||||
|
|
||||||
export const DEMO_USER_ID = '9b112b4d-3b7d-4bad-9bdd-3b0f7b4dac2f';
|
|
||||||
|
|
||||||
export function capitalize(aString: string) {
|
export function capitalize(aString: string) {
|
||||||
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import { Export } from './export.interface';
|
|||||||
import { InfoItem } from './info-item.interface';
|
import { InfoItem } from './info-item.interface';
|
||||||
import { PortfolioChart } from './portfolio-chart.interface';
|
import { PortfolioChart } from './portfolio-chart.interface';
|
||||||
import { PortfolioDetails } from './portfolio-details.interface';
|
import { PortfolioDetails } from './portfolio-details.interface';
|
||||||
|
import { PortfolioInvestments } from './portfolio-investments.interface';
|
||||||
import { PortfolioItem } from './portfolio-item.interface';
|
import { PortfolioItem } from './portfolio-item.interface';
|
||||||
import { PortfolioOverview } from './portfolio-overview.interface';
|
import { PortfolioOverview } from './portfolio-overview.interface';
|
||||||
import { PortfolioPerformance } from './portfolio-performance.interface';
|
import { PortfolioPerformance } from './portfolio-performance.interface';
|
||||||
@ -33,6 +34,7 @@ export {
|
|||||||
InfoItem,
|
InfoItem,
|
||||||
PortfolioChart,
|
PortfolioChart,
|
||||||
PortfolioDetails,
|
PortfolioDetails,
|
||||||
|
PortfolioInvestments,
|
||||||
PortfolioItem,
|
PortfolioItem,
|
||||||
PortfolioOverview,
|
PortfolioOverview,
|
||||||
PortfolioPerformance,
|
PortfolioPerformance,
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import { InvestmentItem } from './investment-item.interface';
|
||||||
|
|
||||||
|
export interface PortfolioInvestments {
|
||||||
|
firstOrderDate: Date;
|
||||||
|
investments: InvestmentItem[];
|
||||||
|
}
|
@ -3,6 +3,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface';
|
|||||||
export interface PortfolioSummary extends PortfolioPerformance {
|
export interface PortfolioSummary extends PortfolioPerformance {
|
||||||
annualizedPerformancePercent: number;
|
annualizedPerformancePercent: number;
|
||||||
cash: number;
|
cash: number;
|
||||||
|
dividend: number;
|
||||||
committedFunds: number;
|
committedFunds: number;
|
||||||
fees: number;
|
fees: number;
|
||||||
firstOrderDate: Date;
|
firstOrderDate: Date;
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
export interface Statistics {
|
export interface Statistics {
|
||||||
activeUsers1d: number;
|
activeUsers1d: number;
|
||||||
activeUsers7d: number;
|
|
||||||
activeUsers30d: number;
|
activeUsers30d: number;
|
||||||
gitHubContributors: number;
|
gitHubContributors: number;
|
||||||
gitHubStargazers: number;
|
gitHubStargazers: number;
|
||||||
newUsers30d: number;
|
newUsers30d: number;
|
||||||
|
slackCommunityUsers: string;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import type { DateRange } from './date-range.type';
|
|||||||
import type { Granularity } from './granularity.type';
|
import type { Granularity } from './granularity.type';
|
||||||
import type { OrderWithAccount } from './order-with-account.type';
|
import type { OrderWithAccount } from './order-with-account.type';
|
||||||
import type { RequestWithUser } from './request-with-user.type';
|
import type { RequestWithUser } from './request-with-user.type';
|
||||||
|
import { ToggleOption } from './toggle-option.type';
|
||||||
|
|
||||||
export type {
|
export type {
|
||||||
AccessWithGranteeUser,
|
AccessWithGranteeUser,
|
||||||
@ -11,5 +12,6 @@ export type {
|
|||||||
DateRange,
|
DateRange,
|
||||||
Granularity,
|
Granularity,
|
||||||
OrderWithAccount,
|
OrderWithAccount,
|
||||||
RequestWithUser
|
RequestWithUser,
|
||||||
|
ToggleOption
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field
|
||||||
|
appearance="outline"
|
||||||
|
class="w-100"
|
||||||
|
[ngClass]="{ 'd-none': !hasPermissionToFilter }"
|
||||||
|
>
|
||||||
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
|
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
|
||||||
<mat-chip-list #chipList aria-label="Search keywords">
|
<mat-chip-list #chipList aria-label="Search keywords">
|
||||||
<mat-chip
|
<mat-chip
|
||||||
@ -73,11 +77,15 @@
|
|||||||
<td mat-cell *matCellDef="let element" class="px-1">
|
<td mat-cell *matCellDef="let element" class="px-1">
|
||||||
<div
|
<div
|
||||||
class="d-inline-flex p-1 type-badge"
|
class="d-inline-flex p-1 type-badge"
|
||||||
[ngClass]="element.type == 'BUY' ? 'buy' : 'sell'"
|
[ngClass]="{
|
||||||
|
buy: element.type === 'BUY',
|
||||||
|
dividend: element.type === 'DIVIDEND',
|
||||||
|
sell: element.type === 'SELL'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<ion-icon
|
<ion-icon
|
||||||
[name]="
|
[name]="
|
||||||
element.type === 'BUY'
|
element.type === 'BUY' || element.type === 'DIVIDEND'
|
||||||
? 'arrow-forward-circle-outline'
|
? 'arrow-forward-circle-outline'
|
||||||
: 'arrow-back-circle-outline'
|
: 'arrow-back-circle-outline'
|
||||||
"
|
"
|
||||||
@ -179,6 +187,27 @@
|
|||||||
</td>
|
</td>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
|
||||||
|
<ng-container matColumnDef="value">
|
||||||
|
<th
|
||||||
|
*matHeaderCellDef
|
||||||
|
class="justify-content-end px-1"
|
||||||
|
i18n
|
||||||
|
mat-header-cell
|
||||||
|
mat-sort-header
|
||||||
|
>
|
||||||
|
Value
|
||||||
|
</th>
|
||||||
|
<td *matCellDef="let element" class="px1" mat-cell>
|
||||||
|
<div class="d-flex justify-content-end">
|
||||||
|
<gf-value
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="isLoading ? undefined : element.value"
|
||||||
|
></gf-value>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ng-container matColumnDef="account">
|
<ng-container matColumnDef="account">
|
||||||
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
<th *matHeaderCellDef class="px-1" mat-header-cell>
|
||||||
<span class="d-none d-lg-block" i18n>Account</span>
|
<span class="d-none d-lg-block" i18n>Account</span>
|
||||||
@ -201,14 +230,14 @@
|
|||||||
<button
|
<button
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[matMenuTriggerFor]="transactionsMenu"
|
[matMenuTriggerFor]="activitiesMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #transactionsMenu="matMenu" xPosition="before">
|
<mat-menu #activitiesMenu="matMenu" xPosition="before">
|
||||||
<button
|
<button
|
||||||
*ngIf="hasPermissionToImportOrders"
|
*ngIf="hasPermissionToImportActivities"
|
||||||
class="align-items-center d-flex"
|
class="align-items-center d-flex"
|
||||||
mat-menu-item
|
mat-menu-item
|
||||||
(click)="onImport()"
|
(click)="onImport()"
|
||||||
@ -230,19 +259,19 @@
|
|||||||
<button
|
<button
|
||||||
class="mx-1 no-min-width px-2"
|
class="mx-1 no-min-width px-2"
|
||||||
mat-button
|
mat-button
|
||||||
[matMenuTriggerFor]="transactionMenu"
|
[matMenuTriggerFor]="activityMenu"
|
||||||
(click)="$event.stopPropagation()"
|
(click)="$event.stopPropagation()"
|
||||||
>
|
>
|
||||||
<ion-icon name="ellipsis-vertical"></ion-icon>
|
<ion-icon name="ellipsis-vertical"></ion-icon>
|
||||||
</button>
|
</button>
|
||||||
<mat-menu #transactionMenu="matMenu" xPosition="before">
|
<mat-menu #activityMenu="matMenu" xPosition="before">
|
||||||
<button i18n mat-menu-item (click)="onUpdateTransaction(element)">
|
<button i18n mat-menu-item (click)="onUpdateActivity(element)">
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
<button i18n mat-menu-item (click)="onCloneTransaction(element)">
|
<button i18n mat-menu-item (click)="onCloneActivity(element)">
|
||||||
Clone
|
Clone
|
||||||
</button>
|
</button>
|
||||||
<button i18n mat-menu-item (click)="onDeleteTransaction(element.id)">
|
<button i18n mat-menu-item (click)="onDeleteActivity(element.id)">
|
||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</mat-menu>
|
</mat-menu>
|
||||||
@ -254,12 +283,13 @@
|
|||||||
*matRowDef="let row; columns: displayedColumns"
|
*matRowDef="let row; columns: displayedColumns"
|
||||||
mat-row
|
mat-row
|
||||||
(click)="
|
(click)="
|
||||||
!row.isDraft &&
|
hasPermissionToOpenDetails &&
|
||||||
|
!row.isDraft &&
|
||||||
onOpenPositionDialog({
|
onOpenPositionDialog({
|
||||||
symbol: row.symbol
|
symbol: row.symbol
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
[ngClass]="{ 'is-draft': row.isDraft }"
|
[ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }"
|
||||||
></tr>
|
></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
@ -275,7 +305,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
*ngIf="
|
*ngIf="
|
||||||
dataSource.data.length === 0 && hasPermissionToCreateOrder && !isLoading
|
dataSource.data.length === 0 && hasPermissionToCreateActivity && !isLoading
|
||||||
"
|
"
|
||||||
class="p-3 text-center"
|
class="p-3 text-center"
|
||||||
>
|
>
|
@ -24,10 +24,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mat-row {
|
.mat-row {
|
||||||
&:not(.is-draft) {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.type-badge {
|
.type-badge {
|
||||||
background-color: rgba(var(--palette-foreground-text), 0.05);
|
background-color: rgba(var(--palette-foreground-text), 0.05);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
@ -41,6 +37,10 @@
|
|||||||
color: var(--green);
|
color: var(--green);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.dividend {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
|
||||||
&.sell {
|
&.sell {
|
||||||
color: var(--orange);
|
color: var(--orange);
|
||||||
}
|
}
|
@ -17,43 +17,41 @@ import {
|
|||||||
MatAutocompleteSelectedEvent
|
MatAutocompleteSelectedEvent
|
||||||
} from '@angular/material/autocomplete';
|
} from '@angular/material/autocomplete';
|
||||||
import { MatChipInputEvent } from '@angular/material/chips';
|
import { MatChipInputEvent } from '@angular/material/chips';
|
||||||
import { MatDialog } from '@angular/material/dialog';
|
|
||||||
import { MatSort } from '@angular/material/sort';
|
import { MatSort } from '@angular/material/sort';
|
||||||
import { MatTableDataSource } from '@angular/material/table';
|
import { MatTableDataSource } from '@angular/material/table';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/common/config';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
import { endOfToday, format, isAfter } from 'date-fns';
|
import { endOfToday, format, isAfter } from 'date-fns';
|
||||||
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
|
|
||||||
|
|
||||||
const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...';
|
const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...';
|
||||||
const SEARCH_STRING_SEPARATOR = ',';
|
const SEARCH_STRING_SEPARATOR = ',';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'gf-transactions-table',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
templateUrl: './transactions-table.component.html',
|
selector: 'gf-activities-table',
|
||||||
styleUrls: ['./transactions-table.component.scss']
|
styleUrls: ['./activities-table.component.scss'],
|
||||||
|
templateUrl: './activities-table.component.html'
|
||||||
})
|
})
|
||||||
export class TransactionsTableComponent
|
export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||||
implements OnChanges, OnDestroy, OnInit
|
@Input() activities: OrderWithAccount[];
|
||||||
{
|
|
||||||
@Input() baseCurrency: string;
|
@Input() baseCurrency: string;
|
||||||
@Input() deviceType: string;
|
@Input() deviceType: string;
|
||||||
@Input() hasPermissionToCreateOrder: boolean;
|
@Input() hasPermissionToCreateActivity: boolean;
|
||||||
@Input() hasPermissionToImportOrders: boolean;
|
@Input() hasPermissionToFilter = true;
|
||||||
|
@Input() hasPermissionToImportActivities: boolean;
|
||||||
|
@Input() hasPermissionToOpenDetails = true;
|
||||||
@Input() locale: string;
|
@Input() locale: string;
|
||||||
@Input() showActions: boolean;
|
@Input() showActions: boolean;
|
||||||
@Input() transactions: OrderWithAccount[];
|
@Input() showSymbolColumn = true;
|
||||||
|
|
||||||
|
@Output() activityDeleted = new EventEmitter<string>();
|
||||||
|
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
|
||||||
|
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
|
||||||
@Output() export = new EventEmitter<void>();
|
@Output() export = new EventEmitter<void>();
|
||||||
@Output() import = new EventEmitter<void>();
|
@Output() import = new EventEmitter<void>();
|
||||||
@Output() transactionDeleted = new EventEmitter<string>();
|
|
||||||
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
|
|
||||||
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
|
|
||||||
|
|
||||||
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
||||||
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
||||||
@ -77,21 +75,7 @@ export class TransactionsTableComponent
|
|||||||
private allFilters: string[];
|
private allFilters: string[];
|
||||||
private unsubscribeSubject = new Subject<void>();
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
public constructor(
|
public constructor(private router: Router) {
|
||||||
private dialog: MatDialog,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
private router: Router
|
|
||||||
) {
|
|
||||||
this.routeQueryParams = route.queryParams
|
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
|
||||||
.subscribe((params) => {
|
|
||||||
if (params['positionDetailDialog'] && params['symbol']) {
|
|
||||||
this.openPositionDialog({
|
|
||||||
symbol: params['symbol']
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.searchControl.valueChanges
|
this.searchControl.valueChanges
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
.subscribe((keyword) => {
|
.subscribe((keyword) => {
|
||||||
@ -138,8 +122,6 @@ export class TransactionsTableComponent
|
|||||||
this.searchControl.setValue(null);
|
this.searchControl.setValue(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnInit() {}
|
|
||||||
|
|
||||||
public ngOnChanges() {
|
public ngOnChanges() {
|
||||||
this.displayedColumns = [
|
this.displayedColumns = [
|
||||||
'count',
|
'count',
|
||||||
@ -150,6 +132,7 @@ export class TransactionsTableComponent
|
|||||||
'quantity',
|
'quantity',
|
||||||
'unitPrice',
|
'unitPrice',
|
||||||
'fee',
|
'fee',
|
||||||
|
'value',
|
||||||
'account'
|
'account'
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -157,10 +140,16 @@ export class TransactionsTableComponent
|
|||||||
this.displayedColumns.push('actions');
|
this.displayedColumns.push('actions');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.showSymbolColumn) {
|
||||||
|
this.displayedColumns = this.displayedColumns.filter((column) => {
|
||||||
|
return column !== 'symbol';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
if (this.transactions) {
|
if (this.activities) {
|
||||||
this.dataSource = new MatTableDataSource(this.transactions);
|
this.dataSource = new MatTableDataSource(this.activities);
|
||||||
this.dataSource.filterPredicate = (data, filter) => {
|
this.dataSource.filterPredicate = (data, filter) => {
|
||||||
const dataString = this.getFilterableValues(data)
|
const dataString = this.getFilterableValues(data)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
@ -178,13 +167,15 @@ export class TransactionsTableComponent
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onDeleteTransaction(aId: string) {
|
public onCloneActivity(aActivity: OrderWithAccount) {
|
||||||
const confirmation = confirm(
|
this.activityToClone.emit(aActivity);
|
||||||
'Do you really want to delete this transaction?'
|
}
|
||||||
);
|
|
||||||
|
public onDeleteActivity(aId: string) {
|
||||||
|
const confirmation = confirm('Do you really want to delete this activity?');
|
||||||
|
|
||||||
if (confirmation) {
|
if (confirmation) {
|
||||||
this.transactionDeleted.emit(aId);
|
this.activityDeleted.emit(aId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,33 +193,8 @@ export class TransactionsTableComponent
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateTransaction(aTransaction: OrderWithAccount) {
|
public onUpdateActivity(aActivity: OrderWithAccount) {
|
||||||
this.transactionToUpdate.emit(aTransaction);
|
this.activityToUpdate.emit(aActivity);
|
||||||
}
|
|
||||||
|
|
||||||
public onCloneTransaction(aTransaction: OrderWithAccount) {
|
|
||||||
this.transactionToClone.emit(aTransaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
public openPositionDialog({ symbol }: { symbol: string }): void {
|
|
||||||
const dialogRef = this.dialog.open(PositionDetailDialog, {
|
|
||||||
autoFocus: false,
|
|
||||||
data: {
|
|
||||||
symbol,
|
|
||||||
baseCurrency: this.baseCurrency,
|
|
||||||
deviceType: this.deviceType,
|
|
||||||
locale: this.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 });
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
@ -245,7 +211,7 @@ export class TransactionsTableComponent
|
|||||||
this.placeholder =
|
this.placeholder =
|
||||||
lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : '';
|
lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : '';
|
||||||
|
|
||||||
this.allFilters = this.getSearchableFieldValues(this.transactions).filter(
|
this.allFilters = this.getSearchableFieldValues(this.activities).filter(
|
||||||
(item) => {
|
(item) => {
|
||||||
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
|
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
|
||||||
}
|
}
|
||||||
@ -254,11 +220,11 @@ export class TransactionsTableComponent
|
|||||||
this.filters$.next(this.allFilters);
|
this.filters$.next(this.allFilters);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSearchableFieldValues(transactions: OrderWithAccount[]): string[] {
|
private getSearchableFieldValues(activities: OrderWithAccount[]): string[] {
|
||||||
const fieldValues = new Set<string>();
|
const fieldValues = new Set<string>();
|
||||||
|
|
||||||
for (const transaction of transactions) {
|
for (const activity of activities) {
|
||||||
this.getFilterableValues(transaction, fieldValues);
|
this.getFilterableValues(activity, fieldValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...fieldValues]
|
return [...fieldValues]
|
||||||
@ -283,15 +249,15 @@ export class TransactionsTableComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getFilterableValues(
|
private getFilterableValues(
|
||||||
transaction: OrderWithAccount,
|
activity: OrderWithAccount,
|
||||||
fieldValues: Set<string> = new Set<string>()
|
fieldValues: Set<string> = new Set<string>()
|
||||||
): string[] {
|
): string[] {
|
||||||
fieldValues.add(transaction.currency);
|
fieldValues.add(activity.currency);
|
||||||
fieldValues.add(transaction.symbol);
|
fieldValues.add(activity.symbol);
|
||||||
fieldValues.add(transaction.type);
|
fieldValues.add(activity.type);
|
||||||
fieldValues.add(transaction.Account?.name);
|
fieldValues.add(activity.Account?.name);
|
||||||
fieldValues.add(transaction.Account?.Platform?.name);
|
fieldValues.add(activity.Account?.Platform?.name);
|
||||||
fieldValues.add(format(transaction.date, 'yyyy'));
|
fieldValues.add(format(activity.date, 'yyyy'));
|
||||||
|
|
||||||
return [...fieldValues].filter((item) => {
|
return [...fieldValues].filter((item) => {
|
||||||
return item !== undefined;
|
return item !== undefined;
|
@ -9,22 +9,20 @@ import { MatMenuModule } from '@angular/material/menu';
|
|||||||
import { MatSortModule } from '@angular/material/sort';
|
import { MatSortModule } from '@angular/material/sort';
|
||||||
import { MatTableModule } from '@angular/material/table';
|
import { MatTableModule } from '@angular/material/table';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
import { GfPositionDetailDialogModule } from '../position/position-detail-dialog/position-detail-dialog.module';
|
import { ActivitiesTableComponent } from './activities-table.component';
|
||||||
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
|
|
||||||
import { TransactionsTableComponent } from './transactions-table.component';
|
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [TransactionsTableComponent],
|
declarations: [ActivitiesTableComponent],
|
||||||
exports: [TransactionsTableComponent],
|
exports: [ActivitiesTableComponent],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
GfNoTransactionsInfoModule,
|
GfNoTransactionsInfoModule,
|
||||||
GfPositionDetailDialogModule,
|
|
||||||
GfSymbolIconModule,
|
GfSymbolIconModule,
|
||||||
GfSymbolModule,
|
GfSymbolModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
@ -39,7 +37,6 @@ import { TransactionsTableComponent } from './transactions-table.component';
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterModule
|
RouterModule
|
||||||
],
|
],
|
||||||
providers: [],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
})
|
})
|
||||||
export class GfTransactionsTableModule {}
|
export class GfActivitiesTableModule {}
|
@ -5,10 +5,10 @@
|
|||||||
<a
|
<a
|
||||||
class="align-items-center justify-content-center"
|
class="align-items-center justify-content-center"
|
||||||
color="primary"
|
color="primary"
|
||||||
[routerLink]="['/portfolio', 'transactions']"
|
[routerLink]="['/portfolio', 'activities']"
|
||||||
[queryParams]="{ createDialog: true }"
|
[queryParams]="{ createDialog: true }"
|
||||||
mat-button
|
mat-button
|
||||||
>
|
>
|
||||||
<span i18n>Time to add your first transaction.</span>
|
<span i18n>Time to add your first activity.</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
58
package.json
58
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ghostfolio",
|
"name": "ghostfolio",
|
||||||
"version": "1.96.0",
|
"version": "1.100.0",
|
||||||
"homepage": "https://ghostfol.io",
|
"homepage": "https://ghostfol.io",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -21,7 +21,7 @@
|
|||||||
"database:gui": "prisma studio",
|
"database:gui": "prisma studio",
|
||||||
"database:migrate": "prisma migrate deploy",
|
"database:migrate": "prisma migrate deploy",
|
||||||
"database:push": "prisma db push",
|
"database:push": "prisma db push",
|
||||||
"database:seed": "prisma db seed --preview-feature",
|
"database:seed": "prisma db seed",
|
||||||
"database:setup": "yarn database:push && yarn database:seed",
|
"database:setup": "yarn database:push && yarn database:seed",
|
||||||
"dep-graph": "nx dep-graph",
|
"dep-graph": "nx dep-graph",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
@ -48,16 +48,16 @@
|
|||||||
"workspace-generator": "nx workspace-generator"
|
"workspace-generator": "nx workspace-generator"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "13.0.2",
|
"@angular/animations": "13.1.1",
|
||||||
"@angular/cdk": "13.0.2",
|
"@angular/cdk": "13.1.1",
|
||||||
"@angular/common": "13.0.2",
|
"@angular/common": "13.1.1",
|
||||||
"@angular/compiler": "13.0.2",
|
"@angular/compiler": "13.1.1",
|
||||||
"@angular/core": "13.0.2",
|
"@angular/core": "13.1.1",
|
||||||
"@angular/forms": "13.0.2",
|
"@angular/forms": "13.1.1",
|
||||||
"@angular/material": "13.0.2",
|
"@angular/material": "13.1.1",
|
||||||
"@angular/platform-browser": "13.0.2",
|
"@angular/platform-browser": "13.1.1",
|
||||||
"@angular/platform-browser-dynamic": "13.0.2",
|
"@angular/platform-browser-dynamic": "13.1.1",
|
||||||
"@angular/router": "13.0.2",
|
"@angular/router": "13.1.1",
|
||||||
"@codewithdan/observable-store": "2.2.11",
|
"@codewithdan/observable-store": "2.2.11",
|
||||||
"@dinero.js/currencies": "2.0.0-alpha.8",
|
"@dinero.js/currencies": "2.0.0-alpha.8",
|
||||||
"@nestjs/common": "8.2.3",
|
"@nestjs/common": "8.2.3",
|
||||||
@ -68,7 +68,7 @@
|
|||||||
"@nestjs/platform-express": "8.2.3",
|
"@nestjs/platform-express": "8.2.3",
|
||||||
"@nestjs/schedule": "1.0.2",
|
"@nestjs/schedule": "1.0.2",
|
||||||
"@nestjs/serve-static": "2.2.2",
|
"@nestjs/serve-static": "2.2.2",
|
||||||
"@nrwl/angular": "13.3.0",
|
"@nrwl/angular": "13.4.1",
|
||||||
"@prisma/client": "3.7.0",
|
"@prisma/client": "3.7.0",
|
||||||
"@simplewebauthn/browser": "4.1.0",
|
"@simplewebauthn/browser": "4.1.0",
|
||||||
"@simplewebauthn/server": "4.1.0",
|
"@simplewebauthn/server": "4.1.0",
|
||||||
@ -82,7 +82,7 @@
|
|||||||
"bootstrap": "4.6.0",
|
"bootstrap": "4.6.0",
|
||||||
"cache-manager": "3.4.3",
|
"cache-manager": "3.4.3",
|
||||||
"cache-manager-redis-store": "2.0.0",
|
"cache-manager-redis-store": "2.0.0",
|
||||||
"chart.js": "3.5.0",
|
"chart.js": "3.7.0",
|
||||||
"chartjs-adapter-date-fns": "2.0.0",
|
"chartjs-adapter-date-fns": "2.0.0",
|
||||||
"chartjs-plugin-datalabels": "2.0.0",
|
"chartjs-plugin-datalabels": "2.0.0",
|
||||||
"cheerio": "1.0.0-rc.6",
|
"cheerio": "1.0.0-rc.6",
|
||||||
@ -117,25 +117,25 @@
|
|||||||
"zone.js": "0.11.4"
|
"zone.js": "0.11.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "13.0.3",
|
"@angular-devkit/build-angular": "13.1.2",
|
||||||
"@angular-eslint/eslint-plugin": "13.0.1",
|
"@angular-eslint/eslint-plugin": "13.0.1",
|
||||||
"@angular-eslint/eslint-plugin-template": "13.0.1",
|
"@angular-eslint/eslint-plugin-template": "13.0.1",
|
||||||
"@angular-eslint/template-parser": "13.0.1",
|
"@angular-eslint/template-parser": "13.0.1",
|
||||||
"@angular/cli": "13.0.3",
|
"@angular/cli": "13.1.2",
|
||||||
"@angular/compiler-cli": "13.0.2",
|
"@angular/compiler-cli": "13.1.1",
|
||||||
"@angular/language-service": "13.0.2",
|
"@angular/language-service": "13.1.1",
|
||||||
"@angular/localize": "13.0.2",
|
"@angular/localize": "13.1.1",
|
||||||
"@nestjs/schematics": "8.0.5",
|
"@nestjs/schematics": "8.0.5",
|
||||||
"@nestjs/testing": "8.2.3",
|
"@nestjs/testing": "8.2.3",
|
||||||
"@nrwl/cli": "13.3.0",
|
"@nrwl/cli": "13.4.1",
|
||||||
"@nrwl/cypress": "13.3.0",
|
"@nrwl/cypress": "13.4.1",
|
||||||
"@nrwl/eslint-plugin-nx": "13.3.0",
|
"@nrwl/eslint-plugin-nx": "13.4.1",
|
||||||
"@nrwl/jest": "13.3.0",
|
"@nrwl/jest": "13.4.1",
|
||||||
"@nrwl/nest": "13.3.0",
|
"@nrwl/nest": "13.4.1",
|
||||||
"@nrwl/node": "13.3.0",
|
"@nrwl/node": "13.4.1",
|
||||||
"@nrwl/storybook": "13.3.0",
|
"@nrwl/storybook": "13.4.1",
|
||||||
"@nrwl/tao": "13.3.0",
|
"@nrwl/tao": "13.4.1",
|
||||||
"@nrwl/workspace": "13.3.0",
|
"@nrwl/workspace": "13.4.1",
|
||||||
"@storybook/addon-essentials": "6.4.9",
|
"@storybook/addon-essentials": "6.4.9",
|
||||||
"@storybook/angular": "6.4.9",
|
"@storybook/angular": "6.4.9",
|
||||||
"@storybook/builder-webpack5": "6.4.9",
|
"@storybook/builder-webpack5": "6.4.9",
|
||||||
@ -166,7 +166,7 @@
|
|||||||
"rimraf": "3.0.2",
|
"rimraf": "3.0.2",
|
||||||
"ts-jest": "27.0.5",
|
"ts-jest": "27.0.5",
|
||||||
"ts-node": "9.1.1",
|
"ts-node": "9.1.1",
|
||||||
"typescript": "4.4.4"
|
"typescript": "4.5.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
|
@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "Type" ADD VALUE 'DIVIDEND';
|
@ -206,5 +206,6 @@ enum Role {
|
|||||||
|
|
||||||
enum Type {
|
enum Type {
|
||||||
BUY
|
BUY
|
||||||
|
DIVIDEND
|
||||||
SELL
|
SELL
|
||||||
}
|
}
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
Date,Code,Currency,Price,Quantity,Action,Fee
|
Date,Code,Currency,Price,Quantity,Action,Fee
|
||||||
|
17/11/2021,MSFT,USD,0.62,5,dividend,0.00
|
||||||
16/09/2021,MSFT,USD,298.580,5,buy,19.00
|
16/09/2021,MSFT,USD,298.580,5,buy,19.00
|
||||||
|
|
Reference in New Issue
Block a user