Browse Source

Merge branch 'dockerpush' into main

pull/5027/head
dandevaud 2 years ago
committed by GitHub
parent
commit
6a8444ccc5
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .admin.cred
  2. 4
      .github/workflows/docker-image.yml
  3. 1
      apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts
  4. 75
      apps/api/src/app/portfolio/portfolio-calculator.ts
  5. 65
      apps/api/src/app/portfolio/portfolio.controller.ts
  6. 338
      apps/api/src/app/portfolio/portfolio.service.ts
  7. 12
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
  8. 19
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
  9. 6
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  10. 12
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  11. 2
      apps/client/src/app/services/import-activities.service.ts
  12. 9
      libs/ui/src/lib/activities-table/activities-table.component.html
  13. 4
      libs/ui/src/lib/activities-table/activities-table.component.scss
  14. 1
      libs/ui/src/lib/i18n.ts
  15. 1
      prisma/schema.prisma

1
.admin.cred

@ -0,0 +1 @@
14d4daf73eefed7da7c32ec19bc37e678be0244fb46c8f4965bfe9ece7384706ed58222ad9b96323893c1d845bc33a308e7524c2c79636062cbb095e0780cb51

4
.github/workflows/docker-image.yml

@ -6,7 +6,7 @@ on:
- '*.*.*'
pull_request:
branches:
- 'main'
- 'dockerpush'
jobs:
build_and_push:
@ -19,7 +19,7 @@ jobs:
id: meta
uses: docker/metadata-action@v4
with:
images: ghostfolio/ghostfolio
images: dandevaud/ghostfolio
tags: |
type=semver,pattern={{version}}

1
apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts

@ -10,6 +10,7 @@ export interface PortfolioPositionDetail {
averagePrice: number;
dataProviderInfo: DataProviderInfo;
dividendInBaseCurrency: number;
stakeRewards: number;
feeInBaseCurrency: number;
firstBuyDate: string;
grossPerformance: number;

75
apps/api/src/app/portfolio/portfolio-calculator.ts

@ -3,6 +3,7 @@ import { IDataGatheringItem } from '@ghostfolio/api/services/interfaces/interfac
import { DATE_FORMAT, parseDate, resetHours } from '@ghostfolio/common/helper';
import {
DataProviderInfo,
HistoricalDataItem,
ResponseError,
TimelinePosition
} from '@ghostfolio/common/interfaces';
@ -92,7 +93,7 @@ export class PortfolioCalculator {
let investment = new Big(0);
if (newQuantity.gt(0)) {
if (order.type === 'BUY') {
if (order.type === 'BUY' || order.type === 'STAKE') {
investment = oldAccumulatedSymbol.investment.plus(
order.quantity.mul(unitPrice)
);
@ -279,46 +280,29 @@ export class PortfolioCalculator {
};
}
for (const currentDate of dates) {
const dateString = format(currentDate, DATE_FORMAT);
return dates.map((date) => {
const dateString = format(date, DATE_FORMAT);
let totalCurrentValue = new Big(0);
let totalInvestmentValue = new Big(0);
let maxTotalInvestmentValue = new Big(0);
let totalNetPerformanceValue = new Big(0);
for (const symbol of Object.keys(valuesBySymbol)) {
const symbolValues = valuesBySymbol[symbol];
const currentValue =
symbolValues.currentValues?.[dateString] ?? new Big(0);
const investmentValue =
symbolValues.investmentValues?.[dateString] ?? new Big(0);
const maxInvestmentValue =
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0);
const netPerformanceValue =
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
valuesByDate[dateString] = {
totalCurrentValue: (
valuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
).add(currentValue),
totalInvestmentValue: (
valuesByDate[dateString]?.totalInvestmentValue ?? new Big(0)
).add(investmentValue),
maxTotalInvestmentValue: (
valuesByDate[dateString]?.maxTotalInvestmentValue ?? new Big(0)
).add(maxInvestmentValue),
totalNetPerformanceValue: (
valuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0)
).add(netPerformanceValue)
};
totalCurrentValue = totalCurrentValue.plus(
symbolValues.currentValues?.[dateString] ?? new Big(0)
);
totalInvestmentValue = totalInvestmentValue.plus(
symbolValues.investmentValues?.[dateString] ?? new Big(0)
);
maxTotalInvestmentValue = maxTotalInvestmentValue.plus(
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0)
);
totalNetPerformanceValue = totalNetPerformanceValue.plus(
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0)
);
}
}
return Object.entries(valuesByDate).map(([date, values]) => {
const {
maxTotalInvestmentValue,
totalCurrentValue,
totalInvestmentValue,
totalNetPerformanceValue
} = values;
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0)
? 0
: totalNetPerformanceValue
@ -327,7 +311,7 @@ export class PortfolioCalculator {
.toNumber();
return {
date,
date: dateString,
netPerformanceInPercentage,
netPerformance: totalNetPerformanceValue.toNumber(),
totalInvestment: totalInvestmentValue.toNumber(),
@ -934,6 +918,7 @@ export class PortfolioCalculator {
switch (type) {
case 'BUY':
case 'STAKE':
factor = 1;
break;
case 'SELL':
@ -1090,6 +1075,20 @@ export class PortfolioCalculator {
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
lastUnitPrice
});
} else {
let orderIndex = orders.findIndex(
(o) => o.date === format(day, DATE_FORMAT) && o.type === 'STAKE'
);
if (orderIndex >= 0) {
let order = orders[orderIndex];
orders.splice(orderIndex, 1);
orders.push({
...order,
unitPrice:
marketSymbolMap[format(day, DATE_FORMAT)]?.[symbol] ??
lastUnitPrice
});
}
}
lastUnitPrice = last(orders).unitPrice;
@ -1159,7 +1158,7 @@ export class PortfolioCalculator {
}
const transactionInvestment =
order.type === 'BUY'
order.type === 'BUY' || order.type === 'STAKE'
? order.quantity.mul(order.unitPrice).mul(this.getFactor(order.type))
: totalUnits.gt(0)
? totalInvestment

65
apps/api/src/app/portfolio/portfolio.controller.ts

@ -111,21 +111,38 @@ export class PortfolioController {
impersonationId ||
this.userService.isRestrictedView(this.request.user)
) {
const totalInvestment = Object.values(holdings)
.map((portfolioPosition) => {
return portfolioPosition.investment;
})
.reduce((a, b) => a + b, 0);
const totalValue = Object.values(holdings)
.map((portfolioPosition) => {
return this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency,
this.request.user.Settings.settings.baseCurrency
);
})
.reduce((a, b) => a + b, 0);
let investmentTuple: [number, number] = [0, 0];
for (let holding of Object.entries(holdings)) {
var portfolioPosition = holding[1];
investmentTuple[0] += portfolioPosition.investment;
investmentTuple[1] += this.exchangeRateDataService.toCurrency(
portfolioPosition.quantity * portfolioPosition.marketPrice,
portfolioPosition.currency,
this.request.user.Settings.settings.baseCurrency
);
}
const totalInvestment = investmentTuple[0];
const totalValue = investmentTuple[1];
if (hasDetails === false) {
portfolioSummary = nullifyValuesInObject(summary, [
'cash',
'committedFunds',
'currentGrossPerformance',
'currentNetPerformance',
'currentValue',
'dividend',
'emergencyFund',
'excludedAccountsAndActivities',
'fees',
'items',
'liabilities',
'netWorth',
'totalBuy',
'totalSell'
]);
}
for (const [symbol, portfolioPosition] of Object.entries(holdings)) {
portfolioPosition.grossPerformance = null;
@ -135,6 +152,24 @@ export class PortfolioController {
portfolioPosition.quantity = null;
portfolioPosition.valueInPercentage =
portfolioPosition.valueInBaseCurrency / totalValue;
(portfolioPosition.assetClass = hasDetails
? portfolioPosition.assetClass
: undefined),
(portfolioPosition.assetSubClass = hasDetails
? portfolioPosition.assetSubClass
: undefined),
(portfolioPosition.countries = hasDetails
? portfolioPosition.countries
: []),
(portfolioPosition.currency = hasDetails
? portfolioPosition.currency
: undefined),
(portfolioPosition.markets = hasDetails
? portfolioPosition.markets
: undefined),
(portfolioPosition.sectors = hasDetails
? portfolioPosition.sectors
: []);
}
for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) {

338
apps/api/src/app/portfolio/portfolio.service.ts

@ -525,11 +525,9 @@ export class PortfolioService {
}
const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {};
for (const position of currentPositions.positions) {
portfolioItemsNow[position.symbol] = position;
}
for (const item of currentPositions.positions) {
portfolioItemsNow[item.symbol] = item;
if (item.quantity.lte(0)) {
// Ignore positions without any quantity
continue;
@ -555,59 +553,13 @@ export class PortfolioService {
otherMarkets: 0
};
if (symbolProfile.countries.length > 0) {
for (const country of symbolProfile.countries) {
if (developedMarkets.includes(country.code)) {
markets.developedMarkets = new Big(markets.developedMarkets)
.plus(country.weight)
.toNumber();
} else if (emergingMarkets.includes(country.code)) {
markets.emergingMarkets = new Big(markets.emergingMarkets)
.plus(country.weight)
.toNumber();
} else {
markets.otherMarkets = new Big(markets.otherMarkets)
.plus(country.weight)
.toNumber();
}
if (country.code === 'JP') {
marketsAdvanced.japan = new Big(marketsAdvanced.japan)
.plus(country.weight)
.toNumber();
} else if (country.code === 'CA' || country.code === 'US') {
marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica)
.plus(country.weight)
.toNumber();
} else if (asiaPacificMarkets.includes(country.code)) {
marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific)
.plus(country.weight)
.toNumber();
} else if (emergingMarkets.includes(country.code)) {
marketsAdvanced.emergingMarkets = new Big(
marketsAdvanced.emergingMarkets
)
.plus(country.weight)
.toNumber();
} else if (europeMarkets.includes(country.code)) {
marketsAdvanced.europe = new Big(marketsAdvanced.europe)
.plus(country.weight)
.toNumber();
} else {
marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets)
.plus(country.weight)
.toNumber();
}
}
} else {
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
.plus(value)
.toNumber();
this.calculateMarketsAllocation(
symbolProfile,
markets,
marketsAdvanced,
value
);
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
.plus(value)
.toNumber();
}
holdings[item.symbol] = {
markets,
@ -640,6 +592,68 @@ export class PortfolioService {
};
}
await this.handleCashPosition(
filters,
isFilteredByAccount,
cashDetails,
userCurrency,
filteredValueInBaseCurrency,
holdings
);
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({
filters,
orders,
portfolioItemsNow,
userCurrency,
userId,
withExcludedAccounts
});
filteredValueInBaseCurrency = await this.handleEmergencyFunds(
filters,
cashDetails,
userCurrency,
filteredValueInBaseCurrency,
emergencyFund,
orders,
accounts,
holdings
);
const summary = await this.getSummary({
impersonationId,
userCurrency,
userId,
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
emergencyFundPositionsValueInBaseCurrency:
this.getEmergencyFundPositionsValueInBaseCurrency({
holdings
})
});
return {
accounts,
holdings,
platforms,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
: 0,
hasErrors: currentPositions.hasErrors,
totalValueInBaseCurrency: summary.netWorth
};
}
private async handleCashPosition(
filters: Filter[],
isFilteredByAccount: boolean,
cashDetails: CashDetails,
userCurrency: string,
filteredValueInBaseCurrency: Big,
holdings: { [symbol: string]: PortfolioPosition }
) {
const isFilteredByCash = filters?.some((filter) => {
return filter.type === 'ASSET_CLASS' && filter.id === 'CASH';
});
@ -655,16 +669,26 @@ export class PortfolioService {
holdings[symbol] = cashPositions[symbol];
}
}
}
const { accounts, platforms } = await this.getValueOfAccountsAndPlatforms({
filters,
orders,
portfolioItemsNow,
userCurrency,
userId,
withExcludedAccounts
});
private async handleEmergencyFunds(
filters: Filter[],
cashDetails: CashDetails,
userCurrency: string,
filteredValueInBaseCurrency: Big,
emergencyFund: Big,
orders: Activity[],
accounts: {
[id: string]: {
balance: number;
currency: string;
name: string;
valueInBaseCurrency: number;
valueInPercentage?: number;
};
},
holdings: { [symbol: string]: PortfolioPosition }
) {
if (
filters?.length === 1 &&
filters[0].id === EMERGENCY_FUND_TAG_ID &&
@ -699,30 +723,79 @@ export class PortfolioService {
valueInBaseCurrency: emergencyFundInCash
};
}
return filteredValueInBaseCurrency;
}
const summary = await this.getSummary({
impersonationId,
userCurrency,
userId,
balanceInBaseCurrency: cashDetails.balanceInBaseCurrency,
emergencyFundPositionsValueInBaseCurrency:
this.getEmergencyFundPositionsValueInBaseCurrency({
holdings
})
});
private calculateMarketsAllocation(
symbolProfile: EnhancedSymbolProfile,
markets: {
developedMarkets: number;
emergingMarkets: number;
otherMarkets: number;
},
marketsAdvanced: {
asiaPacific: number;
emergingMarkets: number;
europe: number;
japan: number;
northAmerica: number;
otherMarkets: number;
},
value: Big
) {
if (symbolProfile.countries.length > 0) {
for (const country of symbolProfile.countries) {
if (developedMarkets.includes(country.code)) {
markets.developedMarkets = new Big(markets.developedMarkets)
.plus(country.weight)
.toNumber();
} else if (emergingMarkets.includes(country.code)) {
markets.emergingMarkets = new Big(markets.emergingMarkets)
.plus(country.weight)
.toNumber();
} else {
markets.otherMarkets = new Big(markets.otherMarkets)
.plus(country.weight)
.toNumber();
}
return {
accounts,
holdings,
platforms,
summary,
filteredValueInBaseCurrency: filteredValueInBaseCurrency.toNumber(),
filteredValueInPercentage: summary.netWorth
? filteredValueInBaseCurrency.div(summary.netWorth).toNumber()
: 0,
hasErrors: currentPositions.hasErrors,
totalValueInBaseCurrency: summary.netWorth
};
if (country.code === 'JP') {
marketsAdvanced.japan = new Big(marketsAdvanced.japan)
.plus(country.weight)
.toNumber();
} else if (country.code === 'CA' || country.code === 'US') {
marketsAdvanced.northAmerica = new Big(marketsAdvanced.northAmerica)
.plus(country.weight)
.toNumber();
} else if (asiaPacificMarkets.includes(country.code)) {
marketsAdvanced.asiaPacific = new Big(marketsAdvanced.asiaPacific)
.plus(country.weight)
.toNumber();
} else if (emergingMarkets.includes(country.code)) {
marketsAdvanced.emergingMarkets = new Big(
marketsAdvanced.emergingMarkets
)
.plus(country.weight)
.toNumber();
} else if (europeMarkets.includes(country.code)) {
marketsAdvanced.europe = new Big(marketsAdvanced.europe)
.plus(country.weight)
.toNumber();
} else {
marketsAdvanced.otherMarkets = new Big(marketsAdvanced.otherMarkets)
.plus(country.weight)
.toNumber();
}
}
} else {
markets[UNKNOWN_KEY] = new Big(markets[UNKNOWN_KEY])
.plus(value)
.toNumber();
marketsAdvanced[UNKNOWN_KEY] = new Big(marketsAdvanced[UNKNOWN_KEY])
.plus(value)
.toNumber();
}
}
public async getPosition(
@ -755,6 +828,7 @@ export class PortfolioService {
averagePrice: undefined,
dataProviderInfo: undefined,
dividendInBaseCurrency: undefined,
stakeRewards: undefined,
feeInBaseCurrency: undefined,
firstBuyDate: undefined,
grossPerformance: undefined,
@ -783,7 +857,11 @@ export class PortfolioService {
.filter((order) => {
tags = tags.concat(order.tags);
return order.type === 'BUY' || order.type === 'SELL';
return (
order.type === 'BUY' ||
order.type === 'SELL' ||
order.type === 'STAKE'
);
})
.map((order) => ({
currency: order.SymbolProfile.currency,
@ -840,6 +918,16 @@ export class PortfolioService {
})
);
const stakeRewards = getSum(
orders
.filter(({ type }) => {
return type === 'STAKE';
})
.map(({ quantity }) => {
return new Big(quantity);
})
);
// Convert investment, gross and net performance to currency of user
const investment = this.exchangeRateDataService.toCurrency(
position.investment?.toNumber(),
@ -934,6 +1022,7 @@ export class PortfolioService {
averagePrice: averagePrice.toNumber(),
dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0],
dividendInBaseCurrency: dividendInBaseCurrency.toNumber(),
stakeRewards: stakeRewards.toNumber(),
feeInBaseCurrency: this.exchangeRateDataService.toCurrency(
fee.toNumber(),
SymbolProfile.currency,
@ -997,6 +1086,7 @@ export class PortfolioService {
averagePrice: 0,
dataProviderInfo: undefined,
dividendInBaseCurrency: 0,
stakeRewards: 0,
feeInBaseCurrency: 0,
firstBuyDate: undefined,
grossPerformance: undefined,
@ -1644,41 +1734,63 @@ export class PortfolioService {
userId
});
const activities = await this.orderService.getOrders({
const ordersRaw = await this.orderService.getOrders({
userCurrency,
userId
});
const excludedActivities = (
await this.orderService.getOrders({
userCurrency,
userId,
withExcludedAccounts: true
})
).filter(({ Account: account }) => {
return account?.isExcluded ?? false;
userId,
withExcludedAccounts: true
});
const activities: Activity[] = [];
const excludedActivities: Activity[] = [];
let dividend = 0;
let fees = 0;
let items = 0;
let liabilities = 0;
let totalBuy = 0;
let totalSell = 0;
for (let order of ordersRaw) {
if (order.Account?.isExcluded ?? false) {
excludedActivities.push(order);
} else {
activities.push(order);
fees += this.exchangeRateDataService.toCurrency(
order.fee,
order.SymbolProfile.currency,
userCurrency
);
let amount = this.exchangeRateDataService.toCurrency(
new Big(order.quantity).mul(order.unitPrice).toNumber(),
order.SymbolProfile.currency,
userCurrency
);
switch (order.type) {
case 'DIVIDEND':
dividend += amount;
break;
case 'ITEM':
items += amount;
break;
case 'SELL':
totalSell += amount;
break;
case 'BUY':
totalBuy += amount;
break;
case 'LIABILITY':
liabilities += amount;
}
}
}
const dividend = this.getDividend({
activities,
userCurrency
}).toNumber();
const emergencyFund = new Big(
Math.max(
emergencyFundPositionsValueInBaseCurrency,
(user.Settings?.settings as UserSettings)?.emergencyFund ?? 0
)
);
const fees = this.getFees({ activities, userCurrency }).toNumber();
const firstOrderDate = activities[0]?.date;
const items = this.getItems(activities).toNumber();
const liabilities = this.getLiabilities({
activities,
userCurrency
}).toNumber();
const totalBuy = this.getTotalByType(activities, userCurrency, 'BUY');
const totalSell = this.getTotalByType(activities, userCurrency, 'SELL');
const firstOrderDate = activities[0]?.date;
const cash = new Big(balanceInBaseCurrency)
.minus(emergencyFund)
@ -1780,7 +1892,7 @@ export class PortfolioService {
userCurrency,
userId,
withExcludedAccounts,
types: ['BUY', 'SELL']
types: ['BUY', 'SELL', 'STAKE']
});
if (orders.length <= 0) {

12
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts

@ -40,6 +40,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
};
public dataProviderInfo: DataProviderInfo;
public dividendInBaseCurrency: number;
public stakeRewards: number;
public feeInBaseCurrency: number;
public firstBuyDate: string;
public grossPerformance: number;
@ -54,6 +55,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public orders: OrderWithAccount[];
public quantity: number;
public quantityPrecision = 2;
public stakePrecision = 2;
public reportDataGlitchMail: string;
public sectors: {
[name: string]: { name: string; value: number };
@ -84,6 +86,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
averagePrice,
dataProviderInfo,
dividendInBaseCurrency,
stakeRewards,
feeInBaseCurrency,
firstBuyDate,
grossPerformance,
@ -107,6 +110,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
this.countries = {};
this.dataProviderInfo = dataProviderInfo;
this.dividendInBaseCurrency = dividendInBaseCurrency;
this.stakeRewards = stakeRewards;
this.feeInBaseCurrency = feeInBaseCurrency;
this.firstBuyDate = firstBuyDate;
this.grossPerformance = grossPerformance;
@ -226,6 +230,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
if (Number.isInteger(this.quantity)) {
this.quantityPrecision = 0;
if (
orders
.filter((o) => o.type === 'STAKE')
.every((o) => Number.isInteger(o.quantity))
) {
this.stakeRewards = 0;
}
} else if (this.SymbolProfile?.assetSubClass === 'CRYPTOCURRENCY') {
if (this.quantity < 1) {
this.quantityPrecision = 7;
@ -234,6 +245,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
} else if (this.quantity > 10000000) {
this.quantityPrecision = 0;
}
this.stakePrecision = this.quantityPrecision;
}
this.changeDetectorRef.markForCheck();

19
apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html

@ -126,7 +126,10 @@
>Investment</gf-value
>
</div>
<div class="col-6 mb-3">
<div
*ngIf="dividendInBaseCurrency > 0 || !stakeRewards"
class="col-6 mb-3"
>
<gf-value
i18n
size="medium"
@ -137,6 +140,20 @@
>Dividend</gf-value
>
</div>
<div
*ngIf="stakeRewards > 0 && dividendInBaseCurrency == 0"
class="col-6 mb-3"
>
<gf-value
i18n
size="medium"
[locale]="data.locale"
[precision]="stakePrecision"
[value]="stakeRewards"
>Stake Rewards
</gf-value>
</div>
<div class="col-6 mb-3">
<gf-value
i18n

6
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -223,6 +223,10 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.activityForm.controls['quantity'].value *
this.activityForm.controls['unitPrice'].value +
this.activityForm.controls['fee'].value ?? 0;
} else if (this.activityForm.controls['type'].value === 'STAKE') {
this.total =
this.activityForm.controls['quantity'].value *
this.currentMarketPrice ?? 0;
} else {
this.total =
this.activityForm.controls['quantity'].value *
@ -237,7 +241,7 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
if (this.activityForm.controls['searchSymbol'].invalid) {
this.data.activity.SymbolProfile = null;
} else if (
['BUY', 'DIVIDEND', 'SELL'].includes(
['BUY', 'DIVIDEND', 'SELL', 'STAKE'].includes(
this.activityForm.controls['type'].value
)
) {

12
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -25,6 +25,13 @@
<mat-option class="line-height-1" value="DIVIDEND">
<span><b>{{ typesTranslationMap['DIVIDEND'] }}</b></span>
</mat-option>
<mat-option class="line-height-1" value="STAKE">
<span><b>{{ typesTranslationMap['STAKE'] }}</b></span
><br />
<small class="text-muted text-nowrap" i18n
>Stake rewards, stock dividends, free/gifted stocks</small
>
</mat-option>
<mat-option class="line-height-1" value="LIABILITY">
<span><b>{{ typesTranslationMap['LIABILITY'] }}</b></span>
<br />
@ -132,7 +139,10 @@
<input formControlName="quantity" matInput type="number" />
</mat-form-field>
</div>
<div class="align-items-start d-flex mb-3">
<div
*ngIf="activityForm.controls['type']?.value !== 'STAKE'"
class="align-items-start d-flex mb-3"
>
<mat-form-field appearance="outline" class="w-100">
<mat-label
><ng-container [ngSwitch]="activityForm.controls['type']?.value">

2
apps/client/src/app/services/import-activities.service.ts

@ -346,6 +346,8 @@ export class ImportActivitiesService {
return Type.LIABILITY;
case 'sell':
return Type.SELL;
case 'stake':
return Type.STAKE;
default:
break;
}

9
libs/ui/src/lib/activities-table/activities-table.component.html

@ -163,11 +163,16 @@
dividend: element.type === 'DIVIDEND',
item: element.type === 'ITEM',
liability: element.type === 'LIABILITY',
sell: element.type === 'SELL'
sell: element.type === 'SELL',
stake: element.type === 'STAKE'
}"
>
<ion-icon
*ngIf="element.type === 'BUY' || element.type === 'DIVIDEND'"
*ngIf="
element.type === 'BUY' ||
element.type === 'DIVIDEND' ||
element.type === 'STAKE'
"
name="arrow-up-circle-outline"
></ion-icon>
<ion-icon

4
libs/ui/src/lib/activities-table/activities-table.component.scss

@ -33,6 +33,10 @@
color: var(--blue);
}
&.stake {
color: var(--blue);
}
&.item {
color: var(--purple);
}

1
libs/ui/src/lib/i18n.ts

@ -33,6 +33,7 @@ const locales = {
ITEM: $localize`Valuable`,
LIABILITY: $localize`Liability`,
SELL: $localize`Sell`,
STAKE: $localize`Stake`,
// enum AssetClass
CASH: $localize`Cash`,

1
prisma/schema.prisma

@ -252,6 +252,7 @@ enum Type {
ITEM
LIABILITY
SELL
STAKE
}
enum ViewMode {

Loading…
Cancel
Save