Browse Source

Add tags support in accounts

pull/7242/head
Thomas Kaul 1 week ago
parent
commit
5ae3327ba3
  1. 52
      apps/api/src/app/account/account.controller.ts
  2. 91
      apps/api/src/app/account/account.service.ts
  3. 2
      apps/api/src/app/activities/activities.service.ts
  4. 2
      apps/api/src/app/endpoints/tags/tags.controller.ts
  5. 3
      apps/api/src/app/export/export.service.ts
  6. 73
      apps/api/src/app/import/import.service.ts
  7. 9
      apps/api/src/app/portfolio/portfolio.service.ts
  8. 14
      apps/api/src/helper/account.helper.ts
  9. 9
      apps/api/src/services/tag/tag.service.ts
  10. 20
      apps/client/src/app/components/admin-tag/admin-tag.component.html
  11. 1
      apps/client/src/app/components/admin-tag/admin-tag.component.ts
  12. 12
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts
  13. 3
      libs/common/src/lib/dtos/create-account.dto.ts
  14. 3
      libs/common/src/lib/dtos/update-account.dto.ts
  15. 15
      libs/common/src/lib/helper.ts
  16. 2
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  17. 10
      libs/ui/src/lib/accounts-table/accounts-table.component.ts
  18. 12
      libs/ui/src/lib/activities-table/activities-table.component.ts

52
apps/api/src/app/account/account.controller.ts

@ -156,33 +156,31 @@ export class AccountController {
public async createAccount( public async createAccount(
@Body() data: CreateAccountDto @Body() data: CreateAccountDto
): Promise<AccountModel> { ): Promise<AccountModel> {
if (data.platformId) { const { tags: tagIds, ...accountData } = data;
const platformId = data.platformId;
delete data.platformId; if (accountData.platformId) {
const platformId = accountData.platformId;
delete accountData.platformId;
return this.accountService.createAccount( return this.accountService.createAccount(
{ {
...data, ...accountData,
platform: { connect: { id: platformId } }, platform: { connect: { id: platformId } },
tags: data.tags?.map((id) => {
return { id };
}),
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
this.request.user.id this.request.user.id,
tagIds
); );
} else { } else {
delete data.platformId; delete accountData.platformId;
return this.accountService.createAccount( return this.accountService.createAccount(
{ {
...data, ...accountData,
tags: data.tags?.map((id) => {
return { id };
}),
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
this.request.user.id this.request.user.id,
tagIds
); );
} }
} }
@ -259,18 +257,17 @@ export class AccountController {
); );
} }
if (data.platformId) { const { tags: tagIds, ...accountData } = data;
const platformId = data.platformId;
delete data.platformId; if (accountData.platformId) {
const platformId = accountData.platformId;
delete accountData.platformId;
return this.accountService.updateAccount( return this.accountService.updateAccount(
{ {
data: { data: {
...data, ...accountData,
platform: { connect: { id: platformId } }, platform: { connect: { id: platformId } },
tags: data.tags?.map((id) => {
return { id };
}),
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
where: { where: {
@ -280,22 +277,20 @@ export class AccountController {
} }
} }
}, },
this.request.user.id this.request.user.id,
tagIds
); );
} else { } else {
// platformId is null, remove it // platformId is null, remove it
delete data.platformId; delete accountData.platformId;
return this.accountService.updateAccount( return this.accountService.updateAccount(
{ {
data: { data: {
...data, ...accountData,
platform: originalAccount.platformId platform: originalAccount.platformId
? { disconnect: true } ? { disconnect: true }
: undefined, : undefined,
tags: data.tags?.map((id) => {
return { id };
}),
user: { connect: { id: this.request.user.id } } user: { connect: { id: this.request.user.id } }
}, },
where: { where: {
@ -305,7 +300,8 @@ export class AccountController {
} }
} }
}, },
this.request.user.id this.request.user.id,
tagIds
); );
} }
} }

91
apps/api/src/app/account/account.service.ts

@ -6,7 +6,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Filter } from '@ghostfolio/common/interfaces'; import { Filter } from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { HttpException, Injectable } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { import {
Account, Account,
@ -19,6 +19,7 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
import { CashDetails } from './interfaces/cash-details.interface'; import { CashDetails } from './interfaces/cash-details.interface';
@ -74,17 +75,20 @@ export class AccountService {
const { include = {}, skip, take, cursor, where, orderBy } = params; const { include = {}, skip, take, cursor, where, orderBy } = params;
const isBalancesIncluded = !!include.balances; const isBalancesIncluded = !!include.balances;
const isTagsIncluded = !!include.tags;
include.balances = { include.balances = {
orderBy: { date: 'desc' }, orderBy: { date: 'desc' },
...(isBalancesIncluded ? {} : { take: 1 }) ...(isBalancesIncluded ? {} : { take: 1 })
}; };
include.tags = { if (isTagsIncluded) {
include: { include.tags = {
tag: true include: {
} tag: true
}; }
};
}
const accounts = await this.prismaService.account.findMany({ const accounts = await this.prismaService.account.findMany({
cursor, cursor,
@ -99,32 +103,38 @@ export class AccountService {
const result = { const result = {
...account, ...account,
balance: account.balances[0]?.value ?? 0, balance: account.balances[0]?.value ?? 0,
tags: (account.tags as unknown as { tag: Tag }[]).map(({ tag }) => { tags: isTagsIncluded
return tag; ? (account.tags as unknown as { tag: Tag }[]).map(({ tag }) => {
}) return tag;
})
: undefined
}; };
if (!isBalancesIncluded) { if (!isBalancesIncluded) {
delete result.balances; delete result.balances;
} }
if (!isTagsIncluded) {
delete result.tags;
}
return result; return result;
}); });
} }
public async createAccount( public async createAccount(
data: Prisma.AccountCreateInput & { tags?: { id: string }[] }, data: Prisma.AccountCreateInput,
aUserId: string aUserId: string,
tagIds?: string[]
): Promise<Account> { ): Promise<Account> {
const tags = data.tags; await this.validateTagIds(tagIds, aUserId);
delete data.tags;
const account = await this.prismaService.account.create({ const account = await this.prismaService.account.create({
data: { data: {
...data, ...data,
tags: tags tags: tagIds
? { ? {
create: tags.map(({ id: tagId }) => { create: tagIds.map((tagId) => {
return { return {
tag: { connect: { id: tagId } } tag: { connect: { id: tagId } }
}; };
@ -172,7 +182,8 @@ export class AccountService {
const accounts = await this.accounts({ const accounts = await this.accounts({
include: { include: {
activities: true, activities: true,
platform: true platform: true,
tags: true
}, },
orderBy: { name: 'asc' }, orderBy: { name: 'asc' },
where: { userId: aUserId } where: { userId: aUserId }
@ -249,28 +260,21 @@ export class AccountService {
public async updateAccount( public async updateAccount(
params: { params: {
where: Prisma.AccountWhereUniqueInput; where: Prisma.AccountWhereUniqueInput;
data: Prisma.AccountUpdateInput & { tags?: { id: string }[] }; data: Prisma.AccountUpdateInput;
}, },
aUserId: string aUserId: string,
tagIds?: string[]
): Promise<Account> { ): Promise<Account> {
const { data, where } = params; const { data, where } = params;
const tags = data.tags; await this.validateTagIds(tagIds, aUserId);
delete data.tags;
await this.accountBalanceService.createOrUpdateAccountBalance({
accountId: data.id as string,
balance: data.balance as number,
date: format(new Date(), DATE_FORMAT),
userId: aUserId
});
const account = await this.prismaService.account.update({ const account = await this.prismaService.account.update({
data: { data: {
...data, ...data,
tags: tags tags: tagIds
? { ? {
create: tags.map(({ id: tagId }) => { create: tagIds.map((tagId) => {
return { return {
tag: { connect: { id: tagId } } tag: { connect: { id: tagId } }
}; };
@ -282,6 +286,13 @@ export class AccountService {
where where
}); });
await this.accountBalanceService.createOrUpdateAccountBalance({
accountId: account.id,
balance: data.balance as number,
date: format(new Date(), DATE_FORMAT),
userId: aUserId
});
this.eventEmitter.emit( this.eventEmitter.emit(
PortfolioChangedEvent.getName(), PortfolioChangedEvent.getName(),
new PortfolioChangedEvent({ new PortfolioChangedEvent({
@ -329,4 +340,26 @@ export class AccountService {
}); });
} }
} }
private async validateTagIds(tagIds: string[], userId: string) {
if (!tagIds?.length) {
return;
}
const uniqueTagIds = Array.from(new Set(tagIds));
const tagsCount = await this.prismaService.tag.count({
where: {
id: { in: uniqueTagIds },
OR: [{ userId }, { userId: null }]
}
});
if (tagsCount !== uniqueTagIds.length) {
throw new HttpException(
getReasonPhrase(StatusCodes.BAD_REQUEST),
StatusCodes.BAD_REQUEST
);
}
}
} }

2
apps/api/src/app/activities/activities.service.ts

@ -570,7 +570,7 @@ export class ActivitiesService {
]; ];
const andConditions: Prisma.OrderWhereInput[] = []; const andConditions: Prisma.OrderWhereInput[] = [];
const where: Prisma.OrderWhereInput = { AND: andConditions, userId }; const where: Prisma.OrderWhereInput = { userId, AND: andConditions };
if (endDate) { if (endDate) {
andConditions.push({ date: { lte: endDate } }); andConditions.push({ date: { lte: endDate } });

2
apps/api/src/app/endpoints/tags/tags.controller.ts

@ -83,7 +83,7 @@ export class TagsController {
@HasPermission(permissions.readTags) @HasPermission(permissions.readTags)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard)
public async getTags() { public async getTags() {
return this.tagService.getTagsWithActivityCount(); return this.tagService.getTagsWithAccountAndActivityCount();
} }
@HasPermission(permissions.updateTag) @HasPermission(permissions.updateTag)

3
apps/api/src/app/export/export.service.ts

@ -72,7 +72,8 @@ export class ExportService {
where, where,
include: { include: {
balances: true, balances: true,
platform: true platform: true,
tags: true
}, },
orderBy: { orderBy: {
name: 'asc' name: 'asc'

73
apps/api/src/app/import/import.service.ts

@ -10,11 +10,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/queues/data-gathe
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config'; import { DATA_GATHERING_QUEUE_PRIORITY_HIGH } from '@ghostfolio/common/config';
import { import { CreateAssetProfileDto, CreateOrderDto } from '@ghostfolio/common/dtos';
CreateAssetProfileDto,
CreateAccountDto,
CreateOrderDto
} from '@ghostfolio/common/dtos';
import { import {
getAssetProfileIdentifier, getAssetProfileIdentifier,
parseDate parseDate
@ -194,9 +190,12 @@ export class ImportService {
const tagIdMapping: { [oldTagId: string]: string } = {}; const tagIdMapping: { [oldTagId: string]: string } = {};
const userCurrency = user.settings.settings.baseCurrency; const userCurrency = user.settings.settings.baseCurrency;
if (tagsDto?.length) { const existingTagsOfUser =
const existingTagsOfUser = await this.tagService.getTagsForUser(user.id); tagsDto?.length || (!isDryRun && accountsWithBalancesDto?.length)
? await this.tagService.getTagsForUser(user.id)
: [];
if (tagsDto?.length) {
const canCreateOwnTag = hasPermission( const canCreateOwnTag = hasPermission(
user.permissions, user.permissions,
permissions.createOwnTag permissions.createOwnTag
@ -207,7 +206,7 @@ export class ImportService {
return id === tag.id; return id === tag.id;
}); });
if (!existingTagOfUser || existingTagOfUser.userId !== null) { if (!existingTagOfUser) {
if (!canCreateOwnTag) { if (!canCreateOwnTag) {
throw new Error( throw new Error(
`Insufficient permissions to create custom tag ("${tag.name}")` `Insufficient permissions to create custom tag ("${tag.name}")`
@ -233,26 +232,37 @@ export class ImportService {
if (existingTag && oldTagId) { if (existingTag && oldTagId) {
tagIdMapping[oldTagId] = newTag.id; tagIdMapping[oldTagId] = newTag.id;
} }
existingTagsOfUser.push({
id: newTag.id,
isUsed: false,
name: newTag.name,
userId: newTag.userId
});
} }
} }
} }
} }
if (!isDryRun && accountsWithBalancesDto?.length) { if (!isDryRun && accountsWithBalancesDto?.length) {
const [existingAccounts, existingPlatforms, existingTags] = const [existingAccounts, existingPlatforms] = await Promise.all([
await Promise.all([ this.accountService.accounts({
this.accountService.accounts({ where: {
where: { id: {
id: { in: accountsWithBalancesDto.map(({ id }) => {
in: accountsWithBalancesDto.map(({ id }) => { return id;
return id; })
})
}
} }
}), }
this.platformService.getPlatforms(), }),
this.tagService.getTagsForUser(user.id) this.platformService.getPlatforms()
]); ]);
const existingTagIds = new Set(
existingTagsOfUser.map(({ id }) => {
return id;
})
);
for (const accountWithBalances of accountsWithBalancesDto) { for (const accountWithBalances of accountsWithBalancesDto) {
// Check if there is any existing account with the same ID // Check if there is any existing account with the same ID
@ -262,10 +272,7 @@ export class ImportService {
// If there is no account or if the account belongs to a different user then create a new account // If there is no account or if the account belongs to a different user then create a new account
if (!accountWithSameId || accountWithSameId.userId !== user.id) { if (!accountWithSameId || accountWithSameId.userId !== user.id) {
const account: CreateAccountDto = omit( const account = omit(accountWithBalances, ['balances', 'tags']);
accountWithBalances,
'balances'
);
let oldAccountId: string; let oldAccountId: string;
const platformId = account.platformId; const platformId = account.platformId;
@ -277,26 +284,19 @@ export class ImportService {
delete account.id; delete account.id;
} }
const tagIds = (account.tags ?? []) const tagIds = (accountWithBalances.tags ?? [])
.map((tagId) => { .map((tagId) => {
return tagIdMapping[tagId] ?? tagId; return tagIdMapping[tagId] ?? tagId;
}) })
.filter((tagId) => { .filter((tagId) => {
return existingTags.some(({ id }) => { return existingTagIds.has(tagId);
return id === tagId;
});
}); });
let accountObject: Prisma.AccountCreateInput & { let accountObject: Prisma.AccountCreateInput = {
tags?: { id: string }[];
} = {
...account, ...account,
balances: { balances: {
create: accountWithBalances.balances ?? [] create: accountWithBalances.balances ?? []
}, },
tags: tagIds.map((id) => {
return { id };
}),
user: { connect: { id: user.id } } user: { connect: { id: user.id } }
}; };
@ -313,7 +313,8 @@ export class ImportService {
const newAccount = await this.accountService.createAccount( const newAccount = await this.accountService.createAccount(
accountObject, accountObject,
user.id user.id,
tagIds
); );
// Store the new to old account ID mappings for updating activities // Store the new to old account ID mappings for updating activities

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

@ -3,7 +3,6 @@ import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface'; import { CashDetails } from '@ghostfolio/api/app/account/interfaces/cash-details.interface';
import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service';
import { UserService } from '@ghostfolio/api/app/user/user.service'; import { UserService } from '@ghostfolio/api/app/user/user.service';
import { isAccountExcluded } from '@ghostfolio/api/helper/account.helper';
import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper';
import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment';
import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account';
@ -42,6 +41,7 @@ import {
DATE_FORMAT, DATE_FORMAT,
getAssetProfileIdentifier, getAssetProfileIdentifier,
getSum, getSum,
isAccountExcluded,
parseDate parseDate
} from '@ghostfolio/common/helper'; } from '@ghostfolio/common/helper';
import { import {
@ -170,7 +170,8 @@ export class PortfolioService {
where, where,
include: { include: {
activities: { include: { SymbolProfile: true } }, activities: { include: { SymbolProfile: true } },
platform: true platform: true,
tags: true
}, },
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}), }),
@ -2131,7 +2132,7 @@ export class PortfolioService {
currentAccounts = await this.accountService.getAccounts(userId); currentAccounts = await this.accountService.getAccounts(userId);
} else if (filters.length === 1 && filters[0].type === 'ACCOUNT') { } else if (filters.length === 1 && filters[0].type === 'ACCOUNT') {
currentAccounts = await this.accountService.accounts({ currentAccounts = await this.accountService.accounts({
include: { platform: true }, include: { platform: true, tags: true },
where: { id: filters[0].id } where: { id: filters[0].id }
}); });
} else { } else {
@ -2148,7 +2149,7 @@ export class PortfolioService {
); );
currentAccounts = await this.accountService.accounts({ currentAccounts = await this.accountService.accounts({
include: { platform: true }, include: { platform: true, tags: true },
where: { id: { in: accountIds } } where: { id: { in: accountIds } }
}); });
} }

14
apps/api/src/helper/account.helper.ts

@ -1,6 +1,6 @@
import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config';
import { Prisma, Tag } from '@prisma/client'; import { Prisma } from '@prisma/client';
export const WHERE_ACCOUNT_NOT_EXCLUDED: Prisma.AccountWhereInput = { export const WHERE_ACCOUNT_NOT_EXCLUDED: Prisma.AccountWhereInput = {
isExcluded: false, isExcluded: false,
@ -10,15 +10,3 @@ export const WHERE_ACCOUNT_NOT_EXCLUDED: Prisma.AccountWhereInput = {
} }
} }
}; };
export function isAccountExcluded(account: {
isExcluded: boolean;
tags?: Tag[];
}) {
return (
account.isExcluded ||
account.tags?.some(({ id }) => {
return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
}) === true
);
}

9
apps/api/src/services/tag/tag.service.ts

@ -92,20 +92,21 @@ export class TagService {
}); });
} }
public async getTagsWithActivityCount() { public async getTagsWithAccountAndActivityCount() {
const tagsWithOrderCount = await this.prismaService.tag.findMany({ const tagsWithAccountAndOrderCount = await this.prismaService.tag.findMany({
include: { include: {
_count: { _count: {
select: { activities: true } select: { accounts: true, activities: true }
} }
} }
}); });
return tagsWithOrderCount.map(({ _count, id, name, userId }) => { return tagsWithAccountAndOrderCount.map(({ _count, id, name, userId }) => {
return { return {
id, id,
name, name,
userId, userId,
accountCount: _count.accounts,
activityCount: _count.activities activityCount: _count.activities
}; };
}); });

20
apps/client/src/app/components/admin-tag/admin-tag.component.html

@ -35,6 +35,24 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="accounts">
<th
*matHeaderCellDef
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="accountCount"
>
<ng-container i18n>Accounts</ng-container>
</th>
<td *matCellDef="let element" class="px-1 text-right" mat-cell>
<gf-value
class="d-inline-block justify-content-end"
[locale]="locale()"
[value]="element.accountCount"
/>
</td>
</ng-container>
<ng-container matColumnDef="activities"> <ng-container matColumnDef="activities">
<th <th
*matHeaderCellDef *matHeaderCellDef
@ -74,7 +92,7 @@
<hr class="m-0" /> <hr class="m-0" />
<button <button
mat-menu-item mat-menu-item
[disabled]="element.activityCount > 0" [disabled]="element.accountCount > 0 || element.activityCount > 0"
(click)="onDeleteTag(element.id)" (click)="onDeleteTag(element.id)"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">

1
apps/client/src/app/components/admin-tag/admin-tag.component.ts

@ -62,6 +62,7 @@ export class GfAdminTagComponent implements OnInit {
protected readonly displayedColumns = [ protected readonly displayedColumns = [
'name', 'name',
'userId', 'userId',
'accounts',
'activities', 'activities',
'actions' 'actions'
]; ];

12
apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts

@ -206,9 +206,15 @@ export class GfCreateOrUpdateAccountDialogComponent {
isExcluded: this.accountForm.get('isExcluded')?.value, isExcluded: this.accountForm.get('isExcluded')?.value,
name: this.accountForm.get('name')?.value, name: this.accountForm.get('name')?.value,
platformId: this.accountForm.get('platformId')?.value?.id || null, platformId: this.accountForm.get('platformId')?.value?.id || null,
tags: this.accountForm.get('tags')?.value?.map(({ id }: Tag) => { tags: this.accountForm
return id; .get('tags')
}) ?.value?.filter(({ id }: Tag) => {
// Skip tags which have not been created yet
return !!id;
})
.map(({ id }: Tag) => {
return id;
})
}; };
try { try {

3
libs/common/src/lib/dtos/create-account.dto.ts

@ -2,6 +2,7 @@ import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code';
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
ArrayUnique,
IsArray, IsArray,
IsBoolean, IsBoolean,
IsNumber, IsNumber,
@ -40,7 +41,9 @@ export class CreateAccountDto {
@ValidateIf((_object, value) => value !== null) @ValidateIf((_object, value) => value !== null)
platformId: string | null; platformId: string | null;
@ArrayUnique()
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@IsString({ each: true })
tags?: string[]; tags?: string[];
} }

3
libs/common/src/lib/dtos/update-account.dto.ts

@ -2,6 +2,7 @@ import { IsCurrencyCode } from '@ghostfolio/common/validators/is-currency-code';
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
ArrayUnique,
IsArray, IsArray,
IsBoolean, IsBoolean,
IsNumber, IsNumber,
@ -39,7 +40,9 @@ export class UpdateAccountDto {
@ValidateIf((_object, value) => value !== null) @ValidateIf((_object, value) => value !== null)
platformId: string | null; platformId: string | null;
@ArrayUnique()
@IsArray() @IsArray()
@IsOptional() @IsOptional()
@IsString({ each: true })
tags?: string[]; tags?: string[];
} }

15
libs/common/src/lib/helper.ts

@ -40,7 +40,8 @@ import {
DERIVED_CURRENCIES, DERIVED_CURRENCIES,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies, ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks, ghostfolioFearAndGreedIndexSymbolStocks,
ghostfolioScraperApiSymbolPrefix ghostfolioScraperApiSymbolPrefix,
TAG_ID_EXCLUDE_FROM_ANALYSIS
} from './config'; } from './config';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
@ -419,6 +420,18 @@ export function interpolate(template: string, context: any) {
}); });
} }
export function isAccountExcluded(account: {
isExcluded: boolean;
tags?: { id: string }[];
}) {
return (
account.isExcluded ||
account.tags?.some(({ id }) => {
return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
}) === true
);
}
export function isCurrency(aCurrency: string) { export function isCurrency(aCurrency: string) {
if (!aCurrency) { if (!aCurrency) {
return false; return false;

2
libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -33,7 +33,7 @@
mat-cell mat-cell
> >
<div class="d-flex justify-content-center"> <div class="d-flex justify-content-center">
@if (element.isExcluded) { @if (isExcluded(element)) {
<ion-icon name="eye-off-outline" /> <ion-icon name="eye-off-outline" />
} }
</div> </div>

10
libs/ui/src/lib/accounts-table/accounts-table.component.ts

@ -1,5 +1,9 @@
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale, getLowercase } from '@ghostfolio/common/helper'; import {
getLocale,
getLowercase,
isAccountExcluded
} from '@ghostfolio/common/helper';
import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo';
import { NotificationService } from '@ghostfolio/ui/notifications'; import { NotificationService } from '@ghostfolio/ui/notifications';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
@ -137,6 +141,10 @@ export class GfAccountsTableComponent {
}); });
} }
protected isExcluded(account: Account & { tags?: { id: string }[] }) {
return isAccountExcluded(account);
}
protected onDeleteAccount(aId: string) { protected onDeleteAccount(aId: string) {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {

12
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -3,7 +3,7 @@ import {
TAG_ID_EXCLUDE_FROM_ANALYSIS TAG_ID_EXCLUDE_FROM_ANALYSIS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getLocale } from '@ghostfolio/common/helper'; import { getLocale, isAccountExcluded } from '@ghostfolio/common/helper';
import { import {
Activity, Activity,
AssetProfileIdentifier AssetProfileIdentifier
@ -281,12 +281,10 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
public isExcludedFromAnalysis(activity: Activity) { public isExcludedFromAnalysis(activity: Activity) {
return ( return (
activity.account?.isExcluded || (activity.account && isAccountExcluded(activity.account)) ||
[...(activity.tags ?? []), ...(activity.account?.tags ?? [])].some( activity.tags?.some(({ id }) => {
({ id }) => { return id === TAG_ID_EXCLUDE_FROM_ANALYSIS;
return id === TAG_ID_EXCLUDE_FROM_ANALYSIS; }) === true
}
)
); );
} }

Loading…
Cancel
Save