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({
...assetProfileData,
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';
import { MarketDataPreset } from '@ghostfolio/common/types';
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 { groupBy } from 'lodash';
@ -204,7 +210,8 @@ export class AdminService {
},
scraperConfiguration: true,
sectors: true,
symbol: true
symbol: true,
tags: true
}
}),
this.prismaService.symbolProfile.count({ where })
@ -222,7 +229,8 @@ export class AdminService {
name,
Order,
sectors,
symbol
symbol,
tags
}) => {
const countriesCount = countries ? Object.keys(countries).length : 0;
const marketDataItemCount =
@ -246,7 +254,8 @@ export class AdminService {
marketDataItemCount,
sectorsCount,
activitiesCount: _count.Order,
date: Order?.[0]?.date
date: Order?.[0]?.date,
tags
};
}
);
@ -378,7 +387,8 @@ export class AdminService {
countriesCount: 0,
currency: symbol.replace(DEFAULT_CURRENCY, ''),
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 {
IsArray,
IsEnum,
@ -26,7 +26,7 @@ export class UpdateAssetProfileDto {
@IsArray()
@IsOptional()
tags?: string[];
tags?: Tag[];
@IsObject()
@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 { Sector } from '@ghostfolio/common/interfaces/sector.interface';
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';
@Injectable()
@ -49,6 +54,7 @@ export class SymbolProfileService {
select: { date: true },
take: 1
},
tags: true,
SymbolProfileOverrides: true
},
where: {
@ -72,7 +78,8 @@ export class SymbolProfileService {
_count: {
select: { Order: true }
},
SymbolProfileOverrides: true
SymbolProfileOverrides: true,
tags: true
},
where: {
id: {
@ -116,6 +123,7 @@ export class SymbolProfileService {
Order?: {
date: Date;
}[];
tags?: Tag[];
SymbolProfileOverrides: SymbolProfileOverrides;
})[]
): EnhancedSymbolProfile[] {
@ -129,7 +137,8 @@ export class SymbolProfileService {
dateOfFirstActivity: <Date>undefined,
scraperConfiguration: this.getScraperConfiguration(symbolProfile),
sectors: this.getSectors(symbolProfile),
symbolMapping: this.getSymbolMapping(symbolProfile)
symbolMapping: this.getSymbolMapping(symbolProfile),
tags: symbolProfile?.tags
};
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,
ChangeDetectorRef,
Component,
ElementRef,
Inject,
OnDestroy,
OnInit
OnInit,
ViewChild
} from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
@ -21,7 +23,8 @@ import {
AssetClass,
AssetSubClass,
MarketData,
SymbolProfile
SymbolProfile,
Tag
} from '@prisma/client';
import { format } from 'date-fns';
import { parse as csvToJson } from 'papaparse';
@ -29,6 +32,8 @@ import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { AssetProfileDialogParams } from './interfaces/interfaces';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
@Component({
host: { class: 'd-flex flex-column h-100' },
@ -38,6 +43,8 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
styleUrls: ['./asset-profile-dialog.component.scss']
})
export class AssetProfileDialog implements OnDestroy, OnInit {
@ViewChild('tagInput') tagInput: ElementRef<HTMLInputElement>;
public separatorKeysCodes: number[] = [ENTER, COMMA];
public assetProfileClass: string;
public assetClasses = Object.keys(AssetClass).map((assetClass) => {
return { id: assetClass, label: translate(assetClass) };
@ -51,7 +58,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
assetSubClass: new FormControl<AssetSubClass>(undefined),
comment: '',
name: ['', Validators.required],
tags: new FormControl<string[]>(undefined),
tags: new FormControl<Tag[]>(undefined),
scraperConfiguration: '',
symbolMapping: ''
});
@ -67,7 +74,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
[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(
new Date(),
@ -94,6 +101,18 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
this.historicalDataAsCsvString =
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
.fetchAdminMarketDataBySymbol({
dataSource: this.data.dataSource,
@ -229,7 +248,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
assetSubClass: this.assetProfileForm.controls['assetSubClass'].value,
comment: this.assetProfileForm.controls['comment'].value ?? null,
name: this.assetProfileForm.controls['name'].value,
tag: this.assetProfileForm.controls['tags'].value,
tags: this.assetProfileForm.controls['tags'].value,
scraperConfiguration,
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() {
this.unsubscribeSubject.next();
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">
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Tags</mat-label>
<mat-select formControlName="tags" multiple>
<mat-option [value]="null"></mat-option>
<mat-option *ngFor="let tag of HoldingTags" [value]="tag.id"
>{{ tag.label }}</mat-option
<mat-chip-grid #tagsChipList>
<mat-chip-row
*ngFor="let tag of assetProfileForm.controls['tags']?.value"
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>
</div>
<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 { AssetProfileDialog } from './asset-profile-dialog.component';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatChipsModule } from '@angular/material/chips';
@NgModule({
declarations: [AssetProfileDialog],
@ -21,6 +23,8 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
FormsModule,
GfAdminMarketDataDetailModule,
GfPortfolioProportionChartModule,
MatAutocompleteModule,
MatChipsModule,
GfValueModule,
MatButtonModule,
MatCheckboxModule,

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

@ -209,7 +209,8 @@ export class AdminService {
name,
scraperConfiguration,
symbol,
symbolMapping
symbolMapping,
tags
}: UniqueAsset & UpdateAssetProfileDto) {
return this.http.patch<EnhancedSymbolProfile>(
`/api/v1/admin/profile-data/${dataSource}/${symbol}`,
@ -219,7 +220,8 @@ export class AdminService {
comment,
name,
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 {
count: number;
@ -16,4 +16,5 @@ export interface AdminMarketDataItem {
name: string;
sectorsCount: number;
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 { ScraperConfiguration } from './scraper-configuration.interface';
@ -23,5 +23,5 @@ export interface EnhancedSymbolProfile {
symbolMapping?: { [key: string]: string };
updatedAt: Date;
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?
url String?
Order Order[]
tags Tag[]
SymbolProfileOverrides SymbolProfileOverrides?
@@unique([dataSource, symbol])
@ -176,6 +177,7 @@ model Tag {
id String @id @default(uuid())
name String @unique
orders Order[]
symbolProfile SymbolProfile[]
}
model User {

Loading…
Cancel
Save