Feature/add filters to analytics page (#1559)

* Add filters to analysis page

* Update changelog
pull/1561/head
Thomas Kaul 2 years ago committed by GitHub
parent dd7a6f1562
commit b20fa55b79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Added support for filtering on the analysis page
- Added the price to the `Subscription` database schema
### Changed

@ -189,11 +189,21 @@ export class PortfolioController {
@UseGuards(AuthGuard('jwt'))
public async getDividends(
@Headers('impersonation-id') impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('groupBy') groupBy?: GroupBy,
@Query('range') dateRange: DateRange = 'max',
@Query('groupBy') groupBy?: GroupBy
@Query('tags') filterByTags?: string
): Promise<PortfolioDividends> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
});
let dividends = await this.portfolioService.getDividends({
dateRange,
filters,
groupBy,
impersonationId
});
@ -229,11 +239,21 @@ export class PortfolioController {
@UseGuards(AuthGuard('jwt'))
public async getInvestments(
@Headers('impersonation-id') impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('groupBy') groupBy?: GroupBy,
@Query('range') dateRange: DateRange = 'max',
@Query('groupBy') groupBy?: GroupBy
@Query('tags') filterByTags?: string
): Promise<PortfolioInvestments> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
});
let investments = await this.portfolioService.getInvestments({
dateRange,
filters,
groupBy,
impersonationId
});
@ -271,10 +291,20 @@ export class PortfolioController {
@Version('2')
public async getPerformanceV2(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange: DateRange = 'max'
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
): Promise<PortfolioPerformanceResponse> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
});
const performanceInformation = await this.portfolioService.getPerformance({
dateRange,
filters,
impersonationId,
userId: this.request.user.id
});
@ -329,12 +359,22 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getPositions(
@Headers('impersonation-id') impersonationId: string,
@Query('range') dateRange: DateRange = 'max'
@Query('accounts') filterByAccounts?: string,
@Query('assetClasses') filterByAssetClasses?: string,
@Query('range') dateRange: DateRange = 'max',
@Query('tags') filterByTags?: string
): Promise<PortfolioPositions> {
const result = await this.portfolioService.getPositions(
impersonationId,
dateRange
);
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterByAssetClasses,
filterByTags
});
const result = await this.portfolioService.getPositions({
dateRange,
filters,
impersonationId
});
if (
impersonationId ||

@ -210,16 +210,19 @@ export class PortfolioService {
public async getDividends({
dateRange,
filters,
groupBy,
impersonationId
}: {
dateRange: DateRange;
filters?: Filter[];
groupBy?: GroupBy;
impersonationId: string;
}): Promise<InvestmentItem[]> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const activities = await this.orderService.getOrders({
filters,
userId,
types: ['DIVIDEND'],
userCurrency: this.request.user.Settings.settings.baseCurrency
@ -248,10 +251,12 @@ export class PortfolioService {
public async getInvestments({
dateRange,
filters,
groupBy,
impersonationId
}: {
dateRange: DateRange;
filters?: Filter[];
groupBy?: GroupBy;
impersonationId: string;
}): Promise<InvestmentItem[]> {
@ -259,6 +264,7 @@ export class PortfolioService {
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
filters,
userId,
includeDrafts: true
});
@ -343,11 +349,13 @@ export class PortfolioService {
public async getChart({
dateRange = 'max',
filters,
impersonationId,
userCurrency,
userId
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userCurrency: string;
userId: string;
@ -356,6 +364,7 @@ export class PortfolioService {
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
filters,
userId
});
@ -397,15 +406,15 @@ export class PortfolioService {
}
public async getDetails({
impersonationId,
dateRange = 'max',
filters,
impersonationId,
userId,
withExcludedAccounts = false
}: {
impersonationId: string;
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userId: string;
withExcludedAccounts?: boolean;
}): Promise<PortfolioDetails & { hasErrors: boolean }> {
@ -850,14 +859,20 @@ export class PortfolioService {
}
}
public async getPositions(
aImpersonationId: string,
aDateRange: DateRange = 'max'
): Promise<{ hasErrors: boolean; positions: Position[] }> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
public async getPositions({
dateRange = 'max',
filters,
impersonationId
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
}): Promise<{ hasErrors: boolean; positions: Position[] }> {
const userId = await this.getUserId(impersonationId, this.request.user.id);
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
filters,
userId
});
@ -877,7 +892,7 @@ export class PortfolioService {
portfolioCalculator.setTransactionPoints(transactionPoints);
const portfolioStart = parseDate(transactionPoints[0].date);
const startDate = this.getStartDate(aDateRange, portfolioStart);
const startDate = this.getStartDate(dateRange, portfolioStart);
const currentPositions = await portfolioCalculator.getCurrentPositions(
startDate
);
@ -928,10 +943,12 @@ export class PortfolioService {
public async getPerformance({
dateRange = 'max',
filters,
impersonationId,
userId
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userId: string;
}): Promise<PortfolioPerformanceResponse> {
@ -941,6 +958,7 @@ export class PortfolioService {
const { portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
filters,
userId
});
@ -996,6 +1014,7 @@ export class PortfolioService {
const historicalDataContainer = await this.getChart({
dateRange,
filters,
impersonationId,
userCurrency,
userId

@ -8,6 +8,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import {
Filter,
HistoricalDataItem,
Position,
User
@ -15,12 +16,13 @@ import {
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types';
import { DataSource, SymbolProfile } from '@prisma/client';
import { translate } from '@ghostfolio/ui/i18n';
import { AssetClass, DataSource, SymbolProfile } from '@prisma/client';
import { differenceInDays } from 'date-fns';
import { sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'page' },
@ -29,6 +31,8 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './analysis-page.html'
})
export class AnalysisPageComponent implements OnDestroy, OnInit {
public activeFilters: Filter[] = [];
public allFilters: Filter[];
public benchmarkDataItems: HistoricalDataItem[] = [];
public benchmarks: Partial<SymbolProfile>[];
public bottom3: Position[];
@ -37,6 +41,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public deviceType: string;
public dividendsByMonth: InvestmentItem[];
public dividendTimelineDataLabel = $localize`Dividend`;
public filters$ = new Subject<Filter[]>();
public firstOrderDate: Date;
public hasImpersonationId: boolean;
public investments: InvestmentItem[];
@ -50,6 +55,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
];
public performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[];
public placeholder = '';
public portfolioEvolutionDataLabel = $localize`Deposit`;
public top3: Position[];
public user: User;
@ -95,12 +101,63 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.hasImpersonationId = !!aId;
});
this.filters$
.pipe(
distinctUntilChanged(),
map((filters) => {
this.activeFilters = filters;
this.placeholder =
this.activeFilters.length <= 0
? $localize`Filter by account or tag...`
: '';
this.update();
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => {});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
const accountFilters: Filter[] = this.user.accounts
.filter(({ accountType }) => {
return accountType === 'SECURITIES';
})
.map(({ id, name }) => {
return {
id,
label: name,
type: 'ACCOUNT'
};
});
const assetClassFilters: Filter[] = [];
for (const assetClass of Object.keys(AssetClass)) {
assetClassFilters.push({
id: assetClass,
label: translate(assetClass),
type: 'ASSET_CLASS'
});
}
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => {
return {
id,
label: name,
type: 'TAG'
};
});
this.allFilters = [
...accountFilters,
...assetClassFilters,
...tagFilters
];
this.update();
}
});
@ -198,6 +255,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService
.fetchPortfolioPerformance({
filters: this.activeFilters,
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
@ -235,6 +293,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService
.fetchDividends({
filters: this.activeFilters,
groupBy: 'month',
range: this.user?.settings?.dateRange
})
@ -247,6 +306,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService
.fetchInvestments({
filters: this.activeFilters,
groupBy: 'month',
range: this.user?.settings?.dateRange
})
@ -258,7 +318,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
});
this.dataService
.fetchPositions({ range: this.user?.settings?.dateRange })
.fetchPositions({
filters: this.activeFilters,
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ positions }) => {
const positionsSorted = sortBy(

@ -8,6 +8,12 @@
(change)="onChangeDateRange($event.value)"
></gf-toggle>
</div>
<gf-activities-filter
[allFilters]="allFilters"
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[placeholder]="placeholder"
(valueChanged)="filters$.next($event)"
></gf-activities-filter>
<div class="mb-5 row">
<div class="col-lg">
<gf-benchmark-comparator

@ -4,6 +4,7 @@ import { MatCardModule } from '@angular/material/card';
import { GfBenchmarkComparatorModule } from '@ghostfolio/client/components/benchmark-comparator/benchmark-comparator.module';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -16,6 +17,7 @@ import { AnalysisPageComponent } from './analysis-page.component';
imports: [
AnalysisPageRoutingModule,
CommonModule,
GfActivitiesFilterModule,
GfBenchmarkComparatorModule,
GfInvestmentChartModule,
GfPremiumIndicatorModule,

@ -102,14 +102,20 @@ export class DataService {
}
public fetchDividends({
filters,
groupBy = 'month',
range
}: {
filters?: Filter[];
groupBy?: GroupBy;
range: DateRange;
}) {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('groupBy', groupBy);
params = params.append('range', range);
return this.http.get<PortfolioDividends>('/api/v1/portfolio/dividends', {
params: { groupBy, range }
params
});
}
@ -191,15 +197,21 @@ export class DataService {
}
public fetchInvestments({
filters,
groupBy = 'month',
range
}: {
filters?: Filter[];
groupBy?: GroupBy;
range: DateRange;
}) {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('groupBy', groupBy);
params = params.append('range', range);
return this.http.get<PortfolioInvestments>(
'/api/v1/portfolio/investments',
{ params: { groupBy, range } }
{ params }
);
}
@ -224,12 +236,17 @@ export class DataService {
}
public fetchPositions({
filters,
range
}: {
filters?: Filter[];
range: DateRange;
}): Observable<PortfolioPositions> {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range);
return this.http.get<PortfolioPositions>('/api/v1/portfolio/positions', {
params: { range }
params
});
}
@ -284,12 +301,19 @@ export class DataService {
}
public fetchPortfolioPerformance({
filters,
range
}: {
filters?: Filter[];
range: DateRange;
}): Observable<PortfolioPerformanceResponse> {
let params = this.buildFiltersAsQueryParams({ filters });
params = params.append('range', range);
return this.http
.get<any>(`/api/v2/portfolio/performance`, { params: { range } })
.get<any>(`/api/v2/portfolio/performance`, {
params
})
.pipe(
map((response) => {
if (response.firstOrderDate) {

Loading…
Cancel
Save