parent
c61a415fb2
commit
b7bbc029ac
@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Added the tags to the create or edit transaction dialog
|
||||||
|
- Added the tags to the position detail dialog
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Changed the date to UTC in the data gathering service
|
- Changed the date to UTC in the data gathering service
|
||||||
|
@ -202,7 +202,8 @@ export class OrderService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
SymbolProfile: true
|
SymbolProfile: true,
|
||||||
|
tags: true
|
||||||
},
|
},
|
||||||
orderBy: { date: 'asc' }
|
orderBy: { date: 'asc' }
|
||||||
})
|
})
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
import { EnhancedSymbolProfile } from '@ghostfolio/api/services/interfaces/symbol-profile.interface';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||||
|
import { Tag } from '@prisma/client';
|
||||||
|
|
||||||
export interface PortfolioPositionDetail {
|
export interface PortfolioPositionDetail {
|
||||||
averagePrice: number;
|
averagePrice: number;
|
||||||
@ -16,6 +17,7 @@ export interface PortfolioPositionDetail {
|
|||||||
orders: OrderWithAccount[];
|
orders: OrderWithAccount[];
|
||||||
quantity: number;
|
quantity: number;
|
||||||
SymbolProfile: EnhancedSymbolProfile;
|
SymbolProfile: EnhancedSymbolProfile;
|
||||||
|
tags: Tag[];
|
||||||
transactionCount: number;
|
transactionCount: number;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,12 @@ import type {
|
|||||||
} from '@ghostfolio/common/types';
|
} from '@ghostfolio/common/types';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { REQUEST } from '@nestjs/core';
|
import { REQUEST } from '@nestjs/core';
|
||||||
import { AssetClass, DataSource, Type as TypeOfOrder } from '@prisma/client';
|
import {
|
||||||
|
AssetClass,
|
||||||
|
DataSource,
|
||||||
|
Tag,
|
||||||
|
Type as TypeOfOrder
|
||||||
|
} from '@prisma/client';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
import {
|
import {
|
||||||
differenceInDays,
|
differenceInDays,
|
||||||
@ -62,7 +67,7 @@ import {
|
|||||||
subDays,
|
subDays,
|
||||||
subYears
|
subYears
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { isEmpty, sortBy } from 'lodash';
|
import { isEmpty, sortBy, uniqBy } from 'lodash';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
HistoricalDataContainer,
|
HistoricalDataContainer,
|
||||||
@ -476,8 +481,11 @@ export class PortfolioService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let tags: Tag[] = [];
|
||||||
|
|
||||||
if (orders.length <= 0) {
|
if (orders.length <= 0) {
|
||||||
return {
|
return {
|
||||||
|
tags,
|
||||||
averagePrice: undefined,
|
averagePrice: undefined,
|
||||||
firstBuyDate: undefined,
|
firstBuyDate: undefined,
|
||||||
grossPerformance: undefined,
|
grossPerformance: undefined,
|
||||||
@ -504,6 +512,8 @@ export class PortfolioService {
|
|||||||
|
|
||||||
const portfolioOrders: PortfolioOrder[] = orders
|
const portfolioOrders: PortfolioOrder[] = orders
|
||||||
.filter((order) => {
|
.filter((order) => {
|
||||||
|
tags = tags.concat(order.tags);
|
||||||
|
|
||||||
return order.type === 'BUY' || order.type === 'SELL';
|
return order.type === 'BUY' || order.type === 'SELL';
|
||||||
})
|
})
|
||||||
.map((order) => ({
|
.map((order) => ({
|
||||||
@ -518,6 +528,8 @@ export class PortfolioService {
|
|||||||
unitPrice: new Big(order.unitPrice)
|
unitPrice: new Big(order.unitPrice)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
tags = uniqBy(tags, 'id');
|
||||||
|
|
||||||
const portfolioCalculator = new PortfolioCalculator({
|
const portfolioCalculator = new PortfolioCalculator({
|
||||||
currency: positionCurrency,
|
currency: positionCurrency,
|
||||||
currentRateService: this.currentRateService,
|
currentRateService: this.currentRateService,
|
||||||
@ -626,6 +638,7 @@ export class PortfolioService {
|
|||||||
netPerformance,
|
netPerformance,
|
||||||
orders,
|
orders,
|
||||||
SymbolProfile,
|
SymbolProfile,
|
||||||
|
tags,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
averagePrice: averagePrice.toNumber(),
|
averagePrice: averagePrice.toNumber(),
|
||||||
grossPerformancePercent:
|
grossPerformancePercent:
|
||||||
@ -682,6 +695,7 @@ export class PortfolioService {
|
|||||||
minPrice,
|
minPrice,
|
||||||
orders,
|
orders,
|
||||||
SymbolProfile,
|
SymbolProfile,
|
||||||
|
tags,
|
||||||
averagePrice: 0,
|
averagePrice: 0,
|
||||||
firstBuyDate: undefined,
|
firstBuyDate: undefined,
|
||||||
grossPerformance: undefined,
|
grossPerformance: undefined,
|
||||||
|
@ -11,7 +11,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
|
|||||||
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
|
||||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
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 { SymbolProfile } from '@prisma/client';
|
import { SymbolProfile, Tag } from '@prisma/client';
|
||||||
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
@ -48,6 +48,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
[name: string]: { name: string; value: number };
|
[name: string]: { name: string; value: number };
|
||||||
};
|
};
|
||||||
public SymbolProfile: SymbolProfile;
|
public SymbolProfile: SymbolProfile;
|
||||||
|
public tags: Tag[];
|
||||||
public transactionCount: number;
|
public transactionCount: number;
|
||||||
public value: number;
|
public value: number;
|
||||||
|
|
||||||
@ -83,6 +84,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
orders,
|
orders,
|
||||||
quantity,
|
quantity,
|
||||||
SymbolProfile,
|
SymbolProfile,
|
||||||
|
tags,
|
||||||
transactionCount,
|
transactionCount,
|
||||||
value
|
value
|
||||||
}) => {
|
}) => {
|
||||||
@ -115,6 +117,7 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
|
|||||||
this.quantity = quantity;
|
this.quantity = quantity;
|
||||||
this.sectors = {};
|
this.sectors = {};
|
||||||
this.SymbolProfile = SymbolProfile;
|
this.SymbolProfile = SymbolProfile;
|
||||||
|
this.tags = tags;
|
||||||
this.transactionCount = transactionCount;
|
this.transactionCount = transactionCount;
|
||||||
this.value = value;
|
this.value = value;
|
||||||
|
|
||||||
|
@ -194,21 +194,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gf-activities-table
|
<div class="mb-3">
|
||||||
*ngIf="orders?.length > 0"
|
<div class="h4 mb-0" i18n>Activities</div>
|
||||||
[activities]="orders"
|
<gf-activities-table
|
||||||
[baseCurrency]="data.baseCurrency"
|
*ngIf="orders?.length > 0"
|
||||||
[deviceType]="data.deviceType"
|
[activities]="orders"
|
||||||
[hasPermissionToCreateActivity]="false"
|
[baseCurrency]="data.baseCurrency"
|
||||||
[hasPermissionToExportActivities]="!hasImpersonationId"
|
[deviceType]="data.deviceType"
|
||||||
[hasPermissionToFilter]="false"
|
[hasPermissionToCreateActivity]="false"
|
||||||
[hasPermissionToImportActivities]="false"
|
[hasPermissionToExportActivities]="!hasImpersonationId"
|
||||||
[hasPermissionToOpenDetails]="false"
|
[hasPermissionToFilter]="false"
|
||||||
[locale]="data.locale"
|
[hasPermissionToImportActivities]="false"
|
||||||
[showActions]="false"
|
[hasPermissionToOpenDetails]="false"
|
||||||
[showSymbolColumn]="false"
|
[locale]="data.locale"
|
||||||
(export)="onExport()"
|
[showActions]="false"
|
||||||
></gf-activities-table>
|
[showSymbolColumn]="false"
|
||||||
|
(export)="onExport()"
|
||||||
|
></gf-activities-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="tags?.length > 0">
|
||||||
|
<div class="h4" i18n>Tags</div>
|
||||||
|
<mat-chip-list>
|
||||||
|
<mat-chip *ngFor="let tag of tags">{{ tag.name }}</mat-chip>
|
||||||
|
</mat-chip-list>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<gf-dialog-footer
|
<gf-dialog-footer
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
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 { MatChipsModule } from '@angular/material/chips';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
|
||||||
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
|
||||||
@ -24,6 +25,7 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
|
|||||||
GfPortfolioProportionChartModule,
|
GfPortfolioProportionChartModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatChipsModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
NgxSkeletonLoaderModule
|
NgxSkeletonLoaderModule
|
||||||
],
|
],
|
||||||
|
@ -86,6 +86,7 @@ export class CreateOrUpdateTransactionDialog implements OnDestroy {
|
|||||||
},
|
},
|
||||||
Validators.required
|
Validators.required
|
||||||
],
|
],
|
||||||
|
tags: [this.data.activity?.tags],
|
||||||
type: [undefined, Validators.required], // Set after value changes subscription
|
type: [undefined, Validators.required], // Set after value changes subscription
|
||||||
unitPrice: [this.data.activity?.unitPrice, Validators.required]
|
unitPrice: [this.data.activity?.unitPrice, Validators.required]
|
||||||
});
|
});
|
||||||
|
@ -134,6 +134,18 @@
|
|||||||
>
|
>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
[ngClass]="{ 'd-none': activityForm.controls['tags']?.value?.length <= 0 }"
|
||||||
|
>
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Tags</mat-label>
|
||||||
|
<mat-chip-list>
|
||||||
|
<mat-chip *ngFor="let tag of activityForm.controls['tags']?.value">
|
||||||
|
{{ tag.name }}
|
||||||
|
</mat-chip>
|
||||||
|
</mat-chip-list>
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex" mat-dialog-actions>
|
<div class="d-flex" mat-dialog-actions>
|
||||||
<gf-value
|
<gf-value
|
||||||
|
@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||||
import { MatButtonModule } from '@angular/material/button';
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatChipsModule } from '@angular/material/chips';
|
||||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
@ -24,6 +25,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction-
|
|||||||
FormsModule,
|
FormsModule,
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
|
MatChipsModule,
|
||||||
MatDatepickerModule,
|
MatDatepickerModule,
|
||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { Account, Order, Platform, SymbolProfile } from '@prisma/client';
|
import { Account, Order, Platform, SymbolProfile, Tag } from '@prisma/client';
|
||||||
|
|
||||||
type AccountWithPlatform = Account & { Platform?: Platform };
|
type AccountWithPlatform = Account & { Platform?: Platform };
|
||||||
|
|
||||||
export type OrderWithAccount = Order & {
|
export type OrderWithAccount = Order & {
|
||||||
Account?: AccountWithPlatform;
|
Account?: AccountWithPlatform;
|
||||||
SymbolProfile?: SymbolProfile;
|
SymbolProfile?: SymbolProfile;
|
||||||
|
tags?: Tag[];
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user