mirror of https://github.com/ghostfolio/ghostfolio
Thomas Kaul
3 years ago
committed by
GitHub
14 changed files with 324 additions and 32 deletions
@ -0,0 +1,15 @@ |
|||||
|
import { NgModule } from '@angular/core'; |
||||
|
import { RouterModule, Routes } from '@angular/router'; |
||||
|
import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; |
||||
|
|
||||
|
import { HoldingsPageComponent } from './holdings-page.component'; |
||||
|
|
||||
|
const routes: Routes = [ |
||||
|
{ path: '', component: HoldingsPageComponent, canActivate: [AuthGuard] } |
||||
|
]; |
||||
|
|
||||
|
@NgModule({ |
||||
|
imports: [RouterModule.forChild(routes)], |
||||
|
exports: [RouterModule] |
||||
|
}) |
||||
|
export class HoldingsPageRoutingModule {} |
@ -0,0 +1,220 @@ |
|||||
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; |
||||
|
import { MatDialog } from '@angular/material/dialog'; |
||||
|
import { ActivatedRoute, Router } from '@angular/router'; |
||||
|
import { PositionDetailDialogParams } from '@ghostfolio/client/components/position/position-detail-dialog/interfaces/interfaces'; |
||||
|
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; |
||||
|
import { DataService } from '@ghostfolio/client/services/data.service'; |
||||
|
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; |
||||
|
import { UserService } from '@ghostfolio/client/services/user/user.service'; |
||||
|
import { |
||||
|
Filter, |
||||
|
PortfolioDetails, |
||||
|
PortfolioPosition, |
||||
|
User |
||||
|
} from '@ghostfolio/common/interfaces'; |
||||
|
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; |
||||
|
import { AssetClass, DataSource } from '@prisma/client'; |
||||
|
import { DeviceDetectorService } from 'ngx-device-detector'; |
||||
|
import { Subject, Subscription } from 'rxjs'; |
||||
|
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; |
||||
|
|
||||
|
@Component({ |
||||
|
host: { class: 'page' }, |
||||
|
selector: 'gf-holdings-page', |
||||
|
styleUrls: ['./holdings-page.scss'], |
||||
|
templateUrl: './holdings-page.html' |
||||
|
}) |
||||
|
export class HoldingsPageComponent implements OnDestroy, OnInit { |
||||
|
public activeFilters: Filter[] = []; |
||||
|
public allFilters: Filter[]; |
||||
|
public deviceType: string; |
||||
|
public filters$ = new Subject<Filter[]>(); |
||||
|
public hasImpersonationId: boolean; |
||||
|
public hasPermissionToCreateOrder: boolean; |
||||
|
public isLoading = false; |
||||
|
public placeholder = ''; |
||||
|
public portfolioDetails: PortfolioDetails; |
||||
|
public positionsArray: PortfolioPosition[]; |
||||
|
public routeQueryParams: Subscription; |
||||
|
public user: User; |
||||
|
|
||||
|
private readonly SEARCH_PLACEHOLDER = 'Filter by account or tag...'; |
||||
|
private unsubscribeSubject = new Subject<void>(); |
||||
|
|
||||
|
/** |
||||
|
* @constructor |
||||
|
*/ |
||||
|
public constructor( |
||||
|
private changeDetectorRef: ChangeDetectorRef, |
||||
|
private dataService: DataService, |
||||
|
private deviceService: DeviceDetectorService, |
||||
|
private dialog: MatDialog, |
||||
|
private impersonationStorageService: ImpersonationStorageService, |
||||
|
private route: ActivatedRoute, |
||||
|
private router: Router, |
||||
|
private userService: UserService |
||||
|
) { |
||||
|
this.routeQueryParams = route.queryParams |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((params) => { |
||||
|
if ( |
||||
|
params['dataSource'] && |
||||
|
params['positionDetailDialog'] && |
||||
|
params['symbol'] |
||||
|
) { |
||||
|
this.openPositionDialog({ |
||||
|
dataSource: params['dataSource'], |
||||
|
symbol: params['symbol'] |
||||
|
}); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Initializes the controller |
||||
|
*/ |
||||
|
public ngOnInit() { |
||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType; |
||||
|
|
||||
|
this.impersonationStorageService |
||||
|
.onChangeHasImpersonation() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((aId) => { |
||||
|
this.hasImpersonationId = !!aId; |
||||
|
}); |
||||
|
|
||||
|
this.filters$ |
||||
|
.pipe( |
||||
|
distinctUntilChanged(), |
||||
|
switchMap((filters) => { |
||||
|
this.isLoading = true; |
||||
|
this.activeFilters = filters; |
||||
|
this.placeholder = |
||||
|
this.activeFilters.length <= 0 ? this.SEARCH_PLACEHOLDER : ''; |
||||
|
|
||||
|
return this.dataService.fetchPortfolioDetails({ |
||||
|
filters: this.activeFilters |
||||
|
}); |
||||
|
}), |
||||
|
takeUntil(this.unsubscribeSubject) |
||||
|
) |
||||
|
.subscribe((portfolioDetails) => { |
||||
|
this.portfolioDetails = portfolioDetails; |
||||
|
|
||||
|
this.initializeAnalysisData(); |
||||
|
|
||||
|
this.isLoading = false; |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
}); |
||||
|
|
||||
|
this.userService.stateChanged |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((state) => { |
||||
|
if (state?.user) { |
||||
|
this.user = state.user; |
||||
|
|
||||
|
this.hasPermissionToCreateOrder = hasPermission( |
||||
|
this.user.permissions, |
||||
|
permissions.createOrder |
||||
|
); |
||||
|
|
||||
|
const accountFilters: Filter[] = this.user.accounts |
||||
|
.filter(({ accountType }) => { |
||||
|
return accountType === 'SECURITIES'; |
||||
|
}) |
||||
|
.map(({ id, name }) => { |
||||
|
return { |
||||
|
id, |
||||
|
label: name, |
||||
|
type: 'ACCOUNT' |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
const assetClassFilters: Filter[] = []; |
||||
|
for (const assetClass of Object.keys(AssetClass)) { |
||||
|
assetClassFilters.push({ |
||||
|
id: assetClass, |
||||
|
label: assetClass, |
||||
|
type: 'ASSET_CLASS' |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => { |
||||
|
return { |
||||
|
id, |
||||
|
label: name, |
||||
|
type: 'TAG' |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
this.allFilters = [ |
||||
|
...accountFilters, |
||||
|
...assetClassFilters, |
||||
|
...tagFilters |
||||
|
]; |
||||
|
|
||||
|
this.changeDetectorRef.markForCheck(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
public initialize() { |
||||
|
this.positionsArray = []; |
||||
|
} |
||||
|
|
||||
|
public initializeAnalysisData() { |
||||
|
this.initialize(); |
||||
|
|
||||
|
for (const [symbol, position] of Object.entries( |
||||
|
this.portfolioDetails.holdings |
||||
|
)) { |
||||
|
this.positionsArray.push(position); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public ngOnDestroy() { |
||||
|
this.unsubscribeSubject.next(); |
||||
|
this.unsubscribeSubject.complete(); |
||||
|
} |
||||
|
|
||||
|
private openPositionDialog({ |
||||
|
dataSource, |
||||
|
symbol |
||||
|
}: { |
||||
|
dataSource: DataSource; |
||||
|
symbol: string; |
||||
|
}) { |
||||
|
this.userService |
||||
|
.get() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe((user) => { |
||||
|
this.user = user; |
||||
|
|
||||
|
const dialogRef = this.dialog.open(PositionDetailDialog, { |
||||
|
autoFocus: false, |
||||
|
data: <PositionDetailDialogParams>{ |
||||
|
dataSource, |
||||
|
symbol, |
||||
|
baseCurrency: this.user?.settings?.baseCurrency, |
||||
|
deviceType: this.deviceType, |
||||
|
hasImpersonationId: this.hasImpersonationId, |
||||
|
hasPermissionToReportDataGlitch: hasPermission( |
||||
|
this.user?.permissions, |
||||
|
permissions.reportDataGlitch |
||||
|
), |
||||
|
locale: this.user?.settings?.locale |
||||
|
}, |
||||
|
height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', |
||||
|
width: this.deviceType === 'mobile' ? '100vw' : '50rem' |
||||
|
}); |
||||
|
|
||||
|
dialogRef |
||||
|
.afterClosed() |
||||
|
.pipe(takeUntil(this.unsubscribeSubject)) |
||||
|
.subscribe(() => { |
||||
|
this.router.navigate(['.'], { relativeTo: this.route }); |
||||
|
}); |
||||
|
}); |
||||
|
} |
||||
|
} |
@ -0,0 +1,32 @@ |
|||||
|
<div class="container"> |
||||
|
<div class="row"> |
||||
|
<div class="col"> |
||||
|
<h3 class="d-flex justify-content-center mb-3" i18n>Holdings</h3> |
||||
|
<gf-activities-filter |
||||
|
[allFilters]="allFilters" |
||||
|
[isLoading]="isLoading" |
||||
|
[placeholder]="placeholder" |
||||
|
(valueChanged)="filters$.next($event)" |
||||
|
></gf-activities-filter> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-lg"> |
||||
|
<gf-positions-table |
||||
|
[baseCurrency]="user?.settings?.baseCurrency" |
||||
|
[deviceType]="deviceType" |
||||
|
[locale]="user?.settings?.locale" |
||||
|
[positions]="positionsArray" |
||||
|
></gf-positions-table> |
||||
|
<div *ngIf="hasPermissionToCreateOrder" class="text-center"> |
||||
|
<a |
||||
|
class="mt-3" |
||||
|
i18n |
||||
|
mat-stroked-button |
||||
|
[routerLink]="['/portfolio', 'activities']" |
||||
|
>Manage Activities</a |
||||
|
> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
@ -0,0 +1,21 @@ |
|||||
|
import { CommonModule } from '@angular/common'; |
||||
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; |
||||
|
import { MatButtonModule } from '@angular/material/button'; |
||||
|
import { GfPositionsTableModule } from '@ghostfolio/client/components/positions-table/positions-table.module'; |
||||
|
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module'; |
||||
|
|
||||
|
import { HoldingsPageRoutingModule } from './holdings-page-routing.module'; |
||||
|
import { HoldingsPageComponent } from './holdings-page.component'; |
||||
|
|
||||
|
@NgModule({ |
||||
|
declarations: [HoldingsPageComponent], |
||||
|
imports: [ |
||||
|
CommonModule, |
||||
|
GfActivitiesFilterModule, |
||||
|
GfPositionsTableModule, |
||||
|
HoldingsPageRoutingModule, |
||||
|
MatButtonModule |
||||
|
], |
||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA] |
||||
|
}) |
||||
|
export class HoldingsPageModule {} |
@ -0,0 +1,3 @@ |
|||||
|
:host { |
||||
|
display: block; |
||||
|
} |
Loading…
Reference in new issue