diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index e5d0dd6da..9fa5df017 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -5,7 +5,7 @@ import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; import { Filter, PortfolioPosition, User } from '@ghostfolio/common/interfaces'; import { InternalRoute } from '@ghostfolio/common/routes/interfaces/internal-route.interface'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; -import { DateRange } from '@ghostfolio/common/types'; +import { AccountWithValue, DateRange } from '@ghostfolio/common/types'; import { FocusKeyManager } from '@angular/cdk/a11y'; import { @@ -25,19 +25,14 @@ import { ViewChild, ViewChildren } from '@angular/core'; -import { - FormBuilder, - FormControl, - FormsModule, - ReactiveFormsModule -} from '@angular/forms'; +import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatMenuTrigger } from '@angular/material/menu'; import { MatSelectModule } from '@angular/material/select'; import { RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; -import { Account, AssetClass, DataSource } from '@prisma/client'; +import { AssetClass, DataSource } from '@prisma/client'; import { differenceInYears } from 'date-fns'; import Fuse from 'fuse.js'; import { addIcons } from 'ionicons'; @@ -60,8 +55,11 @@ import { tap } from 'rxjs/operators'; -import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component'; import { translate } from '../i18n'; +import { + GfPortfolioFilterFormComponent, + PortfolioFilterFormValue +} from '../portfolio-filter-form'; import { GfAssistantListItemComponent } from './assistant-list-item/assistant-list-item.component'; import { SearchMode } from './enums/search-mode'; import { @@ -75,7 +73,7 @@ import { imports: [ FormsModule, GfAssistantListItemComponent, - GfEntityLogoComponent, + GfPortfolioFilterFormComponent, GfSymbolModule, IonIcon, MatButtonModule, @@ -128,6 +126,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { @Input() hasPermissionToChangeDateRange: boolean; @Input() hasPermissionToChangeFilters: boolean; @Input() user: User; + @Input() accountsWithValue: AccountWithValue[] = []; @Output() closed = new EventEmitter(); @Output() dateRangeChanged = new EventEmitter(); @@ -141,16 +140,18 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { public static readonly SEARCH_RESULTS_DEFAULT_LIMIT = 5; - public accounts: Account[] = []; + public accounts: AccountWithValue[] = []; public assetClasses: Filter[] = []; public dateRangeFormControl = new FormControl(undefined); public dateRangeOptions: IDateRangeOption[] = []; - public filterForm = this.formBuilder.group({ - account: new FormControl(undefined), - assetClass: new FormControl(undefined), - holding: new FormControl(undefined), - tag: new FormControl(undefined) - }); + public portfolioFilterFormControl = new FormControl( + { + account: null, + assetClass: null, + holding: null, + tag: null + } + ); public holdings: PortfolioPosition[] = []; public isLoading = { accounts: false, @@ -182,8 +183,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private formBuilder: FormBuilder + private dataService: DataService ) { addIcons({ closeCircleOutline, closeOutline, searchOutline }); } @@ -369,7 +369,29 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { } public ngOnChanges() { - this.accounts = this.user?.accounts ?? []; + // Use accountsWithValue if provided, otherwise transform user.accounts as fallback + if (this.accountsWithValue?.length > 0) { + this.accounts = this.accountsWithValue; + } else { + // Transform basic accounts to AccountWithValue format for compatibility + this.accounts = (this.user?.accounts ?? []).map((account) => ({ + ...account, + allocationInPercentage: 0, + balanceInBaseCurrency: account.balance || 0, + dividendInBaseCurrency: 0, + interestInBaseCurrency: 0, + platform: account.platformId + ? { + id: account.platformId, + name: account.platformId, // Fallback, ideally should be resolved + url: '' + } + : undefined, + transactionCount: 0, + value: account.balance || 0, + valueInBaseCurrency: account.balance || 0 + })) as AccountWithValue[]; + } this.dateRangeOptions = [ { @@ -438,8 +460,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null); - this.filterForm.disable({ emitEvent: false }); - this.tags = this.user?.tags ?.filter(({ isUsed }) => { @@ -452,10 +472,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { type: 'TAG' }; }) ?? []; - - if (this.tags.length === 0) { - this.filterForm.get('tag').disable({ emitEvent: false }); - } } public hasFilter(aFormValue: { [key: string]: string }) { @@ -464,19 +480,6 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { }); } - public holdingComparisonFunction( - option: PortfolioPosition, - value: PortfolioPosition - ): boolean { - if (value === null) { - return false; - } - - return ( - getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value) - ); - } - public initialize() { this.isLoading = { accounts: true, @@ -520,36 +523,33 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { .sort((a, b) => { return a.name?.localeCompare(b.name); }); - this.setFilterFormValues(); - - if (this.hasPermissionToChangeFilters) { - this.filterForm.enable({ emitEvent: false }); - } + this.setPortfolioFilterFormValues(); this.changeDetectorRef.markForCheck(); }); } public onApplyFilters() { + const filterValue = this.portfolioFilterFormControl.value; this.filtersChanged.emit([ { - id: this.filterForm.get('account').value, + id: filterValue?.account, type: 'ACCOUNT' }, { - id: this.filterForm.get('assetClass').value, + id: filterValue?.assetClass, type: 'ASSET_CLASS' }, { - id: this.filterForm.get('holding').value?.dataSource, + id: filterValue?.holding?.dataSource, type: 'DATA_SOURCE' }, { - id: this.filterForm.get('holding').value?.symbol, + id: filterValue?.holding?.symbol, type: 'SYMBOL' }, { - id: this.filterForm.get('tag').value, + id: filterValue?.tag, type: 'TAG' } ]); @@ -568,6 +568,13 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { } public onResetFilters() { + this.portfolioFilterFormControl.setValue({ + account: null, + assetClass: null, + holding: null, + tag: null + }); + this.filtersChanged.emit( this.filterTypes.map((type) => { return { @@ -723,7 +730,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { }); } - private setFilterFormValues() { + private setPortfolioFilterFormValues() { const dataSource = this.user?.settings?.[ 'filters.dataSource' ] as DataSource; @@ -737,16 +744,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { ); }); - this.filterForm.setValue( - { - account: this.user?.settings?.['filters.accounts']?.[0] ?? null, - assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null, - holding: selectedHolding ?? null, - tag: this.user?.settings?.['filters.tags']?.[0] ?? null - }, - { - emitEvent: false - } - ); + this.portfolioFilterFormControl.setValue({ + account: this.user?.settings?.['filters.accounts']?.[0] ?? null, + assetClass: this.user?.settings?.['filters.assetClasses']?.[0] ?? null, + holding: selectedHolding ?? null, + tag: this.user?.settings?.['filters.tags']?.[0] ?? null + }); } } diff --git a/libs/ui/src/lib/assistant/assistant.html b/libs/ui/src/lib/assistant/assistant.html index 5954ce369..bb9ce0df1 100644 --- a/libs/ui/src/lib/assistant/assistant.html +++ b/libs/ui/src/lib/assistant/assistant.html @@ -164,119 +164,31 @@ } -
- @if (!searchFormControl.value) { -
- - Date Range - - @for (range of dateRangeOptions; track range) { - {{ range.label }} - } - - -
-
-
- - Account - - - @for (account of accounts; track account.id) { - -
- @if (account.platform?.url) { - - } - {{ account.name }} -
-
- } -
-
-
-
- - Holding - - {{ - filterForm.get('holding')?.value?.name - }} - - @for (holding of holdings; track holding.name) { - -
- {{ holding.name }} -
- {{ holding.symbol | gfSymbol }} · - {{ holding.currency }} -
-
- } -
-
-
-
- - Tag - - - @for (tag of tags; track tag.id) { - {{ tag.label }} - } - - -
-
- - Asset Class - - - @for (assetClass of assetClasses; track assetClass.id) { - {{ - assetClass.label - }} - } - - -
-
- - - -
-
- } -
+ @if (!searchFormControl.value) { +
+ + Date Range + + @for (range of dateRangeOptions; track range) { + {{ range.label }} + } + + +
+
+ +
+ } diff --git a/libs/ui/src/lib/portfolio-filter-form/README.md b/libs/ui/src/lib/portfolio-filter-form/README.md new file mode 100644 index 000000000..5ee151441 --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/README.md @@ -0,0 +1,130 @@ +# Portfolio Filter Form Component + +## Overview + +The `GfPortfolioFilterFormComponent` is a reusable Angular component that provides a form interface for filtering portfolio data. It implements `ControlValueAccessor` to work seamlessly with Angular reactive forms. + +## Features + +- **Account filtering**: Select specific accounts to filter by +- **Asset class filtering**: Filter by asset classes (Equity, Fixed Income, etc.) +- **Holding filtering**: Filter by specific holdings/securities +- **Tag filtering**: Filter by user-defined tags +- **Form validation**: Built-in validation and state management +- **Accessibility**: Full support for Angular forms and accessibility features + +## Usage + +### Basic Implementation + +```typescript +import { GfPortfolioFilterFormComponent } from '@ghostfolio/ui/portfolio-filter-form'; + +@Component({ + selector: 'my-component', + template: ` + + + ` +}) +export class MyComponent { + portfolioFiltersControl = new FormControl({ + account: null, + assetClass: null, + holding: null, + tag: null + }); + + // ... other properties +} +``` + +### With Reactive Forms + +```typescript +import { PortfolioFilterFormValue } from '@ghostfolio/ui/portfolio-filter-form'; + +import { FormControl } from '@angular/forms'; + +const filterControl = new FormControl({ + account: null, + assetClass: null, + holding: null, + tag: null +}); + +// Subscribe to changes +filterControl.valueChanges.subscribe((filters) => { + console.log('Filter changes:', filters); +}); +``` + +## Inputs + +| Input | Type | Description | +| -------------- | --------------------- | ----------------------------------- | +| `accounts` | `Account[]` | Array of available accounts | +| `assetClasses` | `Filter[]` | Array of available asset classes | +| `holdings` | `PortfolioPosition[]` | Array of available holdings | +| `tags` | `Filter[]` | Array of available tags | +| `disabled` | `boolean` | Whether the form should be disabled | + +## Outputs + +| Output | Type | Description | +| -------------- | -------------------- | -------------------------------------------- | +| `applyFilters` | `EventEmitter` | Emitted when Apply Filters button is clicked | +| `resetFilters` | `EventEmitter` | Emitted when Reset Filters button is clicked | + +## Interface + +### PortfolioFilterFormValue + +```typescript +interface PortfolioFilterFormValue { + account: string | null; + assetClass: string | null; + holding: PortfolioPosition | null; + tag: string | null; +} +``` + +## Implementation Details + +- Implements `ControlValueAccessor` for seamless integration with Angular forms +- Uses Angular Material components for consistent UI +- Handles form state management internally +- Provides validation and dirty state tracking +- Supports disabled state management + +## Testing + +The component includes comprehensive unit tests covering: + +- Component creation and initialization +- Form value management +- Event emission +- Filter detection logic +- ControlValueAccessor implementation + +Run tests with: + +```bash +nx test ui +``` + +## Storybook + +Interactive component documentation and examples are available in Storybook: + +```bash +nx run ui:storybook +``` diff --git a/libs/ui/src/lib/portfolio-filter-form/index.ts b/libs/ui/src/lib/portfolio-filter-form/index.ts new file mode 100644 index 000000000..51d22c034 --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/index.ts @@ -0,0 +1,2 @@ +export * from './interfaces'; +export * from './portfolio-filter-form.component'; diff --git a/libs/ui/src/lib/portfolio-filter-form/interfaces/index.ts b/libs/ui/src/lib/portfolio-filter-form/interfaces/index.ts new file mode 100644 index 000000000..62feaa56a --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/interfaces/index.ts @@ -0,0 +1 @@ +export * from './portfolio-filter-form-value.interface'; diff --git a/libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts b/libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts new file mode 100644 index 000000000..820facb2f --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/interfaces/portfolio-filter-form-value.interface.ts @@ -0,0 +1,8 @@ +import { PortfolioPosition } from '@ghostfolio/common/interfaces'; + +export interface PortfolioFilterFormValue { + account: string | null; + assetClass: string | null; + holding: PortfolioPosition | null; + tag: string | null; +} diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html new file mode 100644 index 000000000..0fc9ec214 --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -0,0 +1,95 @@ +
+
+ + Account + + + @for (account of accounts; track account.id) { + +
+ @if (account.platform?.url) { + + } + {{ account.name }} +
+
+ } +
+
+
+
+ + Holding + + {{ + filterForm.get('holding')?.value?.name + }} + + @for (holding of holdings; track holding.name) { + +
+ {{ holding.name }} +
+ {{ holding.symbol | gfSymbol }} · {{ holding.currency }} +
+
+ } +
+
+
+
+ + Tag + + + @for (tag of tags; track tag.id) { + {{ tag.label }} + } + + +
+
+ + Asset Class + + + @for (assetClass of assetClasses; track assetClass.id) { + {{ + assetClass.label + }} + } + + +
+
+ + + +
+
diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.scss b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.scss new file mode 100644 index 000000000..2baad569d --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.scss @@ -0,0 +1,5 @@ +.gf-portfolio-filter-form { + .gf-spacer { + flex: 1 1 auto; + } +} diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.spec.ts b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.spec.ts new file mode 100644 index 000000000..becc36e5e --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.spec.ts @@ -0,0 +1,69 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { GfPortfolioFilterFormComponent } from './portfolio-filter-form.component'; + +// Mock $localize for testing +(global as any).$localize = (template: any) => { + return template.raw ? template.raw.join('') : template; +}; + +describe('GfPortfolioFilterFormComponent', () => { + let component: GfPortfolioFilterFormComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ + GfPortfolioFilterFormComponent, + MatButtonModule, + MatFormFieldModule, + MatSelectModule, + NoopAnimationsModule, + ReactiveFormsModule + ] + }).compileComponents(); + + fixture = TestBed.createComponent(GfPortfolioFilterFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize with empty form values', () => { + expect(component.filterForm.value).toEqual({ + account: null, + assetClass: null, + holding: null, + tag: null + }); + }); + + it('should detect when filters are applied', () => { + component.filterForm.patchValue({ account: 'test-account-id' }); + expect(component.hasFilters()).toBeTruthy(); + }); + + it('should detect when no filters are applied', () => { + expect(component.hasFilters()).toBeFalsy(); + }); + + it('should emit resetFilters event when onResetFilters is called', () => { + jest.spyOn(component.resetFilters, 'emit'); + component.onResetFilters(); + expect(component.resetFilters.emit).toHaveBeenCalled(); + }); + + it('should emit applyFilters event when onApplyFilters is called', () => { + jest.spyOn(component.applyFilters, 'emit'); + component.onApplyFilters(); + expect(component.applyFilters.emit).toHaveBeenCalled(); + }); +}); diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts new file mode 100644 index 000000000..7869ea632 --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.stories.ts @@ -0,0 +1,70 @@ +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; + +import { GfPortfolioFilterFormComponent } from './portfolio-filter-form.component'; + +const meta: Meta = { + title: 'Portfolio Filter Form', + component: GfPortfolioFilterFormComponent, + decorators: [ + moduleMetadata({ + imports: [GfPortfolioFilterFormComponent] + }) + ] +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + accounts: [ + { + id: '1', + name: 'Trading Account', + platform: { + name: 'Interactive Brokers', + url: 'https://interactivebrokers.com' + } + }, + { + id: '2', + name: 'Investment Account', + platform: { + name: 'Fidelity', + url: 'https://fidelity.com' + } + } + ] as any, + assetClasses: [ + { id: 'EQUITY', label: 'Equity', type: 'ASSET_CLASS' }, + { id: 'FIXED_INCOME', label: 'Fixed Income', type: 'ASSET_CLASS' }, + { id: 'COMMODITY', label: 'Commodity', type: 'ASSET_CLASS' } + ] as any, + holdings: [ + { + name: 'Apple Inc.', + symbol: 'AAPL', + currency: 'USD', + dataSource: 'YAHOO' + }, + { + name: 'Microsoft Corporation', + symbol: 'MSFT', + currency: 'USD', + dataSource: 'YAHOO' + } + ] as any, + tags: [ + { id: 'tech', label: 'Technology', type: 'TAG' }, + { id: 'dividend', label: 'Dividend', type: 'TAG' } + ] as any, + disabled: false + } +}; + +export const Disabled: Story = { + args: { + ...Default.args, + disabled: true + } +}; diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts new file mode 100644 index 000000000..708581467 --- /dev/null +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts @@ -0,0 +1,180 @@ +import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; +import { getAssetProfileIdentifier } from '@ghostfolio/common/helper'; +import { Filter, PortfolioPosition } from '@ghostfolio/common/interfaces'; +import { AccountWithValue } from '@ghostfolio/common/types'; + +import { + CUSTOM_ELEMENTS_SCHEMA, + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnDestroy, + OnInit, + Output, + forwardRef +} from '@angular/core'; +import { + ControlValueAccessor, + FormBuilder, + FormControl, + FormsModule, + NG_VALUE_ACCESSOR, + ReactiveFormsModule +} from '@angular/forms'; +import { MatButtonModule } from '@angular/material/button'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatSelectModule } from '@angular/material/select'; +import { Subject, takeUntil } from 'rxjs'; + +import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component'; +import { PortfolioFilterFormValue } from './interfaces'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ + FormsModule, + GfEntityLogoComponent, + GfSymbolModule, + MatButtonModule, + MatFormFieldModule, + MatSelectModule, + ReactiveFormsModule + ], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GfPortfolioFilterFormComponent), + multi: true + } + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + selector: 'gf-portfolio-filter-form', + styleUrls: ['./portfolio-filter-form.component.scss'], + templateUrl: './portfolio-filter-form.component.html' +}) +export class GfPortfolioFilterFormComponent + implements ControlValueAccessor, OnInit, OnChanges, OnDestroy +{ + @Input() accounts: AccountWithValue[] = []; + @Input() assetClasses: Filter[] = []; + @Input() holdings: PortfolioPosition[] = []; + @Input() tags: Filter[] = []; + @Input() disabled = false; + + @Output() applyFilters = new EventEmitter(); + @Output() resetFilters = new EventEmitter(); + + public filterForm = this.formBuilder.group({ + account: new FormControl(null), + assetClass: new FormControl(null), + holding: new FormControl(null), + tag: new FormControl(null) + }); + + private onChange: (value: PortfolioFilterFormValue) => void = () => { + // ControlValueAccessor callback - implemented by parent + }; + private onTouched: () => void = () => { + // ControlValueAccessor callback - implemented by parent + }; + private unsubscribeSubject = new Subject(); + + public constructor( + private changeDetectorRef: ChangeDetectorRef, + private formBuilder: FormBuilder + ) {} + + public ngOnInit() { + // Subscribe to form changes to notify parent component + this.filterForm.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((value) => { + this.onChange(value as PortfolioFilterFormValue); + this.onTouched(); + }); + } + + public ngOnChanges() { + // Update form disabled state + if (this.disabled) { + this.filterForm.disable({ emitEvent: false }); + } else { + this.filterForm.enable({ emitEvent: false }); + } + + // Disable tag field if no tags available + if (this.tags.length === 0) { + this.filterForm.get('tag')?.disable({ emitEvent: false }); + } + + this.changeDetectorRef.markForCheck(); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + // ControlValueAccessor implementation + public writeValue(value: PortfolioFilterFormValue | null): void { + if (value) { + this.filterForm.setValue( + { + account: value.account ?? null, + assetClass: value.assetClass ?? null, + holding: value.holding ?? null, + tag: value.tag ?? null + }, + { emitEvent: false } + ); + } else { + this.filterForm.reset({}, { emitEvent: false }); + } + } + + public registerOnChange(fn: (value: PortfolioFilterFormValue) => void): void { + this.onChange = fn; + } + + public registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + public setDisabledState(isDisabled: boolean): void { + this.disabled = isDisabled; + this.ngOnChanges(); + } + + // Helper methods + public hasFilters(): boolean { + const formValue = this.filterForm.value; + return Object.values(formValue).some((value) => !!value); + } + + public holdingComparisonFunction( + option: PortfolioPosition, + value: PortfolioPosition + ): boolean { + if (value === null) { + return false; + } + + return ( + getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value) + ); + } + + public onApplyFilters(): void { + this.filterForm.markAsPristine(); + this.onChange(this.filterForm.value as PortfolioFilterFormValue); + this.applyFilters.emit(); + } + + public onResetFilters(): void { + this.filterForm.reset({}, { emitEvent: true }); + this.resetFilters.emit(); + } +}