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

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 { parseDate } from '@ghostfolio/common/helper';
import {
Filter,
PortfolioChart,
PortfolioDetails,
PortfolioInvestments,
@ -19,7 +20,7 @@ import {
PortfolioReport,
PortfolioSummary
} from '@ghostfolio/common/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
import {
Controller,
Get,
@ -105,17 +106,36 @@ export class PortfolioController {
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async getDetails(
@Headers('impersonation-id') impersonationId: string,
@Query('range') range,
@Query('tags') tags?: string
@Query('accounts') filterByAccounts?: string,
@Query('range') range?: DateRange,
@Query('tags') filterByTags?: string
): Promise<PortfolioDetails & { hasError: boolean }> {
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 } =
await this.portfolioService.getDetails(
impersonationId,
this.request.user.id,
range,
tags?.split(',')
filters
);
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
@ -163,7 +183,7 @@ export class PortfolioController {
return {
hasError,
accounts: tags ? {} : accounts,
accounts: filters ? {} : accounts,
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 {
Accounts,
Filter,
PortfolioDetails,
PortfolioPerformanceResponse,
PortfolioReport,
@ -309,7 +310,7 @@ export class PortfolioService {
aImpersonationId: string,
aUserId: string,
aDateRange: DateRange = 'max',
tags?: string[]
aFilters?: Filter[]
): Promise<PortfolioDetails & { hasErrors: boolean }> {
const userId = await this.getUserId(aImpersonationId, aUserId);
const user = await this.userService.user({ id: userId });
@ -324,8 +325,8 @@ export class PortfolioService {
const { orders, portfolioOrders, transactionPoints } =
await this.getTransactionPoints({
tags,
userId
userId,
filters: aFilters
});
const portfolioCalculator = new PortfolioCalculator({
@ -448,7 +449,7 @@ export class PortfolioService {
value: totalValue
});
if (tags === undefined) {
if (aFilters === undefined) {
for (const symbol of Object.keys(cashPositions)) {
holdings[symbol] = cashPositions[symbol];
}
@ -1195,12 +1196,12 @@ export class PortfolioService {
}
private async getTransactionPoints({
filters,
includeDrafts = false,
tags,
userId
}: {
filters?: Filter[];
includeDrafts?: boolean;
tags?: string[];
userId: string;
}): Promise<{
transactionPoints: TransactionPoint[];
@ -1210,8 +1211,8 @@ export class PortfolioService {
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
const orders = await this.orderService.getOrders({
filters,
includeDrafts,
tags,
userCurrency,
userId,
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) {
this.user = state.user;
this.allFilters = this.user.tags.map((tag) => {
const accountFilters: Filter[] = this.user.accounts.map(
({ id, name }) => {
return {
id: tag.id,
label: tag.name,
id: id,
label: name,
type: 'account'
};
}
);
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => {
return {
id,
label: name,
type: 'tag'
};
});
this.allFilters = [...accountFilters, ...tagFilters];
this.changeDetectorRef.markForCheck();
}
});

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

@ -5,7 +5,6 @@
<gf-activities-filter
[allFilters]="allFilters"
[isLoading]="isLoading"
[ngClass]="{ 'd-none': allFilters.length <= 0 }"
[placeholder]="placeholder"
(valueChanged)="filters$.next($event)"
></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 { DataSource, Order as OrderModel } from '@prisma/client';
import { parseISO } from 'date-fns';
import { cloneDeep } from 'lodash';
import { cloneDeep, groupBy } from 'lodash';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@ -187,18 +187,35 @@ export class DataService {
let params = new HttpParams();
if (filters?.length > 0) {
const { account: filtersByAccount, tag: filtersByTag } = groupBy(
filters,
(filter) => {
return filter.type;
}
);
if (filtersByAccount) {
params = params.append(
'tags',
filters
.filter((filter) => {
return filter.type === 'tag';
'accounts',
filtersByAccount
.map(({ id }) => {
return id;
})
.map((filter) => {
return filter.id;
.join(',')
);
}
if (filtersByTag) {
params = params.append(
'tags',
filtersByTag
.map(({ id }) => {
return id;
})
.join(',')
);
}
}
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
params

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

@ -1,5 +1,5 @@
export interface Filter {
id: string;
label: string;
type: 'tag';
label?: string;
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()
.startsWith(currentFilter?.toLowerCase());
})
.sort((a, b) => a.label.localeCompare(b.label))
);
}
});
@ -109,12 +110,14 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
private updateFilter() {
this.filters$.next(
this.allFilters.filter((filter) => {
this.allFilters
.filter((filter) => {
// Filter selected filters
return !this.selectedFilters.some((selectedFilter) => {
return selectedFilter.id === filter.id;
});
})
.sort((a, b) => a.label.localeCompare(b.label))
);
// Emit an array with a new reference

Loading…
Cancel
Save