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 mat-dialog-title>{{ data.categoryName }} › {{ data.rule.name }}</div> |
||||
|
|
||||
<div class="py-3" mat-dialog-content> |
<form |
||||
@if ( |
class="d-flex flex-column h-100" |
||||
data.rule.configuration.thresholdMin && data.rule.configuration.thresholdMax |
[formGroup]="settingsForm" |
||||
) { |
(ngSubmit)="onSubmit()" |
||||
<div class="w-100"> |
> |
||||
<h6 class="d-flex mb-0"> |
<div class="py-3" mat-dialog-content> |
||||
<ng-container i18n>Threshold range</ng-container>: |
@if ( |
||||
<gf-value |
data.rule.configuration.thresholdMin && |
||||
class="ml-1" |
data.rule.configuration.thresholdMax |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
) { |
||||
[locale]="data.locale" |
<div class="w-100"> |
||||
[precision]="2" |
<h6 class="d-flex mb-0"> |
||||
[value]="data.settings.thresholdMin" |
<ng-container i18n>Threshold range</ng-container>: |
||||
/> |
<gf-value |
||||
<span class="mx-1">-</span> |
class="ml-1" |
||||
<gf-value |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[locale]="data.locale" |
||||
[locale]="data.locale" |
[precision]="2" |
||||
[precision]="2" |
[value]="settingsForm.get('thresholdMin').value" |
||||
[value]="data.settings.thresholdMax" |
/> |
||||
/> |
<span class="mx-1">-</span> |
||||
</h6> |
<gf-value |
||||
<div class="align-items-center d-flex w-100"> |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
<gf-value |
[locale]="data.locale" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[precision]="2" |
||||
[locale]="data.locale" |
[value]="settingsForm.get('thresholdMax').value" |
||||
[precision]="2" |
/> |
||||
[value]="data.rule.configuration.threshold.min" |
</h6> |
||||
/> |
<div class="align-items-center d-flex w-100"> |
||||
<mat-slider |
<gf-value |
||||
class="flex-grow-1" |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
[max]="data.rule.configuration.threshold.max" |
[locale]="data.locale" |
||||
[min]="data.rule.configuration.threshold.min" |
[precision]="2" |
||||
[step]="data.rule.configuration.threshold.step" |
[value]="data.rule.configuration.threshold.min" |
||||
> |
/> |
||||
<input matSliderStartThumb [(ngModel)]="data.settings.thresholdMin" /> |
<mat-slider |
||||
<input matSliderEndThumb [(ngModel)]="data.settings.thresholdMax" /> |
class="flex-grow-1" |
||||
</mat-slider> |
[max]="data.rule.configuration.threshold.max" |
||||
<gf-value |
[min]="data.rule.configuration.threshold.min" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[step]="data.rule.configuration.threshold.step" |
||||
[locale]="data.locale" |
> |
||||
[precision]="2" |
<input formControlName="thresholdMin" matSliderStartThumb /> |
||||
[value]="data.rule.configuration.threshold.max" |
<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> |
||||
</div> |
} @else { |
||||
} @else { |
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMin"> |
||||
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMin"> |
<h6 class="d-flex mb-0"> |
||||
<h6 class="d-flex mb-0"> |
<ng-container i18n>Threshold Min</ng-container>: |
||||
<ng-container i18n>Threshold Min</ng-container>: |
<gf-value |
||||
<gf-value |
class="ml-1" |
||||
class="ml-1" |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[locale]="data.locale" |
||||
[locale]="data.locale" |
[precision]="2" |
||||
[precision]="2" |
[value]="settingsForm.get('thresholdMin').value" |
||||
[value]="data.settings.thresholdMin" |
/> |
||||
/> |
</h6> |
||||
</h6> |
<div class="align-items-center d-flex w-100"> |
||||
<div class="align-items-center d-flex w-100"> |
<gf-value |
||||
<gf-value |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[locale]="data.locale" |
||||
[locale]="data.locale" |
[precision]="2" |
||||
[precision]="2" |
[value]="data.rule.configuration.threshold.min" |
||||
[value]="data.rule.configuration.threshold.min" |
/> |
||||
/> |
<mat-slider |
||||
<mat-slider |
class="flex-grow-1" |
||||
class="flex-grow-1" |
name="thresholdMin" |
||||
name="thresholdMin" |
[max]="data.rule.configuration.threshold.max" |
||||
[max]="data.rule.configuration.threshold.max" |
[min]="data.rule.configuration.threshold.min" |
||||
[min]="data.rule.configuration.threshold.min" |
[step]="data.rule.configuration.threshold.step" |
||||
[step]="data.rule.configuration.threshold.step" |
> |
||||
> |
<input formControlName="thresholdMin" matSliderThumb /> |
||||
<input matSliderThumb [(ngModel)]="data.settings.thresholdMin" /> |
</mat-slider> |
||||
</mat-slider> |
<gf-value |
||||
<gf-value |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[locale]="data.locale" |
||||
[locale]="data.locale" |
[precision]="2" |
||||
[precision]="2" |
[value]="data.rule.configuration.threshold.max" |
||||
[value]="data.rule.configuration.threshold.max" |
/> |
||||
/> |
</div> |
||||
</div> |
</div> |
||||
</div> |
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMax"> |
||||
<div class="w-100" [class.d-none]="!data.rule.configuration.thresholdMax"> |
<h6 class="d-flex mb-0"> |
||||
<h6 class="d-flex mb-0"> |
<ng-container i18n>Threshold Max</ng-container>: |
||||
<ng-container i18n>Threshold Max</ng-container>: |
<gf-value |
||||
<gf-value |
class="ml-1" |
||||
class="ml-1" |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[locale]="data.locale" |
||||
[locale]="data.locale" |
[precision]="2" |
||||
[precision]="2" |
[value]="settingsForm.get('thresholdMax').value" |
||||
[value]="data.settings.thresholdMax" |
/> |
||||
/> |
</h6> |
||||
</h6> |
<div class="align-items-center d-flex w-100"> |
||||
<div class="align-items-center d-flex w-100"> |
<gf-value |
||||
<gf-value |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[locale]="data.locale" |
||||
[locale]="data.locale" |
[precision]="2" |
||||
[precision]="2" |
[value]="data.rule.configuration.threshold.min" |
||||
[value]="data.rule.configuration.threshold.min" |
/> |
||||
/> |
<mat-slider |
||||
<mat-slider |
class="flex-grow-1" |
||||
class="flex-grow-1" |
name="thresholdMax" |
||||
name="thresholdMax" |
[max]="data.rule.configuration.threshold.max" |
||||
[max]="data.rule.configuration.threshold.max" |
[min]="data.rule.configuration.threshold.min" |
||||
[min]="data.rule.configuration.threshold.min" |
[step]="data.rule.configuration.threshold.step" |
||||
[step]="data.rule.configuration.threshold.step" |
> |
||||
> |
<input formControlName="thresholdMax" matSliderThumb /> |
||||
<input matSliderThumb [(ngModel)]="data.settings.thresholdMax" /> |
</mat-slider> |
||||
</mat-slider> |
<gf-value |
||||
<gf-value |
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
||||
[isPercent]="data.rule.configuration.threshold.unit === '%'" |
[locale]="data.locale" |
||||
[locale]="data.locale" |
[precision]="2" |
||||
[precision]="2" |
[value]="data.rule.configuration.threshold.max" |
||||
[value]="data.rule.configuration.threshold.max" |
/> |
||||
/> |
</div> |
||||
</div> |
</div> |
||||
</div> |
} |
||||
} |
</div> |
||||
</div> |
|
||||
|
|
||||
<div align="end" mat-dialog-actions> |
<div align="end" mat-dialog-actions> |
||||
<button i18n mat-button (click)="dialogRef.close()">Close</button> |
<button mat-button type="button" (click)="dialogRef.close()"> |
||||
<button |
<ng-container i18n>Close</ng-container> |
||||
color="primary" |
</button> |
||||
mat-flat-button |
<button |
||||
(click)="dialogRef.close(data.settings)" |
color="primary" |
||||
> |
mat-flat-button |
||||
<ng-container i18n>Save</ng-container> |
type="submit" |
||||
</button> |
[disabled]="!settingsForm.dirty" |
||||
</div> |
> |
||||
|
<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