Browse Source

Feature/add holdings to overview (#7861)

* Add holdings

* Update changelog
pull/7874/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
0e1b77bcf7
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 50
      apps/client/src/app/components/home-overview/home-overview.component.ts
  3. 31
      apps/client/src/app/components/home-overview/home-overview.html
  4. 14
      apps/client/src/app/components/home-overview/home-overview.scss
  5. 3
      libs/ui/src/lib/holdings-table/holdings-table.component.html

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added the holdings table to the overview tab of the home page on desktop (experimental)
- Added a tool to search for asset profiles to the server of the Model Context Protocol (MCP) (experimental) - Added a tool to search for asset profiles to the server of the Model Context Protocol (MCP) (experimental)
### Changed ### Changed

50
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -5,17 +5,20 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DEFAULT_DATE_RANGE, DEFAULT_DATE_RANGE,
DEFAULT_LOCALE,
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
LineChartItem, LineChartItem,
PortfolioPerformance, PortfolioPerformance,
PortfolioPosition,
User User
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { GfHoldingsTableComponent } from '@ghostfolio/ui/holdings-table';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
@ -30,15 +33,18 @@ import {
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { MatCardModule } from '@angular/material/card';
import { Router, RouterModule } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
GfHoldingsTableComponent,
GfLineChartComponent, GfLineChartComponent,
GfPortfolioPerformanceComponent, GfPortfolioPerformanceComponent,
MatButtonModule, MatButtonModule,
MatCardModule,
RouterModule RouterModule
], ],
selector: 'gf-home-overview', selector: 'gf-home-overview',
@ -46,9 +52,13 @@ import { DeviceDetectorService } from 'ngx-device-detector';
templateUrl: './home-overview.html' templateUrl: './home-overview.html'
}) })
export class GfHomeOverviewComponent implements OnInit { export class GfHomeOverviewComponent implements OnInit {
protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE;
protected readonly errors = signal<AssetProfileIdentifier[]>([]); protected readonly errors = signal<AssetProfileIdentifier[]>([]);
protected readonly hasImpersonationId = signal(false); protected readonly hasImpersonationId = signal(false);
protected readonly historicalDataItems = signal<LineChartItem[] | null>(null); protected readonly historicalDataItems = signal<LineChartItem[] | null>(null);
protected readonly holdings = signal<PortfolioPosition[] | undefined>(
undefined
);
protected readonly isLoadingPerformance = signal(true); protected readonly isLoadingPerformance = signal(true);
protected readonly performance = signal<PortfolioPerformance | null>(null); protected readonly performance = signal<PortfolioPerformance | null>(null);
protected readonly performanceLabel = $localize`Performance`; protected readonly performanceLabel = $localize`Performance`;
@ -77,13 +87,24 @@ export class GfHomeOverviewComponent implements OnInit {
const user = this.user(); const user = this.user();
return user return user
? !user.settings.isRestrictedView && user.settings.viewMode !== 'ZEN' ? !user?.settings?.isRestrictedView && user?.settings?.viewMode !== 'ZEN'
: false; : false;
}); });
protected readonly showExperimentalHoldings = computed(() => {
const user = this.user();
return (
user?.settings?.isExperimentalFeatures === true &&
user?.settings?.viewMode !== 'ZEN'
);
});
protected readonly unit = computed(() => { protected readonly unit = computed(() => {
const user = this.user();
return this.showDetails() return this.showDetails()
? (this.user()?.settings?.baseCurrency ?? DEFAULT_CURRENCY) ? (user?.settings?.baseCurrency ?? DEFAULT_CURRENCY)
: '%'; : '%';
}); });
@ -94,6 +115,7 @@ export class GfHomeOverviewComponent implements OnInit {
ImpersonationStorageService ImpersonationStorageService
); );
private readonly layoutService = inject(LayoutService); private readonly layoutService = inject(LayoutService);
private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
public constructor() { public constructor() {
@ -122,10 +144,32 @@ export class GfHomeOverviewComponent implements OnInit {
}); });
} }
protected onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) {
if (dataSource && symbol) {
this.router.navigate([], {
queryParams: { dataSource, symbol, holdingDetailDialog: true }
});
}
}
private update() { private update() {
this.historicalDataItems.set(null); this.historicalDataItems.set(null);
this.isLoadingPerformance.set(true); this.isLoadingPerformance.set(true);
if (this.showExperimentalHoldings()) {
this.holdings.set(undefined);
this.dataService
.fetchPortfolioHoldings({
filters: [{ id: 'ACTIVE', type: 'HOLDING_TYPE' }],
range: this.user()?.settings?.dateRange
})
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => {
this.holdings.set(holdings);
});
}
this.dataService this.dataService
.fetchPortfolioPerformance({ .fetchPortfolioPerformance({
range: this.user()?.settings?.dateRange ?? DEFAULT_DATE_RANGE range: this.user()?.settings?.dateRange ?? DEFAULT_DATE_RANGE

31
apps/client/src/app/components/home-overview/home-overview.html

@ -1,5 +1,5 @@
<div <div
class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative" class="align-items-center container d-flex flex-column h-100 justify-content-center overview p-0 position-relative px-xl-3"
> >
@if ( @if (
!hasImpersonationId() && !hasImpersonationId() &&
@ -61,8 +61,8 @@
</div> </div>
</div> </div>
} @else { } @else {
<div class="row w-100"> <div class="align-items-stretch flex-nowrap row w-100">
<div class="col p-0"> <div class="col-12 col-xl p-0">
<div class="chart-container mx-auto position-relative"> <div class="chart-container mx-auto position-relative">
<gf-line-chart <gf-line-chart
class="position-absolute" class="position-absolute"
@ -80,12 +80,9 @@
[showYAxis]="false" [showYAxis]="false"
/> />
</div> </div>
</div>
</div>
<div class="overview-container row mt-1"> <div class="overview-container row mt-1">
<div class="col"> <div class="col">
<gf-portfolio-performance <gf-portfolio-performance
class="pb-4"
[errors]="errors()" [errors]="errors()"
[isLoading]="isLoadingPerformance()" [isLoading]="isLoadingPerformance()"
[locale]="user()?.settings?.locale" [locale]="user()?.settings?.locale"
@ -96,5 +93,27 @@
/> />
</div> </div>
</div> </div>
</div>
@if (showExperimentalHoldings()) {
<div class="col-xl-4 d-none d-xl-block ml-5 p-0 position-relative">
<mat-card
appearance="outlined"
class="h-100 holdings position-absolute w-100"
>
<mat-card-header>
<mat-card-title i18n>Holdings</mat-card-title>
</mat-card-header>
<mat-card-content class="overflow-auto">
<gf-holdings-table
mode="simple"
[holdings]="holdings()"
[locale]="user()?.settings?.locale ?? DEFAULT_LOCALE"
(holdingClicked)="onHoldingClicked($event)"
/>
</mat-card-content>
</mat-card>
</div>
}
</div>
} }
</div> </div>

14
apps/client/src/app/components/home-overview/home-overview.scss

@ -30,6 +30,20 @@
} }
} }
mat-card {
&.holdings {
--mat-card-title-text-size: var(--mat-sys-title-medium-size);
mat-card-content {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
}
}
.introduction { .introduction {
max-width: 50rem; max-width: 50rem;
} }

3
libs/ui/src/lib/holdings-table/holdings-table.component.html

@ -208,7 +208,8 @@
@if (isLoading()) { @if (isLoading()) {
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="px-4 py-3" [class.px-4]="mode() === 'default'"
[class.py-3]="mode() === 'default'"
[theme]="{ [theme]="{
height: '1.5rem', height: '1.5rem',
width: '100%' width: '100%'

Loading…
Cancel
Save