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

View File

@ -25,7 +25,7 @@
class="h-100"
[currency]="user?.settings?.baseCurrency"
[historicalDataItems]="historicalDataItems"
[isInPercent]="hasImpersonationId || user.settings.isRestrictedView"
[isInPercent]="data.hasImpersonationId || user.settings.isRestrictedView"
[isLoading]="isLoadingChart"
[locale]="user?.settings?.locale"
/>
@ -86,13 +86,12 @@
<ion-icon name="swap-vertical-outline" />
<div class="d-none d-sm-block ml-2" i18n>Activities</div>
</ng-template>
<gf-activities-table-lazy
*ngIf="user?.settings?.isExperimentalFeatures === true"
<gf-activities-table
[baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="data.deviceType"
[hasPermissionToCreateActivity]="false"
[hasPermissionToExportActivities]="!hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToExportActivities]="!data.hasImpersonationId && !user.settings.isRestrictedView"
[hasPermissionToFilter]="false"
[hasPermissionToOpenDetails]="false"
[locale]="user?.settings?.locale"
@ -103,19 +102,6 @@
(export)="onExport()"
(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>
<ng-template mat-tab-label>
@ -126,7 +112,7 @@
[accountBalances]="accountBalances"
[accountId]="data.accountId"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
[showActions]="!data.hasImpersonationId && hasPermissionToDeleteAccountBalance && !user.settings.isRestrictedView"
(accountBalanceDeleted)="onDeleteAccountBalance($event)"
/>
</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 { GfInvestmentChartModule } from '@ghostfolio/client/components/investment-chart/investment-chart.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 { GfValueModule } from '@ghostfolio/ui/value';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -21,7 +20,6 @@ import { AccountDetailDialog } from './account-detail-dialog.component';
CommonModule,
GfAccountBalancesModule,
GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfHoldingsTableModule,

View File

@ -6,11 +6,12 @@
mat-button
[ngClass]="{ 'w-100': hasTabs }"
[routerLink]="['/']"
(click)="onLogoClick()"
>
<gf-logo class="px-2" [label]="pageTitle" />
</a>
</div>
<span class="spacer"></span>
<span class="gf-spacer"></span>
<ul class="alig-items-center d-flex list-inline m-0 px-2">
<li class="list-inline-item">
<a
@ -119,11 +120,7 @@
[matMenuTriggerRestoreFocus]="false"
(menuOpened)="onOpenAssistant()"
>
@if (user?.settings?.isExperimentalFeatures) {
<ion-icon class="rotate-90" name="options-outline" />
} @else {
<ion-icon name="search-outline" />
}
<ion-icon class="rotate-90" name="options-outline" />
</button>
<mat-menu
#assistantMenu="matMenu"
@ -324,7 +321,7 @@
/>
</a>
</div>
<span class="spacer"></span>
<span class="gf-spacer"></span>
<ul class="alig-items-center d-flex list-inline m-0 px-2">
<li class="list-inline-item">
<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 { 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 { LayoutService } from '@ghostfolio/client/core/layout.service';
import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import {
@ -89,6 +90,7 @@ export class HeaderComponent implements OnChanges {
private dataService: DataService,
private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService,
private router: Router,
private settingsStorageService: SettingsStorageService,
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() {
this.isMenuOpen = false;
}

View File

@ -1,12 +1,4 @@
<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="align-items-center col-xs-12 col-md-8 offset-md-2">
<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 { DataService } from '@ghostfolio/client/services/data.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 {
LineChartItem,
@ -43,6 +44,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
private dataService: DataService,
private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService,
private userService: UserService
) {
this.userService.stateChanged
@ -73,6 +75,12 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.changeDetectorRef.markForCheck();
});
this.layoutService.shouldReloadContent$
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
this.update();
});
this.showDetails =
!this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN';

View File

@ -96,17 +96,6 @@
[showDetails]="showDetails"
[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>
</ng-template>

View File

@ -3,7 +3,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
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 { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
@ -16,7 +15,6 @@ import { HomeOverviewComponent } from './home-overview.component';
GfLineChartModule,
GfNoTransactionsInfoModule,
GfPortfolioPerformanceModule,
GfToggleModule,
MatButtonModule,
RouterModule
],

View File

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

View File

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

View File

@ -5,7 +5,6 @@ import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.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 { GfDataProviderCreditsModule } from '@ghostfolio/ui/data-provider-credits/data-provider-credits.module';
import { GfLineChartModule } from '@ghostfolio/ui/line-chart/line-chart.module';
@ -20,7 +19,6 @@ import { PositionDetailDialog } from './position-detail-dialog.component';
imports: [
CommonModule,
GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDataProviderCreditsModule,
GfDialogFooterModule,
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() {
if (this.user?.settings?.isExperimentalFeatures === true) {
this.dataService
.fetchActivities({
filters: this.userService.getFilters(),
skip: this.pageIndex * this.pageSize,
sortColumn: this.sortColumn,
sortDirection: this.sortDirection,
take: this.pageSize
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities, count }) => {
this.dataSource = new MatTableDataSource(activities);
this.totalItems = count;
this.dataService
.fetchActivities({
filters: this.userService.getFilters(),
skip: this.pageIndex * this.pageSize,
sortColumn: this.sortColumn,
sortDirection: this.sortDirection,
take: this.pageSize
})
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(({ activities, count }) => {
this.dataSource = new MatTableDataSource(activities);
this.totalItems = count;
if (this.hasPermissionToCreateActivity && this.totalItems <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
if (this.hasPermissionToCreateActivity && this.totalItems <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
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();
});
}
this.changeDetectorRef.markForCheck();
});
}
public onChangePage(page: PageEvent) {

View File

@ -2,8 +2,7 @@
<div class="mb-3 row">
<div class="col">
<h1 class="d-none d-sm-block h3 mb-3 text-center" i18n>Activities</h1>
<gf-activities-table-lazy
*ngIf="user?.settings?.isExperimentalFeatures === true"
<gf-activities-table
[baseCurrency]="user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="deviceType"
@ -27,24 +26,6 @@
(pageChanged)="onChangePage($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>

View File

@ -4,7 +4,6 @@ import { MatButtonModule } from '@angular/material/button';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { RouterModule } from '@angular/router';
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 { ActivitiesPageRoutingModule } from './activities-page-routing.module';
@ -18,7 +17,6 @@ import { GfImportActivitiesDialogModule } from './import-activities-dialog/impor
ActivitiesPageRoutingModule,
CommonModule,
GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfCreateOrUpdateActivityDialogModule,
GfImportActivitiesDialogModule,
MatButtonModule,

View File

@ -116,8 +116,8 @@
</ng-template>
<div class="pt-3">
<ng-container *ngIf="errorMessages?.length === 0; else errorMessage">
<gf-activities-table-lazy
*ngIf="importStep === 1 && data?.user?.settings?.isExperimentalFeatures === true"
<gf-activities-table
*ngIf="importStep === 1"
[baseCurrency]="data?.user?.settings?.baseCurrency"
[dataSource]="dataSource"
[deviceType]="data?.deviceType"
@ -137,23 +137,6 @@
[totalItems]="totalItems"
(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">
<button mat-button (click)="onReset(stepper)">
<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 { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.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';
@ -23,7 +22,6 @@ import { ImportActivitiesDialog } from './import-activities-dialog.component';
CommonModule,
FormsModule,
GfActivitiesTableModule,
GfActivitiesTableLazyModule,
GfDialogFooterModule,
GfDialogHeaderModule,
GfFileDropModule,

View File

@ -11,7 +11,6 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
import { prettifySymbol } from '@ghostfolio/common/helper';
import {
Filter,
PortfolioDetails,
PortfolioPosition,
UniqueAsset,
@ -24,7 +23,7 @@ import { Account, AssetClass, DataSource, Platform } from '@prisma/client';
import { isNumber } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-allocations-page',
@ -38,8 +37,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: number;
};
};
public activeFilters: Filter[] = [];
public allFilters: Filter[];
public continents: {
[code: string]: { name: string; value: number };
};
@ -47,7 +44,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
[code: string]: { name: string; value: number };
};
public deviceType: string;
public filters$ = new Subject<Filter[]>();
public hasImpersonationId: boolean;
public isLoading = false;
public markets: {
@ -60,7 +56,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
value: number;
};
};
public placeholder = '';
public platforms: {
[id: string]: Pick<Platform, 'name'> & {
id: string;
@ -135,98 +130,34 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
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
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (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.hasImpersonationId || this.user.settings.isRestrictedView
? `{0}%`
: `{0} ${this.user?.settings?.baseCurrency}`;
if (this.user?.settings?.isExperimentalFeatures === true) {
this.isLoading = true;
this.isLoading = true;
this.initialize();
this.initialize();
this.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioDetails) => {
this.initialize();
this.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioDetails) => {
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();
}
@ -273,10 +204,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
private fetchPortfolioDetails() {
return this.dataService.fetchPortfolioDetails({
filters:
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters()
filters: this.userService.getFilters()
});
}

View File

@ -2,14 +2,6 @@
<div class="row">
<div class="col">
<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 class="row">

View File

@ -4,7 +4,6 @@ import { MatCardModule } from '@angular/material/card';
import { MatDialogModule } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar';
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 { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value';
@ -17,7 +16,6 @@ import { AllocationsPageComponent } from './allocations-page.component';
imports: [
AllocationsPageRoutingModule,
CommonModule,
GfActivitiesFilterModule,
GfPortfolioProportionChartModule,
GfPremiumIndicatorModule,
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 { UserService } from '@ghostfolio/client/services/user/user.service';
import {
Filter,
HistoricalDataItem,
PortfolioInvestments,
PortfolioPerformance,
@ -17,14 +16,14 @@ import {
} from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
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 { AssetClass, DataSource, SymbolProfile } from '@prisma/client';
import { DataSource, SymbolProfile } from '@prisma/client';
import { differenceInDays } from 'date-fns';
import { isNumber, sortBy } from 'lodash';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-analysis-page',
@ -32,8 +31,6 @@ import { distinctUntilChanged, map, takeUntil } from 'rxjs/operators';
templateUrl: './analysis-page.html'
})
export class AnalysisPageComponent implements OnDestroy, OnInit {
public activeFilters: Filter[] = [];
public allFilters: Filter[];
public benchmarkDataItems: HistoricalDataItem[] = [];
public benchmarks: Partial<SymbolProfile>[];
public bottom3: Position[];
@ -42,7 +39,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public deviceType: string;
public dividendsByGroup: InvestmentItem[];
public dividendTimelineDataLabel = $localize`Dividend`;
public filters$ = new Subject<Filter[]>();
public firstOrderDate: Date;
public hasImpersonationId: boolean;
public investments: InvestmentItem[];
@ -58,7 +54,6 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
public performance: PortfolioPerformance;
public performanceDataItems: HistoricalDataItem[];
public performanceDataItemsInPercentage: HistoricalDataItem[];
public placeholder = '';
public portfolioEvolutionDataLabel = $localize`Investment`;
public streaks: PortfolioInvestments['streaks'];
public top3: Position[];
@ -118,61 +113,12 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
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
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
if (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();
}
});
@ -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) {
this.mode = aMode;
this.fetchDividendsAndInvestments();
@ -227,10 +155,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
private fetchDividendsAndInvestments() {
this.dataService
.fetchDividends({
filters:
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters(),
filters: this.userService.getFilters(),
groupBy: this.mode,
range: this.user?.settings?.dateRange
})
@ -243,10 +168,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService
.fetchInvestments({
filters:
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters(),
filters: this.userService.getFilters(),
groupBy: this.mode,
range: this.user?.settings?.dateRange
})
@ -321,10 +243,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService
.fetchPortfolioPerformance({
filters:
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters(),
filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
@ -370,10 +289,7 @@ export class AnalysisPageComponent implements OnDestroy, OnInit {
this.dataService
.fetchPositions({
filters:
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters(),
filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))

View File

@ -1,21 +1,5 @@
<div class="container">
<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="col-lg">
<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 { UserService } from '@ghostfolio/client/services/user/user.service';
import {
Filter,
PortfolioDetails,
PortfolioPosition,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { translate } from '@ghostfolio/ui/i18n';
import { AssetClass, DataSource } from '@prisma/client';
import { DataSource } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
import { takeUntil } from 'rxjs/operators';
@Component({
selector: 'gf-holdings-page',
@ -25,15 +23,11 @@ import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
templateUrl: './holdings-page.html'
})
export class HoldingsPageComponent implements OnDestroy, OnInit {
public activeFilters: Filter[] = [];
public allFilters: Filter[];
public deviceType: string;
public filters$ = new Subject<Filter[]>();
public hasImpersonationId: boolean;
public hasPermissionToCreateOrder: boolean;
public holdings: PortfolioPosition[];
public isLoading = false;
public placeholder = '';
public portfolioDetails: PortfolioDetails;
public user: User;
@ -75,31 +69,6 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
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
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((state) => {
@ -111,52 +80,17 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
permissions.createOrder
);
const accountFilters: Filter[] = this.user.accounts.map(
({ id, name }) => {
return {
id,
label: name,
type: 'ACCOUNT'
};
}
);
this.holdings = undefined;
const assetClassFilters: Filter[] = [];
for (const assetClass of Object.keys(AssetClass)) {
assetClassFilters.push({
id: assetClass,
label: translate(assetClass),
type: 'ASSET_CLASS'
this.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((portfolioDetails) => {
this.portfolioDetails = portfolioDetails;
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();
}
@ -170,10 +104,7 @@ export class HoldingsPageComponent implements OnDestroy, OnInit {
private fetchPortfolioDetails() {
return this.dataService.fetchPortfolioDetails({
filters:
this.activeFilters.length > 0
? this.activeFilters
: this.userService.getFilters()
filters: this.userService.getFilters()
});
}

View File

@ -2,14 +2,6 @@
<div class="row">
<div class="col">
<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 class="row">

View File

@ -1,7 +1,6 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
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 { HoldingsPageRoutingModule } from './holdings-page-routing.module';
@ -11,7 +10,6 @@ import { HoldingsPageComponent } from './holdings-page.component';
declarations: [HoldingsPageComponent],
imports: [
CommonModule,
GfActivitiesFilterModule,
GfHoldingsTableModule,
HoldingsPageRoutingModule,
MatButtonModule

View File

@ -50,27 +50,25 @@ export class UserService extends ObservableStore<UserStoreState> {
const filters: Filter[] = [];
const user = this.getState().user;
if (user?.settings?.isExperimentalFeatures === true) {
if (user.settings['filters.accounts']) {
filters.push({
id: user.settings['filters.accounts'][0],
type: 'ACCOUNT'
});
}
if (user.settings['filters.accounts']) {
filters.push({
id: user.settings['filters.accounts'][0],
type: 'ACCOUNT'
});
}
if (user.settings['filters.assetClasses']) {
filters.push({
id: user.settings['filters.assetClasses'][0],
type: 'ASSET_CLASS'
});
}
if (user.settings['filters.assetClasses']) {
filters.push({
id: user.settings['filters.assetClasses'][0],
type: 'ASSET_CLASS'
});
}
if (user.settings['filters.tags']) {
filters.push({
id: user.settings['filters.tags'][0],
type: 'TAG'
});
}
if (user.settings['filters.tags']) {
filters.push({
id: user.settings['filters.tags'][0],
type: 'TAG'
});
}
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;
}
.gf-spacer {
flex: 1 1 auto;
}
.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">
<button
class="align-items-center d-flex"
@ -27,7 +19,7 @@
<mat-menu #activitiesMenu="matMenu" xPosition="before">
<button
mat-menu-item
[disabled]="dataSource.data.length === 0"
[disabled]="dataSource?.data.length === 0"
(click)="onImportDividends()"
>
<span class="align-items-center d-flex">
@ -39,7 +31,7 @@
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="dataSource.data.length === 0"
[disabled]="dataSource?.data.length === 0"
(click)="onExport()"
>
<span class="align-items-center d-flex">
@ -77,11 +69,12 @@
class="gf-table w-100"
mat-table
matSort
matSortActive="date"
matSortDirection="desc"
[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>
<mat-checkbox
color="primary"
@ -102,7 +95,6 @@
(click)="$event.stopPropagation()"
></mat-checkbox>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="importStatus">
@ -119,67 +111,26 @@
<ion-icon class="text-danger" name="alert-circle-outline" />
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="count">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
></th>
<td
*matCellDef="let element; let i = index"
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
}}
<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>
<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 matColumnDef="nameWithSymbol">
<th
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="SymbolProfile.symbol"
>
<th *matHeaderCellDef class="px-1" mat-header-cell>
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
<div class="d-flex align-items-center">
<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
@ -196,27 +147,25 @@
}}</small>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="currency">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1"
mat-header-cell
mat-sort-header="SymbolProfile.currency"
>
<ng-container i18n>Currency</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="d-none d-lg-table-cell px-1"
mat-cell
>
{{ element.SymbolProfile?.currency }}
<td *matCellDef="let element" class="px-1" mat-cell>
<gf-activity-type [activityType]="element.type" />
</td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
{{ baseCurrency }}
</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>
@ -242,11 +191,6 @@
/>
</div>
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="unitPrice">
@ -271,11 +215,6 @@
/>
</div>
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="fee">
@ -300,15 +239,6 @@
/>
</div>
</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 matColumnDef="value">
@ -316,7 +246,6 @@
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
mat-header-cell
mat-sort-header
>
<ng-container i18n>Value</ng-container>
</th>
@ -333,16 +262,18 @@
/>
</div>
</td>
<td *matFooterCellDef class="d-none d-lg-table-cell 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>
</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>
@ -351,7 +282,6 @@
*matHeaderCellDef
class="d-lg-none d-xl-none justify-content-end px-1"
mat-header-cell
mat-sort-header
>
<ng-container i18n>Value</ng-container>
</th>
@ -364,26 +294,10 @@
/>
</div>
</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 matColumnDef="account">
<th
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="Account.name"
>
<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>
@ -397,7 +311,6 @@
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="comment">
@ -421,11 +334,6 @@
<ion-icon name="document-text-outline" />
</button>
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="actions" stickyEnd>
@ -456,7 +364,7 @@
<button
*ngIf="hasPermissionToCreateActivity"
mat-menu-item
[disabled]="dataSource.data.length === 0"
[disabled]="dataSource?.data.length === 0"
(click)="onImportDividends()"
>
<span class="align-items-center d-flex">
@ -468,7 +376,7 @@
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
[disabled]="dataSource.data.length === 0"
[disabled]="dataSource?.data.length === 0"
(click)="onExport()"
>
<span class="align-items-center d-flex">
@ -531,7 +439,6 @@
</button>
</mat-menu>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
@ -549,28 +456,9 @@
}"
(click)="onClickActivity(row)"
></tr>
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row
[ngClass]="{
'd-none':
isLoading || dataSource.data.length === 0 || showFooter === false
}"
></tr>
</table>
</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
*ngIf="isLoading"
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
*ngIf="
dataSource.data.length === 0 && hasPermissionToCreateActivity && !isLoading
dataSource?.data.length === 0 && hasPermissionToCreateActivity && !isLoading
"
class="p-3 text-center"
>

View File

@ -5,15 +5,5 @@
.activities {
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 {
AfterViewInit,
ChangeDetectionStrategy,
Component,
EventEmitter,
@ -11,20 +12,17 @@ import {
ViewChild
} from '@angular/core';
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 { 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 { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { OrderWithAccount } from '@ghostfolio/common/types';
import { translate } from '@ghostfolio/ui/i18n';
import Big from 'big.js';
import { isUUID } from 'class-validator';
import { endOfToday, format, isAfter } from 'date-fns';
import { get, isNumber } from 'lodash';
import { Subject, Subscription, distinctUntilChanged, takeUntil } from 'rxjs';
import { endOfToday, isAfter } from 'date-fns';
import { Subject, Subscription, takeUntil } from 'rxjs';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@ -32,63 +30,56 @@ import { Subject, Subscription, distinctUntilChanged, takeUntil } from 'rxjs';
styleUrls: ['./activities-table.component.scss'],
templateUrl: './activities-table.component.html'
})
export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
@Input() activities: Activity[];
export class ActivitiesTableComponent
implements AfterViewInit, OnChanges, OnDestroy, OnInit
{
@Input() baseCurrency: string;
@Input() dataSource: MatTableDataSource<Activity>;
@Input() deviceType: string;
@Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToExportActivities: boolean;
@Input() hasPermissionToFilter = true;
@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<string[]>();
@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 allFilters: Filter[];
public dataSource: MatTableDataSource<Activity> = new MatTableDataSource();
public defaultDateFormat: string;
public displayedColumns = [];
public endOfToday = endOfToday();
public filters$ = new Subject<Filter[]>();
public hasDrafts = false;
public hasErrors = false;
public isAfter = isAfter;
public isLoading = true;
public isUUID = isUUID;
public pageIndex = 0;
public placeholder = '';
public routeQueryParams: Subscription;
public searchKeywords: string[] = [];
public selectedRows = new SelectionModel<Activity>(true, []);
public totalFees: number;
public totalValue: number;
private readonly SEARCH_STRING_SEPARATOR = ',';
private unsubscribeSubject = new Subject<void>();
public constructor(private router: Router) {
this.filters$
.pipe(distinctUntilChanged(), takeUntil(this.unsubscribeSubject))
.subscribe((filters) => {
this.updateFilters(filters);
});
}
public constructor(private router: Router) {}
public ngOnInit() {
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() {
const numSelectedRows = this.selectedRows.selected.length;
const numTotalRows = this.dataSource.data.length;
@ -108,13 +105,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
}
public ngOnChanges() {
this.defaultDateFormat = getDateFormatString(this.locale);
this.displayedColumns = [
'select',
'importStatus',
'count',
'date',
'type',
'icon',
'nameWithSymbol',
'type',
'date',
'quantity',
'unitPrice',
'fee',
@ -126,11 +125,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
'actions'
];
if (this.showCheckbox) {
this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'count';
});
} else {
if (!this.showCheckbox) {
this.displayedColumns = this.displayedColumns.filter((column) => {
return column !== 'importStatus' && column !== 'select';
});
@ -142,60 +137,13 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
});
}
this.defaultDateFormat = getDateFormatString(this.locale);
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;
if (this.dataSource) {
this.isLoading = false;
}
}
public onChangePage(page: PageEvent) {
this.pageIndex = page.pageIndex;
this.totalFees = this.getTotalFees();
this.totalValue = this.getTotalValue();
this.pageChanged.emit(page);
}
public onClickActivity(activity: Activity) {
@ -233,15 +181,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
}
public onExport() {
if (this.searchKeywords.length > 0) {
this.export.emit(
this.dataSource.filteredData.map((activity) => {
return activity.id;
})
);
} else {
this.export.emit();
}
this.export.emit();
}
public onExportDraft(aActivityId: string) {
@ -298,145 +238,4 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy, OnInit {
this.unsubscribeSubject.next();
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 { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.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 { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfValueModule } from '@ghostfolio/ui/value';
@ -23,7 +22,6 @@ import { ActivitiesTableComponent } from './activities-table.component';
exports: [ActivitiesTableComponent],
imports: [
CommonModule,
GfActivitiesFilterModule,
GfActivityTypeModule,
GfNoTransactionsInfoModule,
GfSymbolIconModule,

View File

@ -22,7 +22,7 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { Filter, User } from '@ghostfolio/common/interfaces';
import { DateRange } from '@ghostfolio/common/types';
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 {
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
.pipe(
map((searchTerm) => {
@ -260,6 +241,25 @@ export class AssistantComponent implements OnChanges, OnDestroy, OnInit {
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) {
this.dateRangeChanged.emit(dateRangeString as DateRange);
}

View File

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

View File

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

View File

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

View File

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