Browse Source

Filter by accounts

pull/883/head
Thomas 3 years ago
parent
commit
cfdf75a54f
  1. 29
      apps/api/src/app/order/order.service.ts
  2. 30
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 15
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 18
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  5. 1
      apps/client/src/app/pages/portfolio/allocations/allocations-page.html
  6. 31
      apps/client/src/app/services/data.service.ts
  7. 4
      libs/common/src/lib/interfaces/filter.interface.ts
  8. 5
      libs/ui/src/lib/activities-filter/activities-filter.component.ts

29
apps/api/src/app/order/order.service.ts

@ -4,6 +4,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { Filter } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
@ -16,6 +17,7 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { endOfToday, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
import { groupBy } from 'lodash';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { Activity } from './interfaces/activities.interface'; import { Activity } from './interfaces/activities.interface';
@ -166,31 +168,44 @@ export class OrderService {
} }
public async getOrders({ public async getOrders({
filters,
includeDrafts = false, includeDrafts = false,
tags,
types, types,
userCurrency, userCurrency,
userId userId
}: { }: {
filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
tags?: string[];
types?: TypeOfOrder[]; types?: TypeOfOrder[];
userCurrency: string; userCurrency: string;
userId: string; userId: string;
}): Promise<Activity[]> { }): Promise<Activity[]> {
const where: Prisma.OrderWhereInput = { userId }; const where: Prisma.OrderWhereInput = { userId };
const { account: filtersByAccount, tag: filtersByTag } = groupBy(
filters,
(filter) => {
return filter.type;
}
);
if (filtersByAccount?.length > 0) {
where.accountId = {
in: filtersByAccount.map(({ id }) => {
return id;
})
};
}
if (includeDrafts === false) { if (includeDrafts === false) {
where.isDraft = false; where.isDraft = false;
} }
if (tags?.length > 0) { if (filtersByTag?.length > 0) {
where.tags = { where.tags = {
some: { some: {
OR: tags.map((tag) => { OR: filtersByTag.map(({ id }) => {
return { return { id };
id: tag
};
}) })
} }
}; };

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

@ -11,6 +11,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { baseCurrency } from '@ghostfolio/common/config'; import { baseCurrency } from '@ghostfolio/common/config';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { import {
Filter,
PortfolioChart, PortfolioChart,
PortfolioDetails, PortfolioDetails,
PortfolioInvestments, PortfolioInvestments,
@ -19,7 +20,7 @@ import {
PortfolioReport, PortfolioReport,
PortfolioSummary PortfolioSummary
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types'; import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import { import {
Controller, Controller,
Get, Get,
@ -105,17 +106,36 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor) @UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails( public async getDetails(
@Headers('impersonation-id') impersonationId: string, @Headers('impersonation-id') impersonationId: string,
@Query('range') range, @Query('accounts') filterByAccounts?: string,
@Query('tags') tags?: string @Query('range') range?: DateRange,
@Query('tags') filterByTags?: string
): Promise<PortfolioDetails & { hasError: boolean }> { ): Promise<PortfolioDetails & { hasError: boolean }> {
let hasError = false; let hasError = false;
const accountIds = filterByAccounts?.split(',') ?? [];
const tagIds = filterByTags?.split(',') ?? [];
const filters: Filter[] = [
...accountIds.map((accountId) => {
return <Filter>{
id: accountId,
type: 'account'
};
}),
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,
type: 'tag'
};
})
];
const { accounts, holdings, hasErrors } = const { accounts, holdings, hasErrors } =
await this.portfolioService.getDetails( await this.portfolioService.getDetails(
impersonationId, impersonationId,
this.request.user.id, this.request.user.id,
range, range,
tags?.split(',') filters
); );
if (hasErrors || hasNotDefinedValuesInObject(holdings)) { if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
@ -163,7 +183,7 @@ export class PortfolioController {
return { return {
hasError, hasError,
accounts: tags ? {} : accounts, accounts: filters ? {} : accounts,
holdings: isBasicUser ? {} : holdings holdings: isBasicUser ? {} : holdings
}; };
} }

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

@ -29,6 +29,7 @@ import {
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
import { import {
Accounts, Accounts,
Filter,
PortfolioDetails, PortfolioDetails,
PortfolioPerformanceResponse, PortfolioPerformanceResponse,
PortfolioReport, PortfolioReport,
@ -309,7 +310,7 @@ export class PortfolioService {
aImpersonationId: string, aImpersonationId: string,
aUserId: string, aUserId: string,
aDateRange: DateRange = 'max', aDateRange: DateRange = 'max',
tags?: string[] aFilters?: Filter[]
): Promise<PortfolioDetails & { hasErrors: boolean }> { ): Promise<PortfolioDetails & { hasErrors: boolean }> {
const userId = await this.getUserId(aImpersonationId, aUserId); const userId = await this.getUserId(aImpersonationId, aUserId);
const user = await this.userService.user({ id: userId }); const user = await this.userService.user({ id: userId });
@ -324,8 +325,8 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } = const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({ await this.getTransactionPoints({
tags, userId,
userId filters: aFilters
}); });
const portfolioCalculator = new PortfolioCalculator({ const portfolioCalculator = new PortfolioCalculator({
@ -448,7 +449,7 @@ export class PortfolioService {
value: totalValue value: totalValue
}); });
if (tags === undefined) { if (aFilters === undefined) {
for (const symbol of Object.keys(cashPositions)) { for (const symbol of Object.keys(cashPositions)) {
holdings[symbol] = cashPositions[symbol]; holdings[symbol] = cashPositions[symbol];
} }
@ -1195,12 +1196,12 @@ export class PortfolioService {
} }
private async getTransactionPoints({ private async getTransactionPoints({
filters,
includeDrafts = false, includeDrafts = false,
tags,
userId userId
}: { }: {
filters?: Filter[];
includeDrafts?: boolean; includeDrafts?: boolean;
tags?: string[];
userId: string; userId: string;
}): Promise<{ }): Promise<{
transactionPoints: TransactionPoint[]; transactionPoints: TransactionPoint[];
@ -1210,8 +1211,8 @@ export class PortfolioService {
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency; const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const orders = await this.orderService.getOrders({ const orders = await this.orderService.getOrders({
filters,
includeDrafts, includeDrafts,
tags,
userCurrency, userCurrency,
userId, userId,
types: ['BUY', 'SELL'] types: ['BUY', 'SELL']

18
apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts

@ -151,14 +151,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.allFilters = this.user.tags.map((tag) => { const accountFilters: Filter[] = this.user.accounts.map(
({ id, name }) => {
return { return {
id: tag.id, id: id,
label: tag.name, label: name,
type: 'account'
};
}
);
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => {
return {
id,
label: name,
type: 'tag' type: 'tag'
}; };
}); });
this.allFilters = [...accountFilters, ...tagFilters];
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
}); });

1
apps/client/src/app/pages/portfolio/allocations/allocations-page.html

@ -5,7 +5,6 @@
<gf-activities-filter <gf-activities-filter
[allFilters]="allFilters" [allFilters]="allFilters"
[isLoading]="isLoading" [isLoading]="isLoading"
[ngClass]="{ 'd-none': allFilters.length <= 0 }"
[placeholder]="placeholder" [placeholder]="placeholder"
(valueChanged)="filters$.next($event)" (valueChanged)="filters$.next($event)"
></gf-activities-filter> ></gf-activities-filter>

31
apps/client/src/app/services/data.service.ts

@ -34,7 +34,7 @@ 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';
import { parseISO } from 'date-fns'; import { parseISO } from 'date-fns';
import { cloneDeep } from 'lodash'; import { cloneDeep, groupBy } from 'lodash';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@ -187,18 +187,35 @@ export class DataService {
let params = new HttpParams(); let params = new HttpParams();
if (filters?.length > 0) { if (filters?.length > 0) {
const { account: filtersByAccount, tag: filtersByTag } = groupBy(
filters,
(filter) => {
return filter.type;
}
);
if (filtersByAccount) {
params = params.append( params = params.append(
'tags', 'accounts',
filters filtersByAccount
.filter((filter) => { .map(({ id }) => {
return filter.type === 'tag'; return id;
}) })
.map((filter) => { .join(',')
return filter.id; );
}
if (filtersByTag) {
params = params.append(
'tags',
filtersByTag
.map(({ id }) => {
return id;
}) })
.join(',') .join(',')
); );
} }
}
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', { return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
params params

4
libs/common/src/lib/interfaces/filter.interface.ts

@ -1,5 +1,5 @@
export interface Filter { export interface Filter {
id: string; id: string;
label: string; label?: string;
type: 'tag'; type: 'account' | 'tag';
} }

5
libs/ui/src/lib/activities-filter/activities-filter.component.ts

@ -63,6 +63,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
.toLowerCase() .toLowerCase()
.startsWith(currentFilter?.toLowerCase()); .startsWith(currentFilter?.toLowerCase());
}) })
.sort((a, b) => a.label.localeCompare(b.label))
); );
} }
}); });
@ -109,12 +110,14 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
private updateFilter() { private updateFilter() {
this.filters$.next( this.filters$.next(
this.allFilters.filter((filter) => { this.allFilters
.filter((filter) => {
// Filter selected filters // Filter selected filters
return !this.selectedFilters.some((selectedFilter) => { return !this.selectedFilters.some((selectedFilter) => {
return selectedFilter.id === filter.id; return selectedFilter.id === filter.id;
}); });
}) })
.sort((a, b) => a.label.localeCompare(b.label))
); );
// Emit an array with a new reference // Emit an array with a new reference

Loading…
Cancel
Save