mirror of https://github.com/ghostfolio/ghostfolio
133 changed files with 6501 additions and 7115 deletions
@ -0,0 +1,51 @@ |
|||
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; |
|||
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; |
|||
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; |
|||
import { EnhancedSymbolProfile } from '@ghostfolio/common/interfaces'; |
|||
import { permissions } from '@ghostfolio/common/permissions'; |
|||
import { RequestWithUser } from '@ghostfolio/common/types'; |
|||
|
|||
import { |
|||
Body, |
|||
Controller, |
|||
HttpException, |
|||
Inject, |
|||
Param, |
|||
Patch, |
|||
UseGuards |
|||
} from '@nestjs/common'; |
|||
import { REQUEST } from '@nestjs/core'; |
|||
import { AuthGuard } from '@nestjs/passport'; |
|||
import { DataSource } from '@prisma/client'; |
|||
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; |
|||
|
|||
import { AssetProfilesService } from './asset-profiles.service'; |
|||
|
|||
@Controller('asset-profiles') |
|||
export class AssetProfilesController { |
|||
public constructor( |
|||
private readonly assetProfilesService: AssetProfilesService, |
|||
@Inject(REQUEST) private readonly request: RequestWithUser |
|||
) {} |
|||
|
|||
@HasPermission(permissions.accessAdminControl) |
|||
@Patch(':dataSource/:symbol') |
|||
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) |
|||
public async updateAssetProfileData( |
|||
@Body() assetProfileData: UpdateAssetProfileDataDto, |
|||
@Param('dataSource') dataSource: DataSource, |
|||
@Param('symbol') symbol: string |
|||
): Promise<EnhancedSymbolProfile> { |
|||
if (!this.request.user.settings.settings.isExperimentalFeatures) { |
|||
throw new HttpException( |
|||
getReasonPhrase(StatusCodes.NOT_FOUND), |
|||
StatusCodes.NOT_FOUND |
|||
); |
|||
} |
|||
|
|||
return this.assetProfilesService.updateAssetProfileData( |
|||
{ dataSource, symbol }, |
|||
assetProfileData |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; |
|||
|
|||
import { Module } from '@nestjs/common'; |
|||
|
|||
import { AssetProfilesController } from './asset-profiles.controller'; |
|||
import { AssetProfilesService } from './asset-profiles.service'; |
|||
|
|||
@Module({ |
|||
controllers: [AssetProfilesController], |
|||
imports: [SymbolProfileModule], |
|||
providers: [AssetProfilesService] |
|||
}) |
|||
export class AssetProfilesModule {} |
|||
@ -0,0 +1,90 @@ |
|||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; |
|||
import { UpdateAssetProfileDataDto } from '@ghostfolio/common/dtos'; |
|||
import { |
|||
AssetProfileIdentifier, |
|||
EnhancedSymbolProfile |
|||
} from '@ghostfolio/common/interfaces'; |
|||
|
|||
import { Injectable, NotFoundException } from '@nestjs/common'; |
|||
import { Prisma } from '@prisma/client'; |
|||
|
|||
@Injectable() |
|||
export class AssetProfilesService { |
|||
public constructor( |
|||
private readonly symbolProfileService: SymbolProfileService |
|||
) {} |
|||
|
|||
public async updateAssetProfileData( |
|||
{ dataSource, symbol }: AssetProfileIdentifier, |
|||
assetProfileData: UpdateAssetProfileDataDto |
|||
): Promise<EnhancedSymbolProfile> { |
|||
const notFoundMessage = `Could not find the asset profile for ${symbol} (${dataSource})`; |
|||
|
|||
const data = this.getAssetProfileDataUpdate(assetProfileData); |
|||
|
|||
if (Object.keys(data).length > 0) { |
|||
try { |
|||
await this.symbolProfileService.updateSymbolProfile( |
|||
{ |
|||
dataSource, |
|||
symbol |
|||
}, |
|||
this.symbolProfileService.getAssetProfileUpdateInput( |
|||
{ dataSource, symbol }, |
|||
data |
|||
) |
|||
); |
|||
} catch (error) { |
|||
if ( |
|||
error instanceof Prisma.PrismaClientKnownRequestError && |
|||
error.code === 'P2025' |
|||
) { |
|||
throw new NotFoundException(notFoundMessage); |
|||
} |
|||
|
|||
throw error; |
|||
} |
|||
} |
|||
|
|||
const [assetProfile] = await this.symbolProfileService.getSymbolProfiles([ |
|||
{ |
|||
dataSource, |
|||
symbol |
|||
} |
|||
]); |
|||
|
|||
if (!assetProfile) { |
|||
throw new NotFoundException(notFoundMessage); |
|||
} |
|||
|
|||
return assetProfile; |
|||
} |
|||
|
|||
private getAssetProfileDataUpdate({ |
|||
countries, |
|||
holdings, |
|||
sectors |
|||
}: UpdateAssetProfileDataDto): Pick< |
|||
Prisma.SymbolProfileUpdateInput, |
|||
'countries' | 'holdings' | 'sectors' |
|||
> { |
|||
const data: Pick< |
|||
Prisma.SymbolProfileUpdateInput, |
|||
'countries' | 'holdings' | 'sectors' |
|||
> = {}; |
|||
|
|||
if (countries !== undefined) { |
|||
data.countries = countries as Prisma.JsonArray; |
|||
} |
|||
|
|||
if (holdings !== undefined) { |
|||
data.holdings = holdings as Prisma.JsonArray; |
|||
} |
|||
|
|||
if (sectors !== undefined) { |
|||
data.sectors = sectors as Prisma.JsonArray; |
|||
} |
|||
|
|||
return data; |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
import { countries } from 'countries-list'; |
|||
|
|||
export function getCountryCodeByName({ |
|||
aliases = {}, |
|||
name |
|||
}: { |
|||
aliases?: Record<string, string>; |
|||
name: string; |
|||
}): string { |
|||
for (const [code, country] of Object.entries(countries)) { |
|||
if (country.name === name || country.name === aliases[name]) { |
|||
return code; |
|||
} |
|||
} |
|||
|
|||
return undefined; |
|||
} |
|||
@ -0,0 +1,28 @@ |
|||
import { SECTORS } from '@ghostfolio/common/config'; |
|||
import { SectorName } from '@ghostfolio/common/types'; |
|||
|
|||
import { Logger } from '@nestjs/common'; |
|||
|
|||
export function getSectorName({ |
|||
aliases = {}, |
|||
name |
|||
}: { |
|||
aliases?: Record<string, SectorName>; |
|||
name: string; |
|||
}): SectorName { |
|||
if (aliases[name]) { |
|||
return aliases[name]; |
|||
} |
|||
|
|||
if ((SECTORS as readonly string[]).includes(name)) { |
|||
return name as SectorName; |
|||
} |
|||
|
|||
if (name) { |
|||
const logger = new Logger('getSectorName'); |
|||
|
|||
logger.warn(`Could not map the sector "${name}" to the ontology`); |
|||
} |
|||
|
|||
return 'Other'; |
|||
} |
|||
@ -1,132 +1,142 @@ |
|||
<div mat-dialog-title>{{ data.categoryName }} › {{ data.rule.name }}</div> |
|||
|
|||
<div class="py-3" mat-dialog-content> |
|||
@if ( |
|||
data.rule.configuration.thresholdMin && data.rule.configuration.thresholdMax |
|||
) { |
|||
<div class="w-100"> |
|||
<h6 class="d-flex mb-0"> |
|||
<ng-container i18n>Threshold range</ng-container>: |
|||
<gf-value |
|||
class="ml-1" |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.settings.thresholdMin" |
|||
/> |
|||
<span class="mx-1">-</span> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.settings.thresholdMax" |
|||
/> |
|||
</h6> |
|||
<div class="align-items-center d-flex w-100"> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.min" |
|||
/> |
|||
<mat-slider |
|||
class="flex-grow-1" |
|||
[max]="data.rule.configuration.threshold.max" |
|||
[min]="data.rule.configuration.threshold.min" |
|||
[step]="data.rule.configuration.threshold.step" |
|||
> |
|||
<input matSliderStartThumb [(ngModel)]="data.settings.thresholdMin" /> |
|||
<input matSliderEndThumb [(ngModel)]="data.settings.thresholdMax" /> |
|||
</mat-slider> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.max" |
|||
/> |
|||
<form |
|||
class="d-flex flex-column h-100" |
|||
[formGroup]="settingsForm" |
|||
(ngSubmit)="onSubmit()" |
|||
> |
|||
<div class="py-3" mat-dialog-content> |
|||
@if ( |
|||
data.rule.configuration.thresholdMin && |
|||
data.rule.configuration.thresholdMax |
|||
) { |
|||
<div class="w-100"> |
|||
<h6 class="d-flex mb-0"> |
|||
<ng-container i18n>Threshold range</ng-container>: |
|||
<gf-value |
|||
class="ml-1" |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="settingsForm.get('thresholdMin').value" |
|||
/> |
|||
<span class="mx-1">-</span> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="settingsForm.get('thresholdMax').value" |
|||
/> |
|||
</h6> |
|||
<div class="align-items-center d-flex w-100"> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.min" |
|||
/> |
|||
<mat-slider |
|||
class="flex-grow-1" |
|||
[max]="data.rule.configuration.threshold.max" |
|||
[min]="data.rule.configuration.threshold.min" |
|||
[step]="data.rule.configuration.threshold.step" |
|||
> |
|||
<input formControlName="thresholdMin" matSliderStartThumb /> |
|||
<input formControlName="thresholdMax" matSliderEndThumb /> |
|||
</mat-slider> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.max" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} @else { |
|||
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMin"> |
|||
<h6 class="d-flex mb-0"> |
|||
<ng-container i18n>Threshold Min</ng-container>: |
|||
<gf-value |
|||
class="ml-1" |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.settings.thresholdMin" |
|||
/> |
|||
</h6> |
|||
<div class="align-items-center d-flex w-100"> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.min" |
|||
/> |
|||
<mat-slider |
|||
class="flex-grow-1" |
|||
name="thresholdMin" |
|||
[max]="data.rule.configuration.threshold.max" |
|||
[min]="data.rule.configuration.threshold.min" |
|||
[step]="data.rule.configuration.threshold.step" |
|||
> |
|||
<input matSliderThumb [(ngModel)]="data.settings.thresholdMin" /> |
|||
</mat-slider> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.max" |
|||
/> |
|||
} @else { |
|||
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMin"> |
|||
<h6 class="d-flex mb-0"> |
|||
<ng-container i18n>Threshold Min</ng-container>: |
|||
<gf-value |
|||
class="ml-1" |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="settingsForm.get('thresholdMin').value" |
|||
/> |
|||
</h6> |
|||
<div class="align-items-center d-flex w-100"> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.min" |
|||
/> |
|||
<mat-slider |
|||
class="flex-grow-1" |
|||
name="thresholdMin" |
|||
[max]="data.rule.configuration.threshold.max" |
|||
[min]="data.rule.configuration.threshold.min" |
|||
[step]="data.rule.configuration.threshold.step" |
|||
> |
|||
<input formControlName="thresholdMin" matSliderThumb /> |
|||
</mat-slider> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.max" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMax"> |
|||
<h6 class="d-flex mb-0"> |
|||
<ng-container i18n>Threshold Max</ng-container>: |
|||
<gf-value |
|||
class="ml-1" |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.settings.thresholdMax" |
|||
/> |
|||
</h6> |
|||
<div class="align-items-center d-flex w-100"> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.min" |
|||
/> |
|||
<mat-slider |
|||
class="flex-grow-1" |
|||
name="thresholdMax" |
|||
[max]="data.rule.configuration.threshold.max" |
|||
[min]="data.rule.configuration.threshold.min" |
|||
[step]="data.rule.configuration.threshold.step" |
|||
> |
|||
<input matSliderThumb [(ngModel)]="data.settings.thresholdMax" /> |
|||
</mat-slider> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.max" |
|||
/> |
|||
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMax"> |
|||
<h6 class="d-flex mb-0"> |
|||
<ng-container i18n>Threshold Max</ng-container>: |
|||
<gf-value |
|||
class="ml-1" |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="settingsForm.get('thresholdMax').value" |
|||
/> |
|||
</h6> |
|||
<div class="align-items-center d-flex w-100"> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.min" |
|||
/> |
|||
<mat-slider |
|||
class="flex-grow-1" |
|||
name="thresholdMax" |
|||
[max]="data.rule.configuration.threshold.max" |
|||
[min]="data.rule.configuration.threshold.min" |
|||
[step]="data.rule.configuration.threshold.step" |
|||
> |
|||
<input formControlName="thresholdMax" matSliderThumb /> |
|||
</mat-slider> |
|||
<gf-value |
|||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
|||
[locale]="data.locale" |
|||
[precision]="2" |
|||
[value]="data.rule.configuration.threshold.max" |
|||
/> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
} |
|||
</div> |
|||
} |
|||
</div> |
|||
|
|||
<div align="end" mat-dialog-actions> |
|||
<button i18n mat-button (click)="dialogRef.close()">Close</button> |
|||
<button |
|||
color="primary" |
|||
mat-flat-button |
|||
(click)="dialogRef.close(data.settings)" |
|||
> |
|||
<ng-container i18n>Save</ng-container> |
|||
</button> |
|||
</div> |
|||
<div align="end" mat-dialog-actions> |
|||
<button mat-button type="button" (click)="dialogRef.close()"> |
|||
<ng-container i18n>Close</ng-container> |
|||
</button> |
|||
<button |
|||
color="primary" |
|||
mat-flat-button |
|||
type="submit" |
|||
[disabled]="!settingsForm.dirty" |
|||
> |
|||
<ng-container i18n>Save</ng-container> |
|||
</button> |
|||
</div> |
|||
</form> |
|||
|
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
File diff suppressed because it is too large
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue