Compare commits

..

7 Commits

Author SHA1 Message Date
00a2b60eb5 Release 2.49.0 (#2979) 2024-02-09 19:30:00 +01:00
fcbf2f1645 Feature/remove lazy from name of activities table (#2978)
* Remove lazy from name

* Update translations
2024-02-09 18:48:05 +01:00
460266a501 Feature/upgrade yahoo finance2 to version 2.9.1 (#2963)
* Upgrade yahoo-finance2 to version 2.9.1

* Update changelog
2024-02-09 18:41:18 +01:00
9fe90273c7 Feature/move assistant to general availability (#2977)
* Move assistant from experimental to general availability

* Update changelog
2024-02-09 18:25:15 +01:00
4078229fe6 Feature/add button to apply filters in assistant (#2971)
* Add apply filters button

* Update changelog
2024-02-09 09:45:54 +01:00
609c03f174 Add analytics image (#2970) 2024-02-08 08:00:04 +01:00
e7d4641d13 Feature/reload data on logo click (#2959)
* Reload data on logo click

* Update changelog
2024-02-07 21:03:28 +01:00
54 changed files with 4046 additions and 5108 deletions

View File

@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.49.0 - 2024-02-09
### Added
- Added a button to apply the active filters in the assistant
### Changed
- Moved the assistant from experimental to general availability
- Improved the usability by reloading the content with a logo click on the home page
- Upgraded `yahoo-finance2` from version `2.9.0` to `2.9.1`
## 2.48.1 - 2024-02-06 ## 2.48.1 - 2024-02-06
### Fixed ### Fixed

View File

@ -280,6 +280,10 @@ Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Sl
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio). If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).
## Analytics
![Alt](https://repobeats.axiom.co/api/embed/281a80b2d0c4af1162866c24c803f1f18e5ed60e.svg 'Repobeats analytics image')
## License ## License
© 2021 - 2024 [Ghostfolio](https://ghostfol.io) © 2021 - 2024 [Ghostfolio](https://ghostfol.io)

View File

@ -10,7 +10,6 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { Sort, SortDirection } from '@angular/material/sort'; import { Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { DataService } from '@ghostfolio/client/services/data.service'; 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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper'; import { downloadAsFile } from '@ghostfolio/common/helper';
import { import {
@ -43,7 +42,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
public currency: string; public currency: string;
public dataSource: MatTableDataSource<OrderWithAccount>; public dataSource: MatTableDataSource<OrderWithAccount>;
public equity: number; public equity: number;
public hasImpersonationId: boolean;
public hasPermissionToDeleteAccountBalance: boolean; public hasPermissionToDeleteAccountBalance: boolean;
public historicalDataItems: HistoricalDataItem[]; public historicalDataItems: HistoricalDataItem[];
public holdings: PortfolioPosition[]; public holdings: PortfolioPosition[];
@ -65,7 +63,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
@Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams, @Inject(MAT_DIALOG_DATA) public data: AccountDetailDialogParams,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<AccountDetailDialog>, public dialogRef: MatDialogRef<AccountDetailDialog>,
private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -136,13 +133,6 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.fetchAccountBalances(); this.fetchAccountBalances();
this.fetchActivities(); this.fetchActivities();
this.fetchPortfolioPerformance(); this.fetchPortfolioPerformance();
@ -165,17 +155,9 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
} }
public onExport() { public onExport() {
let activityIds = []; let activityIds = this.dataSource.data.map(({ id }) => {
return id;
if (this.user?.settings?.isExperimentalFeatures === true) { });
activityIds = this.dataSource.data.map(({ id }) => {
return id;
});
} else {
activityIds = this.activities.map(({ id }) => {
return id;
});
}
this.dataService this.dataService
.fetchExport({ activityIds }) .fetchExport({ activityIds })
@ -215,36 +197,21 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
private fetchActivities() { private fetchActivities() {
this.isLoadingActivities = true; this.isLoadingActivities = true;
if (this.user?.settings?.isExperimentalFeatures === true) { this.dataService
this.dataService .fetchActivities({
.fetchActivities({ filters: [{ id: this.data.accountId, type: 'ACCOUNT' }],
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }], sortColumn: this.sortColumn,
sortColumn: this.sortColumn, sortDirection: this.sortDirection
sortDirection: this.sortDirection })
}) .pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ activities, count }) => {
.subscribe(({ activities, count }) => { this.dataSource = new MatTableDataSource(activities);
this.dataSource = new MatTableDataSource(activities); this.totalItems = count;
this.totalItems = count;
this.isLoadingActivities = false; this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} else {
this.dataService
.fetchActivities({
filters: [{ id: this.data.accountId, type: 'ACCOUNT' }]
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
this.isLoadingActivities = false;
this.changeDetectorRef.markForCheck();
});
}
} }
private fetchPortfolioPerformance() { private fetchPortfolioPerformance() {
@ -268,7 +235,8 @@ export class AccountDetailDialog implements OnDestroy, OnInit {
return { return {
date, date,
value: value:
this.hasImpersonationId || this.user.settings.isRestrictedView this.data.hasImpersonationId ||
this.user.settings.isRestrictedView
? netWorthInPercentage ? netWorthInPercentage
: netWorth : netWorth
}; };

View File

@ -25,7 +25,7 @@
class="h-100" class="h-100"
[currency]="user?.settings?.baseCurrency" [currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems" [historicalDataItems]="historicalDataItems"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView" [isInPercent]="data.hasImpersonationId || user.settings.isRestrictedView"
[isLoading]="isLoadingChart" [isLoading]="isLoadingChart"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
/> />
@ -86,13 +86,12 @@
<ion-icon name="swap-vertical-outline" /> <ion-icon name="swap-vertical-outline" />
<div class="d-none d-sm-block ml-2" i18n>Activities</div> <div class="d-none d-sm-block ml-2" i18n>Activities</div>
</ng-template> </ng-template>
<gf-activities-table-lazy <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView" [hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
@ -103,19 +102,6 @@
(export)="onExport()" (export)="onExport()"
(sortChanged)="onSortChanged($event)" (sortChanged)="onSortChanged($event)"
/> />
<gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="true"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
[showActions]="false"
(export)="onExport()"
/>
</mat-tab> </mat-tab>
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
@ -126,7 +112,7 @@
[accountBalances]="accountBalances" [accountBalances]="accountBalances"
[accountId]="data.accountId" [accountId]="data.accountId"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView" [showActions]="!data.hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
(accountBalanceDeleted)="onDeleteAccountBalance($event)" (accountBalanceDeleted)="onDeleteAccountBalance($event)"
/> />
</mat-tab> </mat-tab>

View File

@ -8,7 +8,6 @@ import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-heade
import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module'; import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module';
import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module'; import { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.module';
import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module'; import { GfAccountBalancesModule } from '@ghostfolio/ui/account-balances/account-balances.module';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -21,7 +20,6 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
CommonModule, CommonModule,
GfAccountBalancesModule, GfAccountBalancesModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
GfHoldingsTableModule, GfHoldingsTableModule,

View File

@ -6,11 +6,12 @@
mat-button mat-button
[ngClass]="{ 'w-100': hasTabs }" [ngClass]="{ 'w-100': hasTabs }"
[routerLink]="['/']" [routerLink]="['/']"
(click)="onLogoClick()"
> >
<gf-logo class="px-2" [label]="pageTitle" /> <gf-logo class="px-2" [label]="pageTitle" />
</a> </a>
</div> </div>
<span class="spacer"></span> <span class="gf-spacer"></span>
<ul class="alig-items-center d-flex list-inline m-0 px-2"> <ul class="alig-items-center d-flex list-inline m-0 px-2">
<li class="list-inline-item"> <li class="list-inline-item">
<a <a
@ -119,11 +120,7 @@
[matMenuTriggerRestoreFocus]="false" [matMenuTriggerRestoreFocus]="false"
(menuOpened)="onOpenAssistant()" (menuOpened)="onOpenAssistant()"
> >
@if (user?.settings?.isExperimentalFeatures) { <ion-icon class="rotate-90" name="options-outline" />
<ion-icon class="rotate-90" name="options-outline" />
} @else {
<ion-icon name="search-outline" />
}
</button> </button>
<mat-menu <mat-menu
#assistantMenu="matMenu" #assistantMenu="matMenu"
@ -324,7 +321,7 @@
/> />
</a> </a>
</div> </div>
<span class="spacer"></span> <span class="gf-spacer"></span>
<ul class="alig-items-center d-flex list-inline m-0 px-2"> <ul class="alig-items-center d-flex list-inline m-0 px-2">
<li class="list-inline-item"> <li class="list-inline-item">
<a <a

View File

@ -38,10 +38,6 @@
} }
} }
} }
.spacer {
flex: 1 1 auto;
}
} }
} }

View File

@ -13,6 +13,7 @@ import { MatMenuTrigger } from '@angular/material/menu';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto';
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component'; import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { LayoutService } from '@ghostfolio/client/core/layout.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { import {
@ -89,6 +90,7 @@ export class HeaderComponent implements OnChanges {
private dataService: DataService, private dataService: DataService,
private dialog: MatDialog, private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService,
private router: Router, private router: Router,
private settingsStorageService: SettingsStorageService, private settingsStorageService: SettingsStorageService,
private tokenStorageService: TokenStorageService, private tokenStorageService: TokenStorageService,
@ -192,6 +194,12 @@ export class HeaderComponent implements OnChanges {
}); });
} }
public onLogoClick() {
if (this.currentRoute === 'home' || this.currentRoute === 'zen') {
this.layoutService.getShouldReloadSubject().next();
}
}
public onMenuClosed() { public onMenuClosed() {
this.isMenuOpen = false; this.isMenuOpen = false;
} }

View File

@ -1,12 +1,4 @@
<div class="container justify-content-center p-3"> <div class="container justify-content-center p-3">
<div *ngIf="!user?.settings?.isExperimentalFeatures" class="mb-3 text-center">
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="positions === undefined"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
/>
</div>
<div class="row"> <div class="row">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2"> <div class="align-items-center col-xs-12 col-md-8 offset-md-2">
<mat-card appearance="outlined"> <mat-card appearance="outlined">

View File

@ -2,6 +2,7 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component'; import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { LayoutService } from '@ghostfolio/client/core/layout.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
LineChartItem, LineChartItem,
@ -43,6 +44,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
@ -73,6 +75,12 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
this.layoutService.shouldReloadContent$
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.update();
});
this.showDetails = this.showDetails =
!this.user.settings.isRestrictedView && !this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN'; this.user.settings.viewMode !== 'ZEN';

View File

@ -96,17 +96,6 @@
[showDetails]="showDetails" [showDetails]="showDetails"
[unit]="unit" [unit]="unit"
/> />
<div
*ngIf="showDetails && !user?.settings?.isExperimentalFeatures"
class="text-center"
>
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoadingPerformance"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
/>
</div>
</div> </div>
</div> </div>
</ng-template> </ng-template>

View File

@ -3,7 +3,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module'; import { GfPortfolioPerformanceModule } from '@ghostfolio/client/components/portfolio-performance/portfolio-performance.module';
import { GfToggleModule } from '@ghostfolio/client/components/toggle/toggle.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
@ -16,7 +15,6 @@ import { HomeOverviewComponent } from './home-overview.component';
GfLineChartModule, GfLineChartModule,
GfNoTransactionsInfoModule, GfNoTransactionsInfoModule,
GfPortfolioPerformanceModule, GfPortfolioPerformanceModule,
GfToggleModule,
MatButtonModule, MatButtonModule,
RouterModule RouterModule
], ],

View File

@ -268,17 +268,9 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
} }
public onExport() { public onExport() {
let activityIds = []; let activityIds = this.dataSource.data.map(({ id }) => {
return id;
if (this.user?.settings?.isExperimentalFeatures === true) { });
activityIds = this.dataSource.data.map(({ id }) => {
return id;
});
} else {
activityIds = this.activities.map(({ id }) => {
return id;
});
}
this.dataService this.dataService
.fetchExport({ activityIds }) .fetchExport({ activityIds })

View File

@ -249,13 +249,12 @@
<div class="row" [ngClass]="{ 'd-none': !activities?.length }"> <div class="row" [ngClass]="{ 'd-none': !activities?.length }">
<div class="col mb-3"> <div class="col mb-3">
<div class="h5 mb-0" i18n>Activities</div> <div class="h5 mb-0" i18n>Activities</div>
<gf-activities-table-lazy <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="data.baseCurrency" [baseCurrency]="data.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data.deviceType" [deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false" [hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView" [hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToFilter]="false" [hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[locale]="data.locale" [locale]="data.locale"
@ -267,20 +266,6 @@
[totalItems]="totalItems" [totalItems]="totalItems"
(export)="onExport()" (export)="onExport()"
/> />
<gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="data.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="true"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data.locale"
[showActions]="false"
[showNameColumn]="false"
(export)="onExport()"
/>
</div> </div>
</div> </div>

View File

@ -5,7 +5,6 @@ import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module'; import { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module'; import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
@ -20,7 +19,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDataProviderCreditsModule, GfDataProviderCreditsModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class LayoutService {
public shouldReloadContent$: Observable<void>;
private shouldReloadSubject = new Subject<void>();
public constructor() {
this.shouldReloadContent$ = this.shouldReloadSubject.asObservable();
}
public getShouldReloadSubject() {
return this.shouldReloadSubject;
}
}

View File

@ -121,43 +121,25 @@ export class ActivitiesPageComponent implements OnDestroy, OnInit {
} }
public fetchActivities() { public fetchActivities() {
if (this.user?.settings?.isExperimentalFeatures === true) { this.dataService
this.dataService .fetchActivities({
.fetchActivities({ filters: this.userService.getFilters(),
filters: this.userService.getFilters(), skip: this.pageIndex * this.pageSize,
skip: this.pageIndex * this.pageSize, sortColumn: this.sortColumn,
sortColumn: this.sortColumn, sortDirection: this.sortDirection,
sortDirection: this.sortDirection, take: this.pageSize
take: this.pageSize })
}) .pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ activities, count }) => {
.subscribe(({ activities, count }) => { this.dataSource = new MatTableDataSource(activities);
this.dataSource = new MatTableDataSource(activities); this.totalItems = count;
this.totalItems = count;
if (this.hasPermissionToCreateActivity && this.totalItems <= 0) { if (this.hasPermissionToCreateActivity && this.totalItems <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } }); this.router.navigate([], { queryParams: { createDialog: true } });
} }
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} else {
this.dataService
.fetchActivities({})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities }) => {
this.activities = activities;
if (
this.hasPermissionToCreateActivity &&
this.activities?.length <= 0
) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
});
}
} }
public onChangePage(page: PageEvent) { public onChangePage(page: PageEvent) {

View File

@ -2,8 +2,7 @@
<div class="mb-3 row"> <div class="mb-3 row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
<gf-activities-table-lazy <gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures === true"
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="deviceType" [deviceType]="deviceType"
@ -27,24 +26,6 @@
(pageChanged)="onChangePage($event)" (pageChanged)="onChangePage($event)"
(sortChanged)="onSortChanged($event)" (sortChanged)="onSortChanged($event)"
/> />
<gf-activities-table
*ngIf="user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToCreateActivity]="hasPermissionToCreateActivity"
[hasPermissionToExportActivities]="!hasImpersonationId"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteActivity && !user.settings.isRestrictedView"
(activityDeleted)="onDeleteActivity($event)"
(activityToClone)="onCloneActivity($event)"
(activityToUpdate)="onUpdateActivity($event)"
(deleteAllActivities)="onDeleteAllActivities()"
(export)="onExport($event)"
(exportDrafts)="onExportDrafts($event)"
(import)="onImport()"
(importDividends)="onImportDividends()"
/>
</div> </div>
</div> </div>

View File

@ -4,7 +4,6 @@ import { MatButtonModule } from '@angular/material/button';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service'; import { ImportActivitiesService } from '@ghostfolio/client/services/import-activities.service';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { ActivitiesPageRoutingModule } from './activities-page-routing.module'; import { ActivitiesPageRoutingModule } from './activities-page-routing.module';
@ -18,7 +17,6 @@ import { GfImportActivitiesDialogModule } from './import-activities-dialog/impor
ActivitiesPageRoutingModule, ActivitiesPageRoutingModule,
CommonModule, CommonModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfCreateOrUpdateActivityDialogModule, GfCreateOrUpdateActivityDialogModule,
GfImportActivitiesDialogModule, GfImportActivitiesDialogModule,
MatButtonModule, MatButtonModule,

View File

@ -116,8 +116,8 @@
</ng-template> </ng-template>
<div class="pt-3"> <div class="pt-3">
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage"> <ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
<gf-activities-table-lazy <gf-activities-table
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures === true" *ngIf="importStep === 1"
[baseCurrency]="data?.user?.settings?.baseCurrency" [baseCurrency]="data?.user?.settings?.baseCurrency"
[dataSource]="dataSource" [dataSource]="dataSource"
[deviceType]="data?.deviceType" [deviceType]="data?.deviceType"
@ -137,23 +137,6 @@
[totalItems]="totalItems" [totalItems]="totalItems"
(selectedActivities)="updateSelection($event)" (selectedActivities)="updateSelection($event)"
/> />
<gf-activities-table
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures !== true"
[activities]="activities"
[baseCurrency]="data?.user?.settings?.baseCurrency"
[deviceType]="data?.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="false"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="data?.user?.settings?.locale"
[pageSize]="maxSafeInteger"
[showActions]="false"
[showCheckbox]="true"
[showFooter]="false"
[showSymbolColumn]="false"
(selectedActivities)="updateSelection($event)"
/>
<div class="d-flex justify-content-end mt-3"> <div class="d-flex justify-content-end mt-3">
<button mat-button (click)="onReset(stepper)"> <button mat-button (click)="onReset(stepper)">
<ng-container i18n>Back</ng-container> <ng-container i18n>Back</ng-container>

View File

@ -13,7 +13,6 @@ import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-heade
import { GfFileDropModule } from '@ghostfolio/client/directives/file-drop/file-drop.module'; import { GfFileDropModule } from '@ghostfolio/client/directives/file-drop/file-drop.module';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';
import { GfActivitiesTableLazyModule } from '@ghostfolio/ui/activities-table-lazy/activities-table-lazy.module';
import { ImportActivitiesDialog } from './import-activities-dialog.component'; import { ImportActivitiesDialog } from './import-activities-dialog.component';
@ -23,7 +22,6 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
CommonModule, CommonModule,
FormsModule, FormsModule,
GfActivitiesTableModule, GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDialogFooterModule, GfDialogFooterModule,
GfDialogHeaderModule, GfDialogHeaderModule,
GfFileDropModule, GfFileDropModule,

View File

@ -11,7 +11,6 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper'; import { prettifySymbol } from '@ghostfolio/common/helper';
import { import {
Filter,
PortfolioDetails, PortfolioDetails,
PortfolioPosition, PortfolioPosition,
UniqueAsset, UniqueAsset,
@ -24,7 +23,7 @@ import { Account, AssetClass, DataSource, Platform } from '@prisma/client';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'gf-allocations-page', selector: 'gf-allocations-page',
@ -38,8 +37,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: number; value: number;
}; };
}; };
public activeFilters: Filter[] = [];
public allFilters: Filter[];
public continents: { public continents: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
@ -47,7 +44,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public deviceType: string; public deviceType: string;
public filters$ = new Subject<Filter[]>();
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public isLoading = false; public isLoading = false;
public markets: { public markets: {
@ -60,7 +56,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: number; value: number;
}; };
}; };
public placeholder = '';
public platforms: { public platforms: {
[id: string]: Pick<Platform, 'name'> & { [id: string]: Pick<Platform, 'name'> & {
id: string; id: string;
@ -135,98 +130,34 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.filters$
.pipe(
distinctUntilChanged(),
switchMap((filters) => {
this.isLoading = true;
this.activeFilters = filters;
this.placeholder =
this.activeFilters.length <= 0
? $localize`Filter by account or tag...`
: '';
this.initialize();
return this.fetchPortfolioDetails();
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe((portfolioDetails) => {
this.initialize();
this.portfolioDetails = portfolioDetails;
this.initializeAllocationsData();
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
const accountFilters: Filter[] = this.user.accounts.map(
({ id, name }) => {
return {
id,
label: name,
type: 'ACCOUNT'
};
}
);
const assetClassFilters: Filter[] = [];
for (const assetClass of Object.keys(AssetClass)) {
assetClassFilters.push({
id: assetClass,
label: translate(assetClass),
type: 'ASSET_CLASS'
});
}
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => {
return {
id,
label: translate(name),
type: 'TAG'
};
});
this.allFilters = [
...accountFilters,
...assetClassFilters,
...tagFilters
];
this.worldMapChartFormat = this.worldMapChartFormat =
this.hasImpersonationId || this.user.settings.isRestrictedView this.hasImpersonationId || this.user.settings.isRestrictedView
? `{0}%` ? `{0}%`
: `{0} ${this.user?.settings?.baseCurrency}`; : `{0} ${this.user?.settings?.baseCurrency}`;
if (this.user?.settings?.isExperimentalFeatures === true) { this.isLoading = true;
this.isLoading = true;
this.initialize(); this.initialize();
this.fetchPortfolioDetails() this.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioDetails) => { .subscribe((portfolioDetails) => {
this.initialize(); this.initialize();
this.portfolioDetails = portfolioDetails; this.portfolioDetails = portfolioDetails;
this.initializeAllocationsData(); this.initializeAllocationsData();
this.isLoading = false; this.isLoading = false;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
@ -273,10 +204,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
private fetchPortfolioDetails() { private fetchPortfolioDetails() {
return this.dataService.fetchPortfolioDetails({ return this.dataService.fetchPortfolioDetails({
filters: filters: this.userService.getFilters()
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters()
}); });
} }

View File

@ -2,14 +2,6 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Allocations</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Allocations</h1>
@if (!user?.settings?.isExperimentalFeatures) {
<gf-activities-filter
[allFilters]="allFilters"
[isLoading]="isLoading"
[placeholder]="placeholder"
(valueChanged)="filters$.next($event)"
/>
}
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -4,7 +4,6 @@ import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatProgressBarModule } from '@angular/material/progress-bar';
import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module'; import { GfWorldMapChartModule } from '@ghostfolio/client/components/world-map-chart/world-map-chart.module';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -17,7 +16,6 @@ import { AllocationsPageComponent } from './allocations-page.component';
imports: [ imports: [
AllocationsPageRoutingModule, AllocationsPageRoutingModule,
CommonModule, CommonModule,
GfActivitiesFilterModule,
GfPortfolioProportionChartModule, GfPortfolioProportionChartModule,
GfPremiumIndicatorModule, GfPremiumIndicatorModule,
GfWorldMapChartModule, GfWorldMapChartModule,

View File

@ -8,7 +8,6 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
Filter,
HistoricalDataItem, HistoricalDataItem,
PortfolioInvestments, PortfolioInvestments,
PortfolioPerformance, PortfolioPerformance,
@ -17,14 +16,14 @@ import {
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { DateRange, GroupBy, ToggleOption } from '@ghostfolio/common/types'; import { GroupBy, ToggleOption } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { AssetClass, DataSource, SymbolProfile } from '@prisma/client'; import { DataSource, SymbolProfile } from '@prisma/client';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { isNumber, sortBy } from 'lodash'; import { isNumber, sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'gf-analysis-page', selector: 'gf-analysis-page',
@ -32,8 +31,6 @@ import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
templateUrl: './analysis-page.html' templateUrl: './analysis-page.html'
}) })
export class AnalysisPageComponent implements OnDestroy, OnInit { export class AnalysisPageComponent implements OnDestroy, OnInit {
public activeFilters: Filter[] = [];
public allFilters: Filter[];
public benchmarkDataItems: HistoricalDataItem[] = []; public benchmarkDataItems: HistoricalDataItem[] = [];
public benchmarks: Partial<SymbolProfile>[]; public benchmarks: Partial<SymbolProfile>[];
public bottom3: Position[]; public bottom3: Position[];
@ -42,7 +39,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public deviceType: string; public deviceType: string;
public dividendsByGroup: InvestmentItem[]; public dividendsByGroup: InvestmentItem[];
public dividendTimelineDataLabel = $localize`Dividend`; public dividendTimelineDataLabel = $localize`Dividend`;
public filters$ = new Subject<Filter[]>();
public firstOrderDate: Date; public firstOrderDate: Date;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public investments: InvestmentItem[]; public investments: InvestmentItem[];
@ -58,7 +54,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public performance: PortfolioPerformance; public performance: PortfolioPerformance;
public performanceDataItems: HistoricalDataItem[]; public performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[]; public performanceDataItemsInPercentage: HistoricalDataItem[];
public placeholder = '';
public portfolioEvolutionDataLabel = $localize`Investment`; public portfolioEvolutionDataLabel = $localize`Investment`;
public streaks: PortfolioInvestments['streaks']; public streaks: PortfolioInvestments['streaks'];
public top3: Position[]; public top3: Position[];
@ -118,61 +113,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.filters$
.pipe(
distinctUntilChanged(),
map((filters) => {
this.activeFilters = filters;
this.placeholder =
this.activeFilters.length <= 0
? $localize`Filter by account or tag...`
: '';
this.update();
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => {});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
const accountFilters: Filter[] = this.user.accounts.map(
({ id, name }) => {
return {
id,
label: name,
type: 'ACCOUNT'
};
}
);
const assetClassFilters: Filter[] = [];
for (const assetClass of Object.keys(AssetClass)) {
assetClassFilters.push({
id: assetClass,
label: translate(assetClass),
type: 'ASSET_CLASS'
});
}
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => {
return {
id,
label: translate(name),
type: 'TAG'
};
});
this.allFilters = [
...accountFilters,
...assetClassFilters,
...tagFilters
];
this.update(); this.update();
} }
}); });
@ -196,24 +142,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
}); });
} }
public onChangeDateRange(dateRange: DateRange) {
this.dataService
.putUserSetting({ dateRange })
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.userService.remove();
this.userService
.get()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((user) => {
this.user = user;
this.changeDetectorRef.markForCheck();
});
});
}
public onChangeGroupBy(aMode: GroupBy) { public onChangeGroupBy(aMode: GroupBy) {
this.mode = aMode; this.mode = aMode;
this.fetchDividendsAndInvestments(); this.fetchDividendsAndInvestments();
@ -227,10 +155,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
private fetchDividendsAndInvestments() { private fetchDividendsAndInvestments() {
this.dataService this.dataService
.fetchDividends({ .fetchDividends({
filters: filters: this.userService.getFilters(),
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters(),
groupBy: this.mode, groupBy: this.mode,
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
@ -243,10 +168,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchInvestments({ .fetchInvestments({
filters: filters: this.userService.getFilters(),
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters(),
groupBy: this.mode, groupBy: this.mode,
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
@ -321,10 +243,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchPortfolioPerformance({ .fetchPortfolioPerformance({
filters: filters: this.userService.getFilters(),
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -370,10 +289,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchPositions({ .fetchPositions({
filters: filters: this.userService.getFilters(),
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))

View File

@ -1,21 +1,5 @@
<div class="container"> <div class="container">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Analysis</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Analysis</h1>
@if (!user?.settings?.isExperimentalFeatures) {
<div class="my-4 text-center">
<gf-toggle
[defaultValue]="user?.settings?.dateRange"
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[options]="dateRangeOptions"
(change)="onChangeDateRange($event.value)"
/>
</div>
<gf-activities-filter
[allFilters]="allFilters"
[isLoading]="isLoadingBenchmarkComparator || isLoadingInvestmentChart"
[placeholder]="placeholder"
(valueChanged)="filters$.next($event)"
/>
}
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col-lg"> <div class="col-lg">
<gf-benchmark-comparator <gf-benchmark-comparator

View File

@ -7,17 +7,15 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
Filter,
PortfolioDetails, PortfolioDetails,
PortfolioPosition, 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 { translate } from '@ghostfolio/ui/i18n'; import { DataSource } from '@prisma/client';
import { AssetClass, DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
selector: 'gf-holdings-page', selector: 'gf-holdings-page',
@ -25,15 +23,11 @@ import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
templateUrl: './holdings-page.html' templateUrl: './holdings-page.html'
}) })
export class HoldingsPageComponent implements OnDestroy, OnInit { export class HoldingsPageComponent implements OnDestroy, OnInit {
public activeFilters: Filter[] = [];
public allFilters: Filter[];
public deviceType: string; public deviceType: string;
public filters$ = new Subject<Filter[]>();
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean; public hasPermissionToCreateOrder: boolean;
public holdings: PortfolioPosition[]; public holdings: PortfolioPosition[];
public isLoading = false; public isLoading = false;
public placeholder = '';
public portfolioDetails: PortfolioDetails; public portfolioDetails: PortfolioDetails;
public user: User; public user: User;
@ -75,31 +69,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.filters$
.pipe(
distinctUntilChanged(),
switchMap((filters) => {
this.isLoading = true;
this.activeFilters = filters;
this.placeholder =
this.activeFilters.length <= 0
? $localize`Filter by account or tag...`
: '';
return this.fetchPortfolioDetails();
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe((portfolioDetails) => {
this.portfolioDetails = portfolioDetails;
this.initialize();
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => { .subscribe((state) => {
@ -111,52 +80,17 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
permissions.createOrder permissions.createOrder
); );
const accountFilters: Filter[] = this.user.accounts.map( this.holdings = undefined;
({ id, name }) => {
return {
id,
label: name,
type: 'ACCOUNT'
};
}
);
const assetClassFilters: Filter[] = []; this.fetchPortfolioDetails()
for (const assetClass of Object.keys(AssetClass)) { .pipe(takeUntil(this.unsubscribeSubject))
assetClassFilters.push({ .subscribe((portfolioDetails) => {
id: assetClass, this.portfolioDetails = portfolioDetails;
label: translate(assetClass),
type: 'ASSET_CLASS' this.initialize();
this.changeDetectorRef.markForCheck();
}); });
}
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => {
return {
id,
label: translate(name),
type: 'TAG'
};
});
this.allFilters = [
...accountFilters,
...assetClassFilters,
...tagFilters
];
if (this.user?.settings?.isExperimentalFeatures === true) {
this.holdings = undefined;
this.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioDetails) => {
this.portfolioDetails = portfolioDetails;
this.initialize();
this.changeDetectorRef.markForCheck();
});
}
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
@ -170,10 +104,7 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
private fetchPortfolioDetails() { private fetchPortfolioDetails() {
return this.dataService.fetchPortfolioDetails({ return this.dataService.fetchPortfolioDetails({
filters: filters: this.userService.getFilters()
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters()
}); });
} }

View File

@ -2,14 +2,6 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1> <h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Holdings</h1>
@if (!user?.settings?.isExperimentalFeatures) {
<gf-activities-filter
[allFilters]="allFilters"
[isLoading]="isLoading"
[placeholder]="placeholder"
(valueChanged)="filters$.next($event)"
/>
}
</div> </div>
</div> </div>
<div class="row"> <div class="row">

View File

@ -1,7 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module'; import { GfHoldingsTableModule } from '@ghostfolio/ui/holdings-table/holdings-table.module';
import { HoldingsPageRoutingModule } from './holdings-page-routing.module'; import { HoldingsPageRoutingModule } from './holdings-page-routing.module';
@ -11,7 +10,6 @@ import { HoldingsPageComponent } from './holdings-page.component';
declarations: [HoldingsPageComponent], declarations: [HoldingsPageComponent],
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesFilterModule,
GfHoldingsTableModule, GfHoldingsTableModule,
HoldingsPageRoutingModule, HoldingsPageRoutingModule,
MatButtonModule MatButtonModule

View File

@ -50,27 +50,25 @@ export class UserService extends ObservableStore<UserStoreState> {
const filters: Filter[] = []; const filters: Filter[] = [];
const user = this.getState().user; const user = this.getState().user;
if (user?.settings?.isExperimentalFeatures === true) { if (user.settings['filters.accounts']) {
if (user.settings['filters.accounts']) { filters.push({
filters.push({ id: user.settings['filters.accounts'][0],
id: user.settings['filters.accounts'][0], type: 'ACCOUNT'
type: 'ACCOUNT' });
}); }
}
if (user.settings['filters.assetClasses']) { if (user.settings['filters.assetClasses']) {
filters.push({ filters.push({
id: user.settings['filters.assetClasses'][0], id: user.settings['filters.assetClasses'][0],
type: 'ASSET_CLASS' type: 'ASSET_CLASS'
}); });
} }
if (user.settings['filters.tags']) { if (user.settings['filters.tags']) {
filters.push({ filters.push({
id: user.settings['filters.tags'][0], id: user.settings['filters.tags'][0],
type: 'TAG' type: 'TAG'
}); });
}
} }
return filters; return filters;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -379,6 +379,10 @@ ngx-skeleton-loader {
cursor: pointer; cursor: pointer;
} }
.gf-spacer {
flex: 1 1 auto;
}
.gf-table { .gf-table {
@include gf-table; @include gf-table;
} }

0
git-hooks/pre-commit Executable file → Normal file
View File

View File

@ -1,490 +0,0 @@
<div *ngIf="hasPermissionToCreateActivity" class="d-flex justify-content-end">
<button
class="align-items-center d-flex"
mat-stroked-button
(click)="onImport()"
>
<ion-icon class="mr-2" name="cloud-upload-outline" />
<ng-container i18n>Import Activities</ng-container>...
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="mx-1 no-min-width px-2"
mat-stroked-button
[matMenuTriggerFor]="activitiesMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical" />
</button>
<mat-menu #activitiesMenu="matMenu" xPosition="before">
<button
mat-menu-item
[disabled]="dataSource?.data.length === 0"
(click)="onImportDividends()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="color-wand-outline" />
<ng-container i18n>Import Dividends</ng-container>...
</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="dataSource?.data.length === 0"
(click)="onExport()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-download-outline" />
<span i18n>Export Activities</span>
</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="!hasDrafts"
(click)="onExportDrafts()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline" />
<span i18n>Export Drafts as ICS</span>
</span>
</button>
<button
class="align-items-center d-flex"
mat-menu-item
(click)="onDeleteAllActivities()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete all Activities</span>
</span>
</button>
</mat-menu>
</div>
<div class="activities">
<table
class="gf-table w-100"
mat-table
matSort
[dataSource]="dataSource"
[matSortActive]="sortColumn"
[matSortDirection]="sortDirection"
[matSortDisabled]="sortDisabled"
>
<ng-container matColumnDef="select" sticky>
<th *matHeaderCellDef class="px-1" mat-header-cell>
<mat-checkbox
color="primary"
[checked]="
areAllRowsSelected() && !hasErrors && selectedRows.hasValue()
"
[disabled]="hasErrors"
[indeterminate]="selectedRows.hasValue() && !areAllRowsSelected()"
(change)="$event ? toggleAllRows() : null"
></mat-checkbox>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<mat-checkbox
color="primary"
[checked]="element.error ? false : selectedRows.isSelected(element)"
[disabled]="element.error"
(change)="$event ? selectedRows.toggle(element) : null"
(click)="$event.stopPropagation()"
></mat-checkbox>
</td>
</ng-container>
<ng-container matColumnDef="importStatus">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n></ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div
*ngIf="element.error"
class="d-flex"
matTooltipPosition="above"
[matTooltip]="element.error.message"
>
<ion-icon class="text-danger" name="alert-circle-outline" />
</div>
</td>
</ng-container>
<ng-container matColumnDef="icon">
<th *matHeaderCellDef class="px-1" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<gf-symbol-icon
[dataSource]="element.SymbolProfile?.dataSource"
[symbol]="element.SymbolProfile?.symbol"
[tooltip]="element.SymbolProfile?.name"
/>
<div>{{ element.dataSource }}</div>
</td>
</ng-container>
<ng-container matColumnDef="nameWithSymbol">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="align-items-center d-flex line-height-1">
<div>
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span>
<span
*ngIf="element.isDraft"
class="badge badge-secondary ml-1"
i18n
>Draft</span
>
</div>
</div>
<div *ngIf="!isUUID(element.SymbolProfile?.symbol)">
<small class="text-muted">{{
element.SymbolProfile?.symbol | gfSymbol
}}</small>
</div>
</td>
</ng-container>
<ng-container matColumnDef="type">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Type</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<gf-activity-type [activityType]="element.type" />
</td>
</ng-container>
<ng-container matColumnDef="date">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Date</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
{{ element.date | date: defaultDateFormat }}
</div>
</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
mat-sort-header
>
<ng-container i18n>Quantity</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.quantity"
/>
</div>
</td>
</ng-container>
<ng-container matColumnDef="unitPrice">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
mat-sort-header
>
<ng-container i18n>Unit Price</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.unitPrice"
/>
</div>
</td>
</ng-container>
<ng-container matColumnDef="fee">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
mat-sort-header
>
<ng-container i18n>Fee</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.fee"
/>
</div>
</td>
</ng-container>
<ng-container matColumnDef="value">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
>
<ng-container i18n>Value</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.value"
/>
</div>
</td>
</ng-container>
<ng-container matColumnDef="currency">
<th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
<ng-container i18n>Currency</ng-container>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
{{ element.SymbolProfile?.currency }}
</td>
</ng-container>
<ng-container matColumnDef="valueInBaseCurrency">
<th
*matHeaderCellDef
class="d-lg-none d-xl-none justify-content-end px-1"
mat-header-cell
>
<ng-container i18n>Value</ng-container>
</th>
<td *matCellDef="let element" class="d-lg-none d-xl-none px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.valueInBaseCurrency"
/>
</div>
</td>
</ng-container>
<ng-container matColumnDef="account">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<span class="d-none d-lg-block" i18n>Account</span>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
<gf-symbol-icon
*ngIf="element.Account?.Platform?.url"
class="mr-1"
[tooltip]="element.Account?.Platform?.name"
[url]="element.Account?.Platform?.url"
/>
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
</div>
</td>
</ng-container>
<ng-container matColumnDef="comment">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1"
mat-header-cell
></th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<button
*ngIf="element.comment"
class="mx-1 no-min-width px-2"
mat-button
title="Note"
(click)="onOpenComment(element.comment); $event.stopPropagation()"
>
<ion-icon name="document-text-outline" />
</button>
</td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
<button
*ngIf="
!hasPermissionToCreateActivity && hasPermissionToExportActivities
"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="activitiesMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical" />
</button>
<mat-menu #activitiesMenu="matMenu" xPosition="before">
<button
*ngIf="hasPermissionToCreateActivity"
class="align-items-center d-flex"
mat-menu-item
(click)="onImport()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-upload-outline" />
<ng-container i18n>Import Activities</ng-container>...
</span>
</button>
<button
*ngIf="hasPermissionToCreateActivity"
mat-menu-item
[disabled]="dataSource?.data.length === 0"
(click)="onImportDividends()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="color-wand-outline" />
<ng-container i18n>Import Dividends</ng-container>...
</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="dataSource?.data.length === 0"
(click)="onExport()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="cloud-download-outline" />
<span i18n>Export Activities</span>
</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="!hasDrafts"
(click)="onExportDrafts()"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline" />
<span i18n>Export Drafts as ICS</span>
</span>
</button>
</mat-menu>
</th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<button
*ngIf="showActions"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="activityMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal" />
</button>
<mat-menu #activityMenu="matMenu" xPosition="before">
<button mat-menu-item (click)="onUpdateActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" />
<span i18n>Edit</span>
</span>
</button>
<button mat-menu-item (click)="onCloneActivity(element)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="copy-outline" />
<span i18n>Clone</span>
</span>
</button>
<button
mat-menu-item
[disabled]="!element.isDraft"
(click)="onExportDraft(element.id)"
>
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="calendar-clear-outline" />
<span i18n>Export Draft as ICS</span>
</span>
</button>
<button mat-menu-item (click)="onDeleteActivity(element.id)">
<span class="align-items-center d-flex">
<ion-icon class="mr-2" name="trash-outline" />
<span i18n>Delete</span>
</span>
</button>
</mat-menu>
</td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
mat-row
[ngClass]="{
'cursor-pointer':
hasPermissionToOpenDetails &&
!row.isDraft &&
row.type !== 'FEE' &&
row.type !== 'INTEREST' &&
row.type !== 'ITEM' &&
row.type !== 'LIABILITY'
}"
(click)="onClickActivity(row)"
></tr>
</table>
</div>
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
class="px-4 py-3"
[theme]="{
height: '1.5rem',
width: '100%'
}"
/>
<mat-paginator
[length]="totalItems"
[ngClass]="{
'd-none': (isLoading && !totalItems) || totalItems <= pageSize
}"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[showFirstLastButtons]="true"
(page)="onChangePage($event)"
></mat-paginator>
<div
*ngIf="
dataSource?.data.length === 0 && hasPermissionToCreateActivity && !isLoading
"
class="p-3 text-center"
>
<gf-no-transactions-info-indicator [hasBorder]="false" />
</div>

View File

@ -1,9 +0,0 @@
@import 'apps/client/src/styles/ghostfolio-style';
:host {
display: block;
.activities {
overflow-x: auto;
}
}

View File

@ -1,241 +0,0 @@
import { SelectionModel } from '@angular/cdk/collections';
import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
ViewChild
} from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort, Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { isUUID } from 'class-validator';
import { endOfToday, isAfter } from 'date-fns';
import { Subject, Subscription, takeUntil } from 'rxjs';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'gf-activities-table-lazy',
styleUrls: ['./activities-table-lazy.component.scss'],
templateUrl: './activities-table-lazy.component.html'
})
export class ActivitiesTableLazyComponent
implements AfterViewInit, OnChanges, OnDestroy, OnInit
{
@Input() baseCurrency: string;
@Input() dataSource: MatTableDataSource<Activity>;
@Input() deviceType: string;
@Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToExportActivities: boolean;
@Input() hasPermissionToOpenDetails = true;
@Input() locale: string;
@Input() pageIndex: number;
@Input() pageSize = DEFAULT_PAGE_SIZE;
@Input() showActions = true;
@Input() showCheckbox = false;
@Input() showFooter = true;
@Input() showNameColumn = true;
@Input() sortColumn: string;
@Input() sortDirection: SortDirection;
@Input() sortDisabled = false;
@Input() totalItems = Number.MAX_SAFE_INTEGER;
@Output() activityDeleted = new EventEmitter<string>();
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@Output() deleteAllActivities = new EventEmitter<void>();
@Output() export = new EventEmitter<void>();
@Output() exportDrafts = new EventEmitter<string[]>();
@Output() import = new EventEmitter<void>();
@Output() importDividends = new EventEmitter<UniqueAsset>();
@Output() pageChanged = new EventEmitter<PageEvent>();
@Output() selectedActivities = new EventEmitter<Activity[]>();
@Output() sortChanged = new EventEmitter<Sort>();
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
public defaultDateFormat: string;
public displayedColumns = [];
public endOfToday = endOfToday();
public hasDrafts = false;
public hasErrors = false;
public isAfter = isAfter;
public isLoading = true;
public isUUID = isUUID;
public routeQueryParams: Subscription;
public selectedRows = new SelectionModel<Activity>(true, []);
private unsubscribeSubject = new Subject<void>();
public constructor(private router: Router) {}
public ngOnInit() {
if (this.showCheckbox) {
this.toggleAllRows();
this.selectedRows.changed
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((selectedRows) => {
this.selectedActivities.emit(selectedRows.source.selected);
});
}
}
public ngAfterViewInit() {
this.sort.sortChange.subscribe((value: Sort) => {
this.sortChanged.emit(value);
});
}
public areAllRowsSelected() {
const numSelectedRows = this.selectedRows.selected.length;
const numTotalRows = this.dataSource.data.length;
return numSelectedRows === numTotalRows;
}
public ngOnChanges() {
this.defaultDateFormat = getDateFormatString(this.locale);
this.displayedColumns = [
'select',
'importStatus',
'icon',
'nameWithSymbol',
'type',
'date',
'quantity',
'unitPrice',
'fee',
'value',
'currency',
'valueInBaseCurrency',
'account',
'comment',
'actions'
];
if (!this.showCheckbox) {
this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'importStatus' && column !== 'select';
});
}
if (!this.showNameColumn) {
this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'nameWithSymbol';
});
}
if (this.dataSource) {
this.isLoading = false;
}
}
public onChangePage(page: PageEvent) {
this.pageChanged.emit(page);
}
public onClickActivity(activity: Activity) {
if (this.showCheckbox) {
if (!activity.error) {
this.selectedRows.toggle(activity);
}
} else if (
this.hasPermissionToOpenDetails &&
!activity.isDraft &&
activity.type !== 'FEE' &&
activity.type !== 'INTEREST' &&
activity.type !== 'ITEM' &&
activity.type !== 'LIABILITY'
) {
this.onOpenPositionDialog({
dataSource: activity.SymbolProfile.dataSource,
symbol: activity.SymbolProfile.symbol
});
}
}
public onCloneActivity(aActivity: OrderWithAccount) {
this.activityToClone.emit(aActivity);
}
public onDeleteActivity(aId: string) {
const confirmation = confirm(
$localize`Do you really want to delete this activity?`
);
if (confirmation) {
this.activityDeleted.emit(aId);
}
}
public onExport() {
this.export.emit();
}
public onExportDraft(aActivityId: string) {
this.exportDrafts.emit([aActivityId]);
}
public onExportDrafts() {
this.exportDrafts.emit(
this.dataSource.filteredData
.filter((activity) => {
return activity.isDraft;
})
.map((activity) => {
return activity.id;
})
);
}
public onDeleteAllActivities() {
this.deleteAllActivities.emit();
}
public onImport() {
this.import.emit();
}
public onImportDividends() {
this.importDividends.emit();
}
public onOpenComment(aComment: string) {
alert(aComment);
}
public onOpenPositionDialog({ dataSource, symbol }: UniqueAsset): void {
this.router.navigate([], {
queryParams: { dataSource, symbol, positionDetailDialog: true }
});
}
public onUpdateActivity(aActivity: OrderWithAccount) {
this.activityToUpdate.emit(aActivity);
}
public toggleAllRows() {
this.areAllRowsSelected()
? this.selectedRows.clear()
: this.dataSource.data.forEach((row) => this.selectedRows.select(row));
this.selectedActivities.emit(this.selectedRows.selected);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
}

View File

@ -1,42 +0,0 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatMenuModule } from '@angular/material/menu';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfActivityTypeModule } from '@ghostfolio/ui/activity-type';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { ActivitiesTableLazyComponent } from './activities-table-lazy.component';
@NgModule({
declarations: [ActivitiesTableLazyComponent],
exports: [ActivitiesTableLazyComponent],
imports: [
CommonModule,
GfActivityTypeModule,
GfNoTransactionsInfoModule,
GfSymbolIconModule,
GfSymbolModule,
GfValueModule,
MatButtonModule,
MatCheckboxModule,
MatMenuModule,
MatPaginatorModule,
MatSortModule,
MatTableModule,
MatTooltipModule,
NgxSkeletonLoaderModule,
RouterModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfActivitiesTableLazyModule {}

View File

@ -1,11 +1,3 @@
<gf-activities-filter
[allFilters]="allFilters"
[isLoading]="isLoading"
[ngClass]="{ 'd-none': !hasPermissionToFilter }"
[placeholder]="placeholder"
(valueChanged)="filters$.next($event)"
/>
<div *ngIf="hasPermissionToCreateActivity" class="d-flex justify-content-end"> <div *ngIf="hasPermissionToCreateActivity" class="d-flex justify-content-end">
<button <button
class="align-items-center d-flex" class="align-items-center d-flex"
@ -27,7 +19,7 @@
<mat-menu #activitiesMenu="matMenu" xPosition="before"> <mat-menu #activitiesMenu="matMenu" xPosition="before">
<button <button
mat-menu-item mat-menu-item
[disabled]="dataSource.data.length === 0" [disabled]="dataSource?.data.length === 0"
(click)="onImportDividends()" (click)="onImportDividends()"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -39,7 +31,7 @@
*ngIf="hasPermissionToExportActivities" *ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex" class="align-items-center d-flex"
mat-menu-item mat-menu-item
[disabled]="dataSource.data.length === 0" [disabled]="dataSource?.data.length === 0"
(click)="onExport()" (click)="onExport()"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -77,11 +69,12 @@
class="gf-table w-100" class="gf-table w-100"
mat-table mat-table
matSort matSort
matSortActive="date"
matSortDirection="desc"
[dataSource]="dataSource" [dataSource]="dataSource"
[matSortActive]="sortColumn"
[matSortDirection]="sortDirection"
[matSortDisabled]="sortDisabled"
> >
<ng-container matColumnDef="select"> <ng-container matColumnDef="select" sticky>
<th *matHeaderCellDef class="px-1" mat-header-cell> <th *matHeaderCellDef class="px-1" mat-header-cell>
<mat-checkbox <mat-checkbox
color="primary" color="primary"
@ -102,7 +95,6 @@
(click)="$event.stopPropagation()" (click)="$event.stopPropagation()"
></mat-checkbox> ></mat-checkbox>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<ng-container matColumnDef="importStatus"> <ng-container matColumnDef="importStatus">
@ -119,67 +111,26 @@
<ion-icon class="text-danger" name="alert-circle-outline" /> <ion-icon class="text-danger" name="alert-circle-outline" />
</div> </div>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<ng-container matColumnDef="count"> <ng-container matColumnDef="icon">
<th <th *matHeaderCellDef class="px-1" mat-header-cell></th>
*matHeaderCellDef <td *matCellDef="let element" class="px-1 text-center" mat-cell>
class="d-none d-lg-table-cell px-1 text-right" <gf-symbol-icon
i18n [dataSource]="element.SymbolProfile?.dataSource"
mat-header-cell [symbol]="element.SymbolProfile?.symbol"
></th> [tooltip]="element.SymbolProfile?.name"
<td />
*matCellDef="let element; let i = index" <div>{{ element.dataSource }}</div>
class="d-none d-lg-table-cell px-1 text-right"
mat-cell
>
{{
dataSource.data.length > pageSize
? dataSource.data.length - pageSize * pageIndex - i
: dataSource.data.length - i
}}
</td> </td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="date">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Date</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
{{ element.date | date: defaultDateFormat }}
</div>
</td>
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
</ng-container>
<ng-container matColumnDef="type">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Type</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<gf-activity-type [activityType]="element.type" />
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<ng-container matColumnDef="nameWithSymbol"> <ng-container matColumnDef="nameWithSymbol">
<th <th *matHeaderCellDef class="px-1" mat-header-cell>
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="SymbolProfile.symbol"
>
<ng-container i18n>Name</ng-container> <ng-container i18n>Name</ng-container>
</th> </th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center"> <div class="align-items-center d-flex line-height-1">
<div> <div>
<span class="text-truncate">{{ element.SymbolProfile?.name }}</span> <span class="text-truncate">{{ element.SymbolProfile?.name }}</span>
<span <span
@ -196,27 +147,25 @@
}}</small> }}</small>
</div> </div>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<ng-container matColumnDef="currency"> <ng-container matColumnDef="type">
<th <th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
*matHeaderCellDef <ng-container i18n>Type</ng-container>
class="d-none d-lg-table-cell px-1"
mat-header-cell
mat-sort-header="SymbolProfile.currency"
>
<ng-container i18n>Currency</ng-container>
</th> </th>
<td <td *matCellDef="let element" class="px-1" mat-cell>
*matCellDef="let element" <gf-activity-type [activityType]="element.type" />
class="d-none d-lg-table-cell px-1"
mat-cell
>
{{ element.SymbolProfile?.currency }}
</td> </td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell> </ng-container>
{{ baseCurrency }}
<ng-container matColumnDef="date">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Date</ng-container>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
{{ element.date | date: defaultDateFormat }}
</div>
</td> </td>
</ng-container> </ng-container>
@ -242,11 +191,6 @@
/> />
</div> </div>
</td> </td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container> </ng-container>
<ng-container matColumnDef="unitPrice"> <ng-container matColumnDef="unitPrice">
@ -271,11 +215,6 @@
/> />
</div> </div>
</td> </td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container> </ng-container>
<ng-container matColumnDef="fee"> <ng-container matColumnDef="fee">
@ -300,15 +239,6 @@
/> />
</div> </div>
</td> </td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : totalFees"
/>
</div>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="value"> <ng-container matColumnDef="value">
@ -316,7 +246,6 @@
*matHeaderCellDef *matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1" class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell mat-header-cell
mat-sort-header
> >
<ng-container i18n>Value</ng-container> <ng-container i18n>Value</ng-container>
</th> </th>
@ -333,16 +262,18 @@
/> />
</div> </div>
</td> </td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell> </ng-container>
<div class="d-flex justify-content-end">
<gf-value <ng-container matColumnDef="currency">
*ngIf="totalValue !== null" <th *matHeaderCellDef class="d-none d-lg-table-cell px-1" mat-header-cell>
[isAbsolute]="true" <ng-container i18n>Currency</ng-container>
[isCurrency]="true" </th>
[locale]="locale" <td
[value]="isLoading ? undefined : totalValue" *matCellDef="let element"
/> class="d-none d-lg-table-cell px-1"
</div> mat-cell
>
{{ element.SymbolProfile?.currency }}
</td> </td>
</ng-container> </ng-container>
@ -351,7 +282,6 @@
*matHeaderCellDef *matHeaderCellDef
class="d-lg-none d-xl-none justify-content-end px-1" class="d-lg-none d-xl-none justify-content-end px-1"
mat-header-cell mat-header-cell
mat-sort-header
> >
<ng-container i18n>Value</ng-container> <ng-container i18n>Value</ng-container>
</th> </th>
@ -364,26 +294,10 @@
/> />
</div> </div>
</td> </td>
<td *matFooterCellDef class="d-lg-none d-xl-none px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
*ngIf="totalValue !== null"
[isAbsolute]="true"
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : totalValue"
/>
</div>
</td>
</ng-container> </ng-container>
<ng-container matColumnDef="account"> <ng-container matColumnDef="account">
<th <th *matHeaderCellDef class="px-1" mat-header-cell>
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="Account.name"
>
<span class="d-none d-lg-block" i18n>Account</span> <span class="d-none d-lg-block" i18n>Account</span>
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
@ -397,7 +311,6 @@
<span class="d-none d-lg-block">{{ element.Account?.name }}</span> <span class="d-none d-lg-block">{{ element.Account?.name }}</span>
</div> </div>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<ng-container matColumnDef="comment"> <ng-container matColumnDef="comment">
@ -421,11 +334,6 @@
<ion-icon name="document-text-outline" /> <ion-icon name="document-text-outline" />
</button> </button>
</td> </td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container> </ng-container>
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
@ -456,7 +364,7 @@
<button <button
*ngIf="hasPermissionToCreateActivity" *ngIf="hasPermissionToCreateActivity"
mat-menu-item mat-menu-item
[disabled]="dataSource.data.length === 0" [disabled]="dataSource?.data.length === 0"
(click)="onImportDividends()" (click)="onImportDividends()"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -468,7 +376,7 @@
*ngIf="hasPermissionToExportActivities" *ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex" class="align-items-center d-flex"
mat-menu-item mat-menu-item
[disabled]="dataSource.data.length === 0" [disabled]="dataSource?.data.length === 0"
(click)="onExport()" (click)="onExport()"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
@ -531,7 +439,6 @@
</button> </button>
</mat-menu> </mat-menu>
</td> </td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container> </ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
@ -549,28 +456,9 @@
}" }"
(click)="onClickActivity(row)" (click)="onClickActivity(row)"
></tr> ></tr>
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row
[ngClass]="{
'd-none':
isLoading || dataSource.data.length === 0 || showFooter === false
}"
></tr>
</table> </table>
</div> </div>
<mat-paginator
[ngClass]="{
'd-none':
(isLoading && dataSource.data.length === 0) ||
dataSource.data.length <= pageSize
}"
[pageSize]="pageSize"
[showFirstLastButtons]="true"
(page)="onChangePage($event)"
></mat-paginator>
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading" *ngIf="isLoading"
animation="pulse" animation="pulse"
@ -581,9 +469,20 @@
}" }"
/> />
<mat-paginator
[length]="totalItems"
[ngClass]="{
'd-none': (isLoading && !totalItems) || totalItems <= pageSize
}"
[pageIndex]="pageIndex"
[pageSize]="pageSize"
[showFirstLastButtons]="true"
(page)="onChangePage($event)"
></mat-paginator>
<div <div
*ngIf=" *ngIf="
dataSource.data.length === 0 && hasPermissionToCreateActivity && !isLoading dataSource?.data.length === 0 && hasPermissionToCreateActivity && !isLoading
" "
class="p-3 text-center" class="p-3 text-center"
> >

View File

@ -5,15 +5,5 @@
.activities { .activities {
overflow-x: auto; overflow-x: auto;
.mat-mdc-table {
th {
::ng-deep {
.mat-sort-header-container {
justify-content: inherit;
}
}
}
}
} }
} }

View File

@ -1,5 +1,6 @@
import { SelectionModel } from '@angular/cdk/collections'; import { SelectionModel } from '@angular/cdk/collections';
import { import {
AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter, EventEmitter,
@ -11,20 +12,17 @@ import {
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatPaginator, PageEvent } from '@angular/material/paginator'; import { MatPaginator, PageEvent } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort'; import { MatSort, Sort, SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types'; import { OrderWithAccount } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
import Big from 'big.js';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
import { endOfToday, format, isAfter } from 'date-fns'; import { endOfToday, isAfter } from 'date-fns';
import { get, isNumber } from 'lodash'; import { Subject, Subscription, takeUntil } from 'rxjs';
import { Subject, Subscription, distinctUntilChanged, takeUntil } from 'rxjs';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -32,63 +30,56 @@ import { Subject, Subscription, distinctUntilChanged, takeUntil } from 'rxjs';
styleUrls: ['./activities-table.component.scss'], styleUrls: ['./activities-table.component.scss'],
templateUrl: './activities-table.component.html' templateUrl: './activities-table.component.html'
}) })
export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit { export class ActivitiesTableComponent
@Input() activities: Activity[]; implements AfterViewInit, OnChanges, OnDestroy, OnInit
{
@Input() baseCurrency: string; @Input() baseCurrency: string;
@Input() dataSource: MatTableDataSource<Activity>;
@Input() deviceType: string; @Input() deviceType: string;
@Input() hasPermissionToCreateActivity: boolean; @Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToExportActivities: boolean; @Input() hasPermissionToExportActivities: boolean;
@Input() hasPermissionToFilter = true;
@Input() hasPermissionToOpenDetails = true; @Input() hasPermissionToOpenDetails = true;
@Input() locale: string; @Input() locale: string;
@Input() pageIndex: number;
@Input() pageSize = DEFAULT_PAGE_SIZE; @Input() pageSize = DEFAULT_PAGE_SIZE;
@Input() showActions = true; @Input() showActions = true;
@Input() showCheckbox = false; @Input() showCheckbox = false;
@Input() showFooter = true; @Input() showFooter = true;
@Input() showNameColumn = true; @Input() showNameColumn = true;
@Input() sortColumn: string;
@Input() sortDirection: SortDirection;
@Input() sortDisabled = false;
@Input() totalItems = Number.MAX_SAFE_INTEGER;
@Output() activityDeleted = new EventEmitter<string>(); @Output() activityDeleted = new EventEmitter<string>();
@Output() activityToClone = new EventEmitter<OrderWithAccount>(); @Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>(); @Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@Output() deleteAllActivities = new EventEmitter<void>(); @Output() deleteAllActivities = new EventEmitter<void>();
@Output() export = new EventEmitter<string[]>(); @Output() export = new EventEmitter<void>();
@Output() exportDrafts = new EventEmitter<string[]>(); @Output() exportDrafts = new EventEmitter<string[]>();
@Output() import = new EventEmitter<void>(); @Output() import = new EventEmitter<void>();
@Output() importDividends = new EventEmitter<UniqueAsset>(); @Output() importDividends = new EventEmitter<UniqueAsset>();
@Output() pageChanged = new EventEmitter<PageEvent>();
@Output() selectedActivities = new EventEmitter<Activity[]>(); @Output() selectedActivities = new EventEmitter<Activity[]>();
@Output() sortChanged = new EventEmitter<Sort>();
@ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort; @ViewChild(MatSort) sort: MatSort;
public allFilters: Filter[];
public dataSource: MatTableDataSource<Activity> = new MatTableDataSource();
public defaultDateFormat: string; public defaultDateFormat: string;
public displayedColumns = []; public displayedColumns = [];
public endOfToday = endOfToday(); public endOfToday = endOfToday();
public filters$ = new Subject<Filter[]>();
public hasDrafts = false; public hasDrafts = false;
public hasErrors = false; public hasErrors = false;
public isAfter = isAfter; public isAfter = isAfter;
public isLoading = true; public isLoading = true;
public isUUID = isUUID; public isUUID = isUUID;
public pageIndex = 0;
public placeholder = '';
public routeQueryParams: Subscription; public routeQueryParams: Subscription;
public searchKeywords: string[] = [];
public selectedRows = new SelectionModel<Activity>(true, []); public selectedRows = new SelectionModel<Activity>(true, []);
public totalFees: number;
public totalValue: number;
private readonly SEARCH_STRING_SEPARATOR = ',';
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor(private router: Router) { public constructor(private router: Router) {}
this.filters$
.pipe(distinctUntilChanged(), takeUntil(this.unsubscribeSubject))
.subscribe((filters) => {
this.updateFilters(filters);
});
}
public ngOnInit() { public ngOnInit() {
if (this.showCheckbox) { if (this.showCheckbox) {
@ -101,6 +92,12 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
} }
} }
public ngAfterViewInit() {
this.sort.sortChange.subscribe((value: Sort) => {
this.sortChanged.emit(value);
});
}
public areAllRowsSelected() { public areAllRowsSelected() {
const numSelectedRows = this.selectedRows.selected.length; const numSelectedRows = this.selectedRows.selected.length;
const numTotalRows = this.dataSource.data.length; const numTotalRows = this.dataSource.data.length;
@ -108,13 +105,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
} }
public ngOnChanges() { public ngOnChanges() {
this.defaultDateFormat = getDateFormatString(this.locale);
this.displayedColumns = [ this.displayedColumns = [
'select', 'select',
'importStatus', 'importStatus',
'count', 'icon',
'date',
'type',
'nameWithSymbol', 'nameWithSymbol',
'type',
'date',
'quantity', 'quantity',
'unitPrice', 'unitPrice',
'fee', 'fee',
@ -126,11 +125,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
'actions' 'actions'
]; ];
if (this.showCheckbox) { if (!this.showCheckbox) {
this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'count';
});
} else {
this.displayedColumns = this.displayedColumns.filter((column) => { this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'importStatus' && column !== 'select'; return column !== 'importStatus' && column !== 'select';
}); });
@ -142,60 +137,13 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
}); });
} }
this.defaultDateFormat = getDateFormatString(this.locale); if (this.dataSource) {
this.isLoading = false;
if (this.activities) {
this.activities = this.activities.map((activity) => {
return {
...activity,
error: activity.error
? {
...activity.error,
message: translate(
`IMPORT_ACTIVITY_ERROR_${activity.error.code}`
)
}
: undefined
};
});
this.allFilters = this.getSearchableFieldValues(this.activities);
this.dataSource = new MatTableDataSource(this.activities);
this.dataSource.filterPredicate = (data, filter) => {
const filterableLabels = this.getFilterableValues(data).map(
({ label }) => {
return label.toLowerCase();
}
);
let includes = true;
for (const singleFilter of filter.split(this.SEARCH_STRING_SEPARATOR)) {
includes =
includes &&
filterableLabels.includes(singleFilter.trim().toLowerCase());
}
return includes;
};
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.dataSource.sortingDataAccessor = get;
this.updateFilters();
this.hasErrors = this.activities.some(({ error }) => {
return !!error;
});
} else {
this.hasErrors = false;
} }
} }
public onChangePage(page: PageEvent) { public onChangePage(page: PageEvent) {
this.pageIndex = page.pageIndex; this.pageChanged.emit(page);
this.totalFees = this.getTotalFees();
this.totalValue = this.getTotalValue();
} }
public onClickActivity(activity: Activity) { public onClickActivity(activity: Activity) {
@ -233,15 +181,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
} }
public onExport() { public onExport() {
if (this.searchKeywords.length > 0) { this.export.emit();
this.export.emit(
this.dataSource.filteredData.map((activity) => {
return activity.id;
})
);
} else {
this.export.emit();
}
} }
public onExportDraft(aActivityId: string) { public onExportDraft(aActivityId: string) {
@ -298,145 +238,4 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
private getFilterableValues(
activity: OrderWithAccount,
fieldValueMap: { [id: string]: Filter } = {}
): Filter[] {
if (activity.Account?.id) {
fieldValueMap[activity.Account.id] = {
id: activity.Account.id,
label: activity.Account.name,
type: 'ACCOUNT'
};
}
if (activity.SymbolProfile?.currency) {
fieldValueMap[activity.SymbolProfile.currency] = {
id: activity.SymbolProfile.currency,
label: activity.SymbolProfile.currency,
type: 'TAG'
};
}
if (
activity.SymbolProfile?.symbol &&
!isUUID(activity.SymbolProfile.symbol)
) {
fieldValueMap[activity.SymbolProfile.symbol] = {
id: activity.SymbolProfile.symbol,
label: activity.SymbolProfile.symbol,
type: 'SYMBOL'
};
}
fieldValueMap[activity.type] = {
id: activity.type,
label: activity.type,
type: 'TAG'
};
fieldValueMap[format(new Date(activity.date), 'yyyy')] = {
id: format(new Date(activity.date), 'yyyy'),
label: format(new Date(activity.date), 'yyyy'),
type: 'TAG'
};
return Object.values(fieldValueMap);
}
private getPaginatedData() {
if (this.dataSource.data.length > this.pageSize) {
const sortedData = this.dataSource.sortData(
this.dataSource.filteredData,
this.dataSource.sort
);
return sortedData.slice(
this.pageIndex * this.pageSize,
(this.pageIndex + 1) * this.pageSize
);
}
return this.dataSource.filteredData;
}
private getSearchableFieldValues(activities: OrderWithAccount[]): Filter[] {
const fieldValueMap: { [id: string]: Filter } = {};
for (const activity of activities) {
this.getFilterableValues(activity, fieldValueMap);
}
return Object.values(fieldValueMap);
}
private getTotalFees() {
let totalFees = new Big(0);
const paginatedData = this.getPaginatedData();
for (const activity of paginatedData) {
if (isNumber(activity.feeInBaseCurrency)) {
totalFees = totalFees.plus(activity.feeInBaseCurrency);
} else {
return null;
}
}
return totalFees.toNumber();
}
private getTotalValue() {
const paginatedData = this.getPaginatedData();
let totalValue = new Big(0);
for (const { type, valueInBaseCurrency } of paginatedData) {
if (isNumber(valueInBaseCurrency)) {
if (type === 'BUY' || type === 'ITEM') {
totalValue = totalValue.plus(valueInBaseCurrency);
} else if (
type === 'DIVIDEND' ||
type === 'FEE' ||
type === 'INTEREST' ||
type === 'LIABILITY' ||
type === 'SELL'
) {
return null;
}
} else {
return null;
}
}
return totalValue.toNumber();
}
private updateFilters(filters: Filter[] = []) {
this.isLoading = true;
this.dataSource.filter = filters
.map((filter) => {
return filter.label;
})
.join(this.SEARCH_STRING_SEPARATOR);
const lowercaseSearchKeywords = filters.map((filter) => {
return filter.label.trim().toLowerCase();
});
this.placeholder =
lowercaseSearchKeywords.length <= 0
? $localize`Filter by account, currency, symbol or type...`
: '';
this.searchKeywords = filters.map((filter) => {
return filter.label;
});
this.hasDrafts = this.dataSource.filteredData.some((activity) => {
return activity.isDraft === true;
});
this.totalFees = this.getTotalFees();
this.totalValue = this.getTotalValue();
this.isLoading = false;
}
} }

View File

@ -10,7 +10,6 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { GfActivityTypeModule } from '@ghostfolio/ui/activity-type'; import { GfActivityTypeModule } from '@ghostfolio/ui/activity-type';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -23,7 +22,6 @@ import { ActivitiesTableComponent } from './activities-table.component';
exports: [ActivitiesTableComponent], exports: [ActivitiesTableComponent],
imports: [ imports: [
CommonModule, CommonModule,
GfActivitiesFilterModule,
GfActivityTypeModule, GfActivityTypeModule,
GfNoTransactionsInfoModule, GfNoTransactionsInfoModule,
GfSymbolIconModule, GfSymbolIconModule,

View File

@ -22,7 +22,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { Filter, User } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types'; import { DateRange } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { Account, AssetClass, Tag } from '@prisma/client'; import { Account, AssetClass } from '@prisma/client';
import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs'; import { EMPTY, Observable, Subject, lastValueFrom } from 'rxjs';
import { import {
catchError, catchError,
@ -158,25 +158,6 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
}; };
}); });
this.filterForm.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ account, assetClass, tag }) => {
this.filtersChanged.emit([
{
id: account,
type: 'ACCOUNT'
},
{
id: assetClass,
type: 'ASSET_CLASS'
},
{
id: tag,
type: 'TAG'
}
]);
});
this.searchFormControl.valueChanges this.searchFormControl.valueChanges
.pipe( .pipe(
map((searchTerm) => { map((searchTerm) => {
@ -260,6 +241,25 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
} }
public onApplyFilters() {
this.filtersChanged.emit([
{
id: this.filterForm.get('account').value,
type: 'ACCOUNT'
},
{
id: this.filterForm.get('assetClass').value,
type: 'ASSET_CLASS'
},
{
id: this.filterForm.get('tag').value,
type: 'TAG'
}
]);
this.onCloseAssistant();
}
public onChangeDateRange(dateRangeString: string) { public onChangeDateRange(dateRangeString: string) {
this.dateRangeChanged.emit(dateRangeString as DateRange); this.dateRangeChanged.emit(dateRangeString as DateRange);
} }

View File

@ -87,11 +87,8 @@
</div> </div>
</div> </div>
<form [formGroup]="filterForm"> <form [formGroup]="filterForm">
<div <ng-container *ngIf="!(isLoading || searchFormControl.value)">
*ngIf="!(isLoading || searchFormControl.value) && user?.settings?.isExperimentalFeatures" <div class="date-range-selector-container p-3">
class="filter-container p-3"
>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Date Range</mat-label> <mat-label i18n>Date Range</mat-label>
<mat-select <mat-select
@ -104,62 +101,72 @@
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mb-3"> <div class="p-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <div class="mb-3">
<mat-label i18n>Accounts</mat-label> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select formControlName="account"> <mat-label i18n>Accounts</mat-label>
<mat-option [value]="null"></mat-option> <mat-select formControlName="account">
@for (account of accounts; track account.id) { <mat-option [value]="null"></mat-option>
<mat-option [value]="account.id"> @for (account of accounts; track account.id) {
<div class="d-flex"> <mat-option [value]="account.id">
<gf-symbol-icon <div class="d-flex">
*ngIf="account.Platform?.url" <gf-symbol-icon
class="mr-1" *ngIf="account.Platform?.url"
[tooltip]="account.Platform?.name" class="mr-1"
[url]="account.Platform?.url" [tooltip]="account.Platform?.name"
/><span>{{ account.name }}</span> [url]="account.Platform?.url"
</div> /><span>{{ account.name }}</span>
</mat-option> </div>
} </mat-option>
</mat-select> }
</mat-form-field> </mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Tags</mat-label>
<mat-select formControlName="tag">
<mat-option [value]="null"></mat-option>
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Classes</mat-label>
<mat-select formControlName="assetClass">
<mat-option [value]="null"></mat-option>
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id"
>{{ assetClass.label }}</mat-option
>
}
</mat-select>
</mat-form-field>
</div>
<div class="d-flex w-100">
<button
i18n
mat-button
[disabled]="!hasFilter(filterForm.value)"
(click)="onResetFilters()"
>
Reset Filters
</button>
<span class="gf-spacer"></span>
<button
color="primary"
i18n
mat-flat-button
[disabled]="!filterForm.dirty"
(click)="onApplyFilters()"
>
Apply Filters
</button>
</div>
</div> </div>
<div class="mb-3"> </ng-container>
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Tags</mat-label>
<mat-select formControlName="tag">
<mat-option [value]="null"></mat-option>
@for (tag of tags; track tag.id) {
<mat-option [value]="tag.id">{{ tag.label }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="mb-3">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Classes</mat-label>
<mat-select formControlName="assetClass">
<mat-option [value]="null"></mat-option>
@for (assetClass of assetClasses; track assetClass.id) {
<mat-option [value]="assetClass.id"
>{{ assetClass.label }}</mat-option
>
}
</mat-select>
</mat-form-field>
</div>
<div>
<button
class="w-100"
color="primary"
i18n
mat-flat-button
[disabled]="!hasFilter(filterForm.value)"
(click)="onResetFilters()"
>
Reset Filters
</button>
</div>
</div>
</form> </form>
</div> </div>

View File

@ -1,16 +1,8 @@
:host { :host {
display: block; display: block;
.filter-container { .date-range-selector-container {
.mat-mdc-tab-group { border-bottom: 1px solid rgba(var(--dark-dividers));
max-height: 20vh;
}
::ng-deep {
label {
margin-bottom: 0;
}
}
} }
.result-container { .result-container {
@ -35,6 +27,10 @@
} }
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {
.date-range-selector-container {
border-color: rgba(var(--light-dividers));
}
.search-container { .search-container {
border-color: rgba(var(--light-dividers)); border-color: rgba(var(--light-dividers));

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.48.1", "version": "2.49.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -133,7 +133,7 @@
"svgmap": "2.6.0", "svgmap": "2.6.0",
"twitter-api-v2": "1.14.2", "twitter-api-v2": "1.14.2",
"uuid": "9.0.1", "uuid": "9.0.1",
"yahoo-finance2": "2.9.0", "yahoo-finance2": "2.9.1",
"zone.js": "0.14.2" "zone.js": "0.14.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -20184,10 +20184,10 @@ y18n@^5.0.5:
resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"
integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==
yahoo-finance2@2.9.0: yahoo-finance2@2.9.1:
version "2.9.0" version "2.9.1"
resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.9.0.tgz#7842580de36606197f7d64897dd2e5e55b9371d3" resolved "https://registry.yarnpkg.com/yahoo-finance2/-/yahoo-finance2-2.9.1.tgz#43e22465403f48c688ff8e762f3894aac8014d70"
integrity sha512-Q1UhB5uA0Uj2bBcSDqsZLt0tCxoHwrWCuvu4NMUgioyN8dlpq8ppbdKhZlzTD9ipIyKSgqG5TT7IlwB1x6eHZA== integrity sha512-s+i5arE6+zUwHRJnze4EsU5aCTmsMFKFeBc9sMzSceDOjH+BSeEZG9twMYtWlSCjKbWLCmUEUCxtH1fvcq+f6Q==
dependencies: dependencies:
"@types/tough-cookie" "^4.0.2" "@types/tough-cookie" "^4.0.2"
ajv "8.10.0" ajv "8.10.0"