Browse Source

Further Work on Holdings on tags

pull/5027/head
Dan 2 years ago
parent
commit
fd62153407
  1. 15
      apps/api/src/app/admin/admin.controller.ts
  2. 20
      apps/api/src/app/admin/admin.service.ts
  3. 4
      apps/api/src/app/admin/update-asset-profile.dto.ts
  4. 15
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  5. 47
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  6. 30
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  7. 4
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts
  8. 6
      apps/client/src/app/services/admin.service.ts
  9. 3
      libs/common/src/lib/interfaces/admin-market-data.interface.ts
  10. 4
      libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts
  11. 20
      prisma/migrations/20231106132716_added_tag_to_symbol_profile/migration.sql
  12. 2
      prisma/schema.prisma

15
apps/api/src/app/admin/admin.controller.ts

@ -448,10 +448,23 @@ export class AdminController {
); );
} }
await this.adminService.patchAssetProfileData({
dataSource,
symbol,
tags: {
set: []
}
});
return this.adminService.patchAssetProfileData({ return this.adminService.patchAssetProfileData({
...assetProfileData, ...assetProfileData,
dataSource, dataSource,
symbol symbol,
tags: {
connect: assetProfileData.tags?.map(({ id }) => {
return { id };
})
}
}); });
} }

20
apps/api/src/app/admin/admin.service.ts

@ -23,7 +23,13 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { MarketDataPreset } from '@ghostfolio/common/types'; import { MarketDataPreset } from '@ghostfolio/common/types';
import { BadRequestException, Injectable } from '@nestjs/common'; import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client'; import {
AssetSubClass,
Prisma,
Property,
SymbolProfile,
Tag
} from '@prisma/client';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
@ -204,7 +210,8 @@ export class AdminService {
}, },
scraperConfiguration: true, scraperConfiguration: true,
sectors: true, sectors: true,
symbol: true symbol: true,
tags: true
} }
}), }),
this.prismaService.symbolProfile.count({ where }) this.prismaService.symbolProfile.count({ where })
@ -222,7 +229,8 @@ export class AdminService {
name, name,
Order, Order,
sectors, sectors,
symbol symbol,
tags
}) => { }) => {
const countriesCount = countries ? Object.keys(countries).length : 0; const countriesCount = countries ? Object.keys(countries).length : 0;
const marketDataItemCount = const marketDataItemCount =
@ -246,7 +254,8 @@ export class AdminService {
marketDataItemCount, marketDataItemCount,
sectorsCount, sectorsCount,
activitiesCount: _count.Order, activitiesCount: _count.Order,
date: Order?.[0]?.date date: Order?.[0]?.date,
tags
}; };
} }
); );
@ -378,7 +387,8 @@ export class AdminService {
countriesCount: 0, countriesCount: 0,
currency: symbol.replace(DEFAULT_CURRENCY, ''), currency: symbol.replace(DEFAULT_CURRENCY, ''),
name: symbol, name: symbol,
sectorsCount: 0 sectorsCount: 0,
tags: []
}; };
}); });

4
apps/api/src/app/admin/update-asset-profile.dto.ts

@ -1,4 +1,4 @@
import { AssetClass, AssetSubClass, Prisma } from '@prisma/client'; import { AssetClass, AssetSubClass, Prisma, Tag } from '@prisma/client';
import { import {
IsArray, IsArray,
IsEnum, IsEnum,
@ -26,7 +26,7 @@ export class UpdateAssetProfileDto {
@IsArray() @IsArray()
@IsOptional() @IsOptional()
tags?: string[]; tags?: Tag[];
@IsObject() @IsObject()
@IsOptional() @IsOptional()

15
apps/api/src/services/symbol-profile/symbol-profile.service.ts

@ -8,7 +8,12 @@ import {
import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface';
import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, SymbolProfile, SymbolProfileOverrides } from '@prisma/client'; import {
Prisma,
SymbolProfile,
SymbolProfileOverrides,
Tag
} from '@prisma/client';
import { continents, countries } from 'countries-list'; import { continents, countries } from 'countries-list';
@Injectable() @Injectable()
@ -49,6 +54,7 @@ export class SymbolProfileService {
select: { date: true }, select: { date: true },
take: 1 take: 1
}, },
tags: true,
SymbolProfileOverrides: true SymbolProfileOverrides: true
}, },
where: { where: {
@ -72,7 +78,8 @@ export class SymbolProfileService {
_count: { _count: {
select: { Order: true } select: { Order: true }
}, },
SymbolProfileOverrides: true SymbolProfileOverrides: true,
tags: true
}, },
where: { where: {
id: { id: {
@ -116,6 +123,7 @@ export class SymbolProfileService {
Order?: { Order?: {
date: Date; date: Date;
}[]; }[];
tags?: Tag[];
SymbolProfileOverrides: SymbolProfileOverrides; SymbolProfileOverrides: SymbolProfileOverrides;
})[] })[]
): EnhancedSymbolProfile[] { ): EnhancedSymbolProfile[] {
@ -129,7 +137,8 @@ export class SymbolProfileService {
dateOfFirstActivity: <Date>undefined, dateOfFirstActivity: <Date>undefined,
scraperConfiguration: this.getScraperConfiguration(symbolProfile), scraperConfiguration: this.getScraperConfiguration(symbolProfile),
sectors: this.getSectors(symbolProfile), sectors: this.getSectors(symbolProfile),
symbolMapping: this.getSymbolMapping(symbolProfile) symbolMapping: this.getSymbolMapping(symbolProfile),
tags: symbolProfile?.tags
}; };
item.activitiesCount = symbolProfile._count.Order; item.activitiesCount = symbolProfile._count.Order;

47
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -2,9 +2,11 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef,
Inject, Inject,
OnDestroy, OnDestroy,
OnInit OnInit,
ViewChild
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@ -21,7 +23,8 @@ import {
AssetClass, AssetClass,
AssetSubClass, AssetSubClass,
MarketData, MarketData,
SymbolProfile SymbolProfile,
Tag
} from '@prisma/client'; } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { parse as csvToJson } from 'papaparse'; import { parse as csvToJson } from 'papaparse';
@ -29,6 +32,8 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { AssetProfileDialogParams } from './interfaces/interfaces'; import { AssetProfileDialogParams } from './interfaces/interfaces';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
@Component({ @Component({
host: { class: 'd-flex flex-column h-100' }, host: { class: 'd-flex flex-column h-100' },
@ -38,6 +43,8 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
styleUrls: ['./asset-profile-dialog.component.scss'] styleUrls: ['./asset-profile-dialog.component.scss']
}) })
export class AssetProfileDialog implements OnDestroy, OnInit { export class AssetProfileDialog implements OnDestroy, OnInit {
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
public separatorKeysCodes: number[] = [ENTER, COMMA];
public assetProfileClass: string; public assetProfileClass: string;
public assetClasses = Object.keys(AssetClass).map((assetClass) => { public assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { id: assetClass, label: translate(assetClass) }; return { id: assetClass, label: translate(assetClass) };
@ -51,7 +58,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
assetSubClass: new FormControl<AssetSubClass>(undefined), assetSubClass: new FormControl<AssetSubClass>(undefined),
comment: '', comment: '',
name: ['', Validators.required], name: ['', Validators.required],
tags: new FormControl<string[]>(undefined), tags: new FormControl<Tag[]>(undefined),
scraperConfiguration: '', scraperConfiguration: '',
symbolMapping: '' symbolMapping: ''
}); });
@ -67,7 +74,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
[name: string]: { name: string; value: number }; [name: string]: { name: string; value: number };
}; };
public HoldingTags: { id: string; label: string }[]; public HoldingTags: { id: string; name: string }[];
private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format( private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format(
new Date(), new Date(),
@ -94,6 +101,18 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
this.historicalDataAsCsvString = this.historicalDataAsCsvString =
AssetProfileDialog.HISTORICAL_DATA_TEMPLATE; AssetProfileDialog.HISTORICAL_DATA_TEMPLATE;
this.adminService
.fetchTags()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((tags) => {
this.HoldingTags = tags.map(({ id, name }) => {
return { id, name };
});
this.dataService.updateInfo();
this.changeDetectorRef.markForCheck();
});
this.adminService this.adminService
.fetchAdminMarketDataBySymbol({ .fetchAdminMarketDataBySymbol({
dataSource: this.data.dataSource, dataSource: this.data.dataSource,
@ -229,7 +248,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
assetSubClass: this.assetProfileForm.controls['assetSubClass'].value, assetSubClass: this.assetProfileForm.controls['assetSubClass'].value,
comment: this.assetProfileForm.controls['comment'].value ?? null, comment: this.assetProfileForm.controls['comment'].value ?? null,
name: this.assetProfileForm.controls['name'].value, name: this.assetProfileForm.controls['name'].value,
tag: this.assetProfileForm.controls['tags'].value, tags: this.assetProfileForm.controls['tags'].value,
scraperConfiguration, scraperConfiguration,
symbolMapping symbolMapping
}; };
@ -258,6 +277,24 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}); });
} }
public onRemoveTag(aTag: Tag) {
this.assetProfileForm.controls['tags'].setValue(
this.assetProfileForm.controls['tags'].value.filter(({ id }) => {
return id !== aTag.id;
})
);
}
public onAddTag(event: MatAutocompleteSelectedEvent) {
this.assetProfileForm.controls['tags'].setValue([
...(this.assetProfileForm.controls['tags'].value ?? []),
this.HoldingTags.find(({ id }) => {
return id === event.option.value;
})
]);
this.tagInput.nativeElement.value = '';
}
public ngOnDestroy() { public ngOnDestroy() {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();

30
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -213,12 +213,32 @@
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Tags</mat-label> <mat-label i18n>Tags</mat-label>
<mat-select formControlName="tags" multiple> <mat-chip-grid #tagsChipList>
<mat-option [value]="null"></mat-option> <mat-chip-row
<mat-option *ngFor="let tag of HoldingTags" [value]="tag.id" *ngFor="let tag of assetProfileForm.controls['tags']?.value"
>{{ tag.label }}</mat-option matChipRemove
[removable]="true"
(removed)="onRemoveTag(tag)"
> >
</mat-select> {{ tag.name }}
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
</mat-chip-row>
<input
#tagInput
name="close-outline"
[matAutocomplete]="autocompleteTags"
[matChipInputFor]="tagsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
/>
</mat-chip-grid>
<mat-autocomplete
#autocompleteTags="matAutocomplete"
(optionSelected)="onAddTag($event)"
>
<mat-option *ngFor="let tag of HoldingTags" [value]="tag.id">
{{ tag.name }}
</mat-option>
</mat-autocomplete>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="d-flex my-3"> <div class="d-flex my-3">

4
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts

@ -13,6 +13,8 @@ import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-propo
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { AssetProfileDialog } from './asset-profile-dialog.component'; import { AssetProfileDialog } from './asset-profile-dialog.component';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
@NgModule({ @NgModule({
declarations: [AssetProfileDialog], declarations: [AssetProfileDialog],
@ -21,6 +23,8 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
FormsModule, FormsModule,
GfAdminMarketDataDetailModule, GfAdminMarketDataDetailModule,
GfPortfolioProportionChartModule, GfPortfolioProportionChartModule,
MatAutocompleteModule,
MatChipsModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule, MatCheckboxModule,

6
apps/client/src/app/services/admin.service.ts

@ -209,7 +209,8 @@ export class AdminService {
name, name,
scraperConfiguration, scraperConfiguration,
symbol, symbol,
symbolMapping symbolMapping,
tags
}: UniqueAsset & UpdateAssetProfileDto) { }: UniqueAsset & UpdateAssetProfileDto) {
return this.http.patch<EnhancedSymbolProfile>( return this.http.patch<EnhancedSymbolProfile>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}`, `/api/v1/admin/profile-data/${dataSource}/${symbol}`,
@ -219,7 +220,8 @@ export class AdminService {
comment, comment,
name, name,
scraperConfiguration, scraperConfiguration,
symbolMapping symbolMapping,
tags
} }
); );
} }

3
libs/common/src/lib/interfaces/admin-market-data.interface.ts

@ -1,4 +1,4 @@
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client';
export interface AdminMarketData { export interface AdminMarketData {
count: number; count: number;
@ -16,4 +16,5 @@ export interface AdminMarketDataItem {
name: string; name: string;
sectorsCount: number; sectorsCount: number;
symbol: string; symbol: string;
tags: Tag[];
} }

4
libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts

@ -1,4 +1,4 @@
import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client';
import { Country } from './country.interface'; import { Country } from './country.interface';
import { ScraperConfiguration } from './scraper-configuration.interface'; import { ScraperConfiguration } from './scraper-configuration.interface';
@ -23,5 +23,5 @@ export interface EnhancedSymbolProfile {
symbolMapping?: { [key: string]: string }; symbolMapping?: { [key: string]: string };
updatedAt: Date; updatedAt: Date;
url?: string; url?: string;
tags?: string[]; tags?: Tag[];
} }

20
prisma/migrations/20231106132716_added_tag_to_symbol_profile/migration.sql

@ -0,0 +1,20 @@
-- AlterEnum
ALTER TYPE "Type" ADD VALUE 'STAKE';
-- CreateTable
CREATE TABLE "_SymbolProfileToTag" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "_SymbolProfileToTag_AB_unique" ON "_SymbolProfileToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_SymbolProfileToTag_B_index" ON "_SymbolProfileToTag"("B");
-- AddForeignKey
ALTER TABLE "_SymbolProfileToTag" ADD CONSTRAINT "_SymbolProfileToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "SymbolProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_SymbolProfileToTag" ADD CONSTRAINT "_SymbolProfileToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;

2
prisma/schema.prisma

@ -145,6 +145,7 @@ model SymbolProfile {
symbolMapping Json? symbolMapping Json?
url String? url String?
Order Order[] Order Order[]
tags Tag[]
SymbolProfileOverrides SymbolProfileOverrides? SymbolProfileOverrides SymbolProfileOverrides?
@@unique([dataSource, symbol]) @@unique([dataSource, symbol])
@ -176,6 +177,7 @@ model Tag {
id String @id @default(uuid()) id String @id @default(uuid())
name String @unique name String @unique
orders Order[] orders Order[]
symbolProfile SymbolProfile[]
} }
model User { model User {

Loading…
Cancel
Save