Browse Source

Merge 859314d4b6 into 7628e40f4b

pull/3507/merge
Usiel Riedl 3 days ago
committed by GitHub
parent
commit
9cc96ab640
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 26
      apps/api/src/app/order/order.service.ts
  2. 23
      apps/api/src/app/portfolio/portfolio.controller.ts
  3. 49
      apps/api/src/app/portfolio/portfolio.service.ts
  4. 15
      apps/client/src/app/services/data.service.ts
  5. 19
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html
  6. 4
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.scss
  7. 50
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

26
apps/api/src/app/order/order.service.ts

@ -353,6 +353,24 @@ export class OrderService {
];
const where: Prisma.OrderWhereInput = { userId };
const symbolProfileConditions: Prisma.SymbolProfileWhereInput[] = [];
where.SymbolProfile = {
AND: symbolProfileConditions
};
const searchQuery = filters?.find(({ type }) => {
return type === 'SEARCH_QUERY';
})?.id;
if (searchQuery) {
symbolProfileConditions.push({
OR: [
{ id: { mode: 'insensitive', contains: searchQuery } },
{ isin: { mode: 'insensitive', contains: searchQuery } },
{ name: { mode: 'insensitive', contains: searchQuery } },
{ symbol: { mode: 'insensitive', contains: searchQuery } }
]
});
}
if (endDate || startDate) {
where.AND = [];
@ -381,10 +399,6 @@ export class OrderService {
return type === 'SYMBOL';
})?.id;
const searchQuery = filters?.find(({ type }) => {
return type === 'SEARCH_QUERY';
})?.id;
if (filtersByAccount?.length > 0) {
where.accountId = {
in: filtersByAccount.map(({ id }) => {
@ -398,7 +412,7 @@ export class OrderService {
}
if (filtersByAssetClass?.length > 0) {
where.SymbolProfile = {
symbolProfileConditions.push({
OR: [
{
AND: [
@ -423,7 +437,7 @@ export class OrderService {
}
}
]
};
});
}
if (filterByDataSource && filterBySymbol) {

23
apps/api/src/app/portfolio/portfolio.controller.ts

@ -18,6 +18,7 @@ import {
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import {
LookupItem,
PortfolioDetails,
PortfolioDividends,
PortfolioHoldingResponse,
@ -428,6 +429,28 @@ export class PortfolioController {
return { holdings };
}
@Get('lookup')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInResponseInterceptor)
public async lookupSymbol(
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Query('accounts') filterByAccounts?: string,
@Query('query') filterBySearchQuery?: string
): Promise<{ items: LookupItem[] }> {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts,
filterBySearchQuery
});
return {
items: await this.portfolioService.getLookup({
filters,
impersonationId,
userId: this.request.user.id
})
};
}
@Get('investments')
@UseGuards(AuthGuard('jwt'), HasPermissionGuard)
@UseInterceptors(TransformDataSourceInRequestInterceptor)

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

@ -35,13 +35,19 @@ import {
TAG_ID_EMERGENCY_FUND,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper';
import {
DATE_FORMAT,
getAssetProfileIdentifier,
getSum,
parseDate
} from '@ghostfolio/common/helper';
import {
AccountsResponse,
EnhancedSymbolProfile,
Filter,
HistoricalDataItem,
InvestmentItem,
LookupItem,
PortfolioDetails,
PortfolioHoldingResponse,
PortfolioInvestments,
@ -85,7 +91,7 @@ import {
parseISO,
set
} from 'date-fns';
import { isEmpty } from 'lodash';
import { isEmpty, uniqBy } from 'lodash';
import { PortfolioCalculator } from './calculator/portfolio-calculator';
import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory';
@ -387,6 +393,45 @@ export class PortfolioService {
};
}
public async getLookup({
filters,
impersonationId,
userId
}: {
dateRange?: DateRange;
filters?: Filter[];
impersonationId: string;
userId: string;
}): Promise<LookupItem[]> {
userId = await this.getUserId(impersonationId, userId);
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
const { activities } = await this.orderService.getOrders({
filters,
userCurrency,
userId
});
return uniqBy(activities, (activity) => {
return getAssetProfileIdentifier({
dataSource: activity.SymbolProfile.dataSource,
symbol: activity.SymbolProfile.symbol
});
}).map((activity) => ({
assetClass: activity.SymbolProfile.assetClass,
assetSubClass: activity.SymbolProfile.assetSubClass,
currency: activity.SymbolProfile.currency,
dataProviderInfo: {
isPremium: false,
name: 'Ghostfolio API'
},
dataSource: activity.SymbolProfile.dataSource,
name: activity.SymbolProfile.name,
symbol: activity.SymbolProfile.symbol
}));
}
public async getDetails({
dateRange = 'max',
filters,

15
apps/client/src/app/services/data.service.ts

@ -36,6 +36,7 @@ import {
Filter,
ImportResponse,
InfoItem,
LookupItem,
LookupResponse,
MarketDataDetailsResponse,
MarketDataOfMarketsResponse,
@ -602,6 +603,20 @@ export class DataService {
);
}
public fetchPortfolioLookup({ query }: { query: string }) {
const params = new HttpParams().set('query', query);
return this.http
.get<{ items: LookupItem[] }>('/api/v1/portfolio/lookup', {
params
})
.pipe(
map((response) => {
return response.items;
})
);
}
public fetchPortfolioHoldings({
filters,
range

19
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html

@ -11,8 +11,10 @@
[displayWith]="displayFn"
(optionSelected)="onUpdateSymbol($event)"
>
@if (!isLoading) {
@for (lookupItem of lookupItems; track lookupItem) {
@if (
(!isLoadingRemote || !isLoadingLocal) && filteredLookupItems.length > 0
) {
@for (lookupItem of filteredLookupItems; track lookupItem) {
<mat-option
class="line-height-1"
[disabled]="lookupItem.dataProviderInfo.isPremium"
@ -41,9 +43,20 @@
>
}
}
@if (isLoadingRemote || isLoadingLocal) {
<mat-option class="line-height-1" [disabled]="true">
<span class="align-items-center d-flex line-height-1">
<mat-spinner
class="mat-more-symbols-progress-spinner"
[diameter]="20"
/>
Loading more symbols ...
</span>
</mat-option>
}
}
</mat-autocomplete>
@if (isLoading) {
@if (isLoadingRemote || isLoadingLocal) {
<mat-spinner class="position-absolute" [diameter]="20" />
}

4
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.scss

@ -6,3 +6,7 @@
top: calc(50% - 10px);
}
}
.mat-more-symbols-progress-spinner {
margin-right: 0.5rem;
}

50
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

@ -36,11 +36,12 @@ import {
import { MatInput, MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { isString } from 'lodash';
import { Subject, tap } from 'rxjs';
import { combineLatest, Subject, tap } from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
filter,
startWith,
switchMap,
takeUntil
} from 'rxjs/operators';
@ -79,7 +80,8 @@ export class GfSymbolAutocompleteComponent
implements OnChanges, OnDestroy, OnInit
{
@Input() public defaultLookupItems: LookupItem[] = [];
@Input() public isLoading = false;
@Input() public isLoadingLocal = false;
@Input() public isLoadingRemote = false;
@ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;
@ -88,8 +90,8 @@ export class GfSymbolAutocompleteComponent
@ViewChild(MatInput) private input: MatInput;
public control = new FormControl();
public lookupItems: (LookupItem & { assetSubClassString: string })[] = [];
public filteredLookupItems: (LookupItem & { assetSubClassString: string })[] =
[];
private unsubscribeSubject = new Subject<void>();
public constructor(
@ -129,29 +131,53 @@ export class GfSymbolAutocompleteComponent
return isString(query);
}),
tap(() => {
this.isLoading = true;
this.isLoadingRemote = true;
this.isLoadingLocal = true;
this.changeDetectorRef.markForCheck();
}),
debounceTime(400),
distinctUntilChanged(),
takeUntil(this.unsubscribeSubject),
switchMap((query: string) => {
return this.dataService.fetchSymbols({
switchMap((query: string) =>
combineLatest([
this.dataService
.fetchPortfolioLookup({
query
})
.pipe(startWith(undefined)),
this.dataService
.fetchSymbols({
query,
includeIndices: this.includeIndices
});
})
.pipe(startWith(undefined))
])
)
)
.subscribe((filteredLookupItems) => {
this.lookupItems = filteredLookupItems.map((lookupItem) => {
const [localItems, remoteItems]: [LookupItem[], LookupItem[]] =
filteredLookupItems;
const uniqueItems = [
...new Map(
(localItems ?? [])
.concat(remoteItems ?? [])
.map((item) => [item.symbol, item])
).values()
];
this.filteredLookupItems = uniqueItems.map((lookupItem) => {
return {
...lookupItem,
assetSubClassString: translate(lookupItem.assetSubClass)
};
});
this.isLoading = false;
if (localItems !== undefined) {
this.isLoadingLocal = false;
}
if (remoteItems !== undefined) {
this.isLoadingRemote = false;
}
this.changeDetectorRef.markForCheck();
});
@ -176,7 +202,7 @@ export class GfSymbolAutocompleteComponent
}
public isValueInOptions(value: string) {
return this.lookupItems.some((item) => {
return this.filteredLookupItems.some((item) => {
return item.symbol === value;
});
}
@ -209,7 +235,7 @@ export class GfSymbolAutocompleteComponent
}
private showDefaultOptions() {
this.lookupItems = this.defaultLookupItems.map((lookupItem) => {
this.filteredLookupItems = this.defaultLookupItems.map((lookupItem) => {
return {
...lookupItem,
assetSubClassString: translate(lookupItem.assetSubClass)

Loading…
Cancel
Save