From 88ae4b5303383ec9a2c70020ed24f9a1900c4e7b Mon Sep 17 00:00:00 2001 From: stdevman91 Date: Thu, 20 Aug 2026 11:06:53 +0300 Subject: [PATCH 1/3] Add filter --- .trae/rules/project_rules.md | 0 .vercel/project.json | 1 + ...orical-inspect-results-data.component.html | 2 +- ...storical-inspect-results-data.component.ts | 2 + .../historical-payments-data.component.html | 2 +- .../historical-payments-data.component.ts | 2 + .../search/history-data-search.component.html | 6 +++ .../search/history-data-search.component.ts | 7 ++- .../search-historical-params.ts | 2 + .../services/fetch-historical.service.ts | 20 +++++++- .../components/infinite-scroll-trigger.ts | 44 ++++++++++++++++++ ...how-more-continuation-panel.component.html | 6 +-- ...how-more-continuation-panel.component.scss | 9 ++-- .../show-more-continuation-panel.component.ts | 46 +++++++++++++++---- .../show-more-continuation-panel.module.ts | 7 ++- .../show-more-panel.component.html | 6 +-- .../show-more-panel.component.scss | 9 ++-- .../show-more-panel.component.ts | 46 +++++++++++++++---- .../show-more-panel/show-more-panel.module.ts | 7 ++- src/assets/appConfig.json | 2 +- 20 files changed, 182 insertions(+), 44 deletions(-) create mode 100644 .trae/rules/project_rules.md create mode 100644 .vercel/project.json create mode 100644 src/app/shared/components/infinite-scroll-trigger.ts diff --git a/.trae/rules/project_rules.md b/.trae/rules/project_rules.md new file mode 100644 index 00000000..e69de29b diff --git a/.vercel/project.json b/.vercel/project.json new file mode 100644 index 00000000..8781a38f --- /dev/null +++ b/.vercel/project.json @@ -0,0 +1 @@ +{ "projectName": "trae_fraudbusters-ui_7iw0" } diff --git a/src/app/sections/historical-data/components/inspect-results/historical-inspect-results-data.component.html b/src/app/sections/historical-data/components/inspect-results/historical-inspect-results-data.component.html index 21a2003c..d8c6ed6e 100644 --- a/src/app/sections/historical-data/components/inspect-results/historical-inspect-results-data.component.html +++ b/src/app/sections/historical-data/components/inspect-results/historical-inspect-results-data.component.html @@ -1,5 +1,5 @@
- +
- +
diff --git a/src/app/sections/historical-data/components/search/history-data-search.component.ts b/src/app/sections/historical-data/components/search/history-data-search.component.ts index 032583a2..1ba28f66 100644 --- a/src/app/sections/historical-data/components/search/history-data-search.component.ts +++ b/src/app/sections/historical-data/components/search/history-data-search.component.ts @@ -1,4 +1,4 @@ -import { Component, EventEmitter, Output } from '@angular/core'; +import { Component, EventEmitter, Input, Output } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { debounceTime, map, take } from 'rxjs/operators'; @@ -12,6 +12,7 @@ import { removeEmptyProperties } from '../../../../shared/utils/remove-empty-pro styleUrls: ['search.component.scss'], }) export class HistoryDataSearchComponent { + @Input() showRuleFilters = false; @Output() valueChanges: EventEmitter = new EventEmitter(); form: FormGroup = this.fb.group({ @@ -24,6 +25,8 @@ export class HistoryDataSearchComponent { fingerprint: '', email: '', terminal: '', + template: '', + rule: '', from: this.searchFieldService.todayFromTime().toISOString(), to: new Date().toISOString(), }); @@ -46,6 +49,8 @@ export class HistoryDataSearchComponent { params.fingerprint = v.fingerprint; params.email = v.email; params.terminal = v.terminal; + params.template = v.template; + params.rule = v.rule; params.country = v.country; this.router.navigate([location.pathname], { queryParams: params }); this.valueChanges.emit(v); diff --git a/src/app/sections/historical-data/search-historical-params.ts b/src/app/sections/historical-data/search-historical-params.ts index f313fb61..144d283b 100644 --- a/src/app/sections/historical-data/search-historical-params.ts +++ b/src/app/sections/historical-data/search-historical-params.ts @@ -14,4 +14,6 @@ export interface SearchHistoricalParams { email?: string; terminal?: string; maskedPan?: string; + template?: string; + rule?: string; } diff --git a/src/app/sections/historical-data/services/fetch-historical.service.ts b/src/app/sections/historical-data/services/fetch-historical.service.ts index 2a71cb83..48fa3bb0 100644 --- a/src/app/sections/historical-data/services/fetch-historical.service.ts +++ b/src/app/sections/historical-data/services/fetch-historical.service.ts @@ -23,6 +23,8 @@ export interface FetchPaymentParams { status?: string; fingerprint?: string; email?: string; + template?: string; + rule?: string; } export abstract class FetchHistoricalService extends PartialFetcherContinuation { @@ -36,7 +38,21 @@ export abstract class FetchHistoricalService extends PartialFetcherContinu protected abstract filter(params: SearchHistoricalParams): Observable

; protected fetch(params: FetchPaymentParams, continuationId?: string): Observable> { - const { sortOrder, size, from, to, paymentId, cardToken, shopId, partyId, status, fingerprint, email } = params; + const { + sortOrder, + size, + from, + to, + paymentId, + cardToken, + shopId, + partyId, + status, + fingerprint, + email, + template, + rule, + } = params; return this.filter({ from: this.datepipe.transform(from, DateFormat._yyyyMMDdHHMmSs), to: this.datepipe.transform(to, DateFormat._yyyyMMDdHHMmSs), @@ -48,6 +64,8 @@ export abstract class FetchHistoricalService extends PartialFetcherContinu status: status || '', fingerprint: fingerprint || '', email: email || '', + template: template || '', + rule: rule || '', size: size ? size : this.pageSize, ...(continuationId ? { continuationId } : {}), }); diff --git a/src/app/shared/components/infinite-scroll-trigger.ts b/src/app/shared/components/infinite-scroll-trigger.ts new file mode 100644 index 00000000..8f6f6313 --- /dev/null +++ b/src/app/shared/components/infinite-scroll-trigger.ts @@ -0,0 +1,44 @@ +export class InfiniteScrollTrigger { + private observer?: IntersectionObserver; + private retryTimer?: number; + private isIntersecting = false; + private isLoading = false; + private requestPending = false; + + constructor(private readonly loadMore: () => void) {} + + observe(element: Element) { + this.observer = new IntersectionObserver( + ([entry]) => { + this.isIntersecting = entry.isIntersecting; + this.loadIfNeeded(); + }, + { rootMargin: '300px 0px' } + ); + this.observer.observe(element); + } + + setLoading(isLoading: boolean) { + const requestFinished = this.isLoading && !isLoading; + this.isLoading = isLoading; + if (requestFinished) { + this.requestPending = false; + this.retryTimer = window.setTimeout(() => this.loadIfNeeded()); + } + } + + disconnect() { + this.observer?.disconnect(); + if (this.retryTimer) { + window.clearTimeout(this.retryTimer); + } + } + + private loadIfNeeded() { + if (!this.isIntersecting || this.isLoading || this.requestPending) { + return; + } + this.requestPending = true; + this.loadMore(); + } +} diff --git a/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.html b/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.html index 195d712a..0647117c 100644 --- a/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.html +++ b/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.html @@ -1,3 +1,3 @@ - - - +

+ +
diff --git a/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.scss b/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.scss index caf74cf4..6458a0b8 100644 --- a/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.scss +++ b/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.scss @@ -1,4 +1,7 @@ -.show-more-panel { - margin: 0; - padding: 0 !important; +.infinite-scroll-sentinel { + display: flex; + min-height: 48px; + align-items: center; + justify-content: center; + width: 100%; } diff --git a/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.ts b/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.ts index 5abe8f78..8a66eb6b 100644 --- a/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.ts +++ b/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.component.ts @@ -1,26 +1,52 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + OnDestroy, + Output, + ViewChild, +} from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; -import { take } from 'rxjs/operators'; + +import { InfiniteScrollTrigger } from '../infinite-scroll-trigger'; @Component({ selector: 'fb-show-more-continuation-panel', templateUrl: 'show-more-continuation-panel.component.html', styleUrls: ['show-more-continuation-panel.component.scss'], }) -export class ShowMoreContinuationPanelComponent { - @Input() - isLoading = false; +export class ShowMoreContinuationPanelComponent implements AfterViewInit, OnDestroy { + @ViewChild('sentinel', { static: true }) sentinel: ElementRef; + + @Input() set isLoading(value: boolean | null) { + this.loading = Boolean(value); + this.infiniteScrollTrigger.setLoading(this.loading); + } + + get isLoading(): boolean { + return this.loading; + } @Output() showMore: EventEmitter = new EventEmitter(); - queryParams = {}; + private loading = false; + private readonly infiniteScrollTrigger: InfiniteScrollTrigger; + + constructor(private route: ActivatedRoute, private ngZone: NgZone) { + this.infiniteScrollTrigger = new InfiniteScrollTrigger(() => + this.ngZone.run(() => this.showMore.emit(this.route.snapshot.queryParams)) + ); + } - constructor(private route: ActivatedRoute) { - this.route.queryParams.pipe(take(1)).subscribe((v) => (this.queryParams = v)); + ngAfterViewInit() { + this.infiniteScrollTrigger.observe(this.sentinel.nativeElement); } - getMore() { - this.showMore.emit(this.queryParams); + ngOnDestroy() { + this.infiniteScrollTrigger.disconnect(); } } diff --git a/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.module.ts b/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.module.ts index 0d779254..3c175273 100644 --- a/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.module.ts +++ b/src/app/shared/components/show-more-panel-continuation/show-more-continuation-panel.module.ts @@ -1,13 +1,12 @@ +import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { FlexModule } from '@angular/flex-layout'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ShowMoreContinuationPanelComponent } from './show-more-continuation-panel.component'; @NgModule({ declarations: [ShowMoreContinuationPanelComponent], - imports: [FlexModule, MatCardModule, MatButtonModule], + imports: [CommonModule, MatProgressSpinnerModule], exports: [ShowMoreContinuationPanelComponent], }) export class ShowMoreContinuationPanelModule {} diff --git a/src/app/shared/components/show-more-panel/show-more-panel.component.html b/src/app/shared/components/show-more-panel/show-more-panel.component.html index 195d712a..0647117c 100644 --- a/src/app/shared/components/show-more-panel/show-more-panel.component.html +++ b/src/app/shared/components/show-more-panel/show-more-panel.component.html @@ -1,3 +1,3 @@ - - - +
+ +
diff --git a/src/app/shared/components/show-more-panel/show-more-panel.component.scss b/src/app/shared/components/show-more-panel/show-more-panel.component.scss index caf74cf4..6458a0b8 100644 --- a/src/app/shared/components/show-more-panel/show-more-panel.component.scss +++ b/src/app/shared/components/show-more-panel/show-more-panel.component.scss @@ -1,4 +1,7 @@ -.show-more-panel { - margin: 0; - padding: 0 !important; +.infinite-scroll-sentinel { + display: flex; + min-height: 48px; + align-items: center; + justify-content: center; + width: 100%; } diff --git a/src/app/shared/components/show-more-panel/show-more-panel.component.ts b/src/app/shared/components/show-more-panel/show-more-panel.component.ts index 047cbbaf..708641aa 100644 --- a/src/app/shared/components/show-more-panel/show-more-panel.component.ts +++ b/src/app/shared/components/show-more-panel/show-more-panel.component.ts @@ -1,24 +1,52 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + Input, + NgZone, + OnDestroy, + Output, + ViewChild, +} from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; -import { take } from 'rxjs/operators'; + +import { InfiniteScrollTrigger } from '../infinite-scroll-trigger'; @Component({ selector: 'fb-show-more-panel', templateUrl: 'show-more-panel.component.html', styleUrls: ['show-more-panel.component.scss'], }) -export class ShowMorePanelComponent { - @Input() - isLoading = false; +export class ShowMorePanelComponent implements AfterViewInit, OnDestroy { + @ViewChild('sentinel', { static: true }) sentinel: ElementRef; + + @Input() set isLoading(value: boolean | null) { + this.loading = Boolean(value); + this.infiniteScrollTrigger.setLoading(this.loading); + } + + get isLoading(): boolean { + return this.loading; + } @Output() showMore: EventEmitter = new EventEmitter(); - constructor(private route: ActivatedRoute) { - this.route.queryParams.pipe(take(1)).subscribe((v) => this.showMore.emit(v)); + private loading = false; + private readonly infiniteScrollTrigger: InfiniteScrollTrigger; + + constructor(private route: ActivatedRoute, private ngZone: NgZone) { + this.infiniteScrollTrigger = new InfiniteScrollTrigger(() => + this.ngZone.run(() => this.showMore.emit(this.route.snapshot.queryParams)) + ); + } + + ngAfterViewInit() { + this.infiniteScrollTrigger.observe(this.sentinel.nativeElement); } - getMore() { - this.showMore.emit(); + ngOnDestroy() { + this.infiniteScrollTrigger.disconnect(); } } diff --git a/src/app/shared/components/show-more-panel/show-more-panel.module.ts b/src/app/shared/components/show-more-panel/show-more-panel.module.ts index 06c2647a..3cee64ab 100644 --- a/src/app/shared/components/show-more-panel/show-more-panel.module.ts +++ b/src/app/shared/components/show-more-panel/show-more-panel.module.ts @@ -1,13 +1,12 @@ +import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; -import { FlexModule } from '@angular/flex-layout'; -import { MatButtonModule } from '@angular/material/button'; -import { MatCardModule } from '@angular/material/card'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { ShowMorePanelComponent } from './show-more-panel.component'; @NgModule({ declarations: [ShowMorePanelComponent], - imports: [FlexModule, MatCardModule, MatButtonModule], + imports: [CommonModule, MatProgressSpinnerModule], exports: [ShowMorePanelComponent], }) export class ShowMorePanelModule {} diff --git a/src/assets/appConfig.json b/src/assets/appConfig.json index 8297e61c..9f457379 100644 --- a/src/assets/appConfig.json +++ b/src/assets/appConfig.json @@ -1,4 +1,4 @@ { "fbManagementEndpoint": "https://api.empayre.com/fb-management/v1", - "pageSize": 10 + "pageSize": 100 } From 4fde36f0abc9491fa48b1b177baf38fb94fa310e Mon Sep 17 00:00:00 2001 From: stdevman91 Date: Thu, 20 Aug 2026 16:34:24 +0300 Subject: [PATCH 2/3] Fix bar chart --- .../components/charts/bar-chart/bar-chart.component.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/shared/components/charts/bar-chart/bar-chart.component.ts b/src/app/shared/components/charts/bar-chart/bar-chart.component.ts index b6abd690..aae8287c 100644 --- a/src/app/shared/components/charts/bar-chart/bar-chart.component.ts +++ b/src/app/shared/components/charts/bar-chart/bar-chart.component.ts @@ -1,4 +1,7 @@ import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; +// Apex options contain formatter callbacks, which structuredClone cannot clone. +// eslint-disable-next-line you-dont-need-lodash-underscore/clone-deep +import cloneDeep from 'lodash-es/cloneDeep'; import { ApexAxisChartSeries } from 'ng-apexcharts'; import { DEFAULT_CONFIG } from './default-config'; @@ -18,7 +21,7 @@ export class BarChartComponent implements OnChanges { @Input() height?: number; - config = structuredClone(DEFAULT_CONFIG); + config = cloneDeep(DEFAULT_CONFIG); ngOnChanges(changes: SimpleChanges) { if (changes.height && changes.height.currentValue !== changes.height.previousValue) { From c8ea5f7ebee8ce42820e4208d6bebda5af042cfe Mon Sep 17 00:00:00 2001 From: stdevman91 Date: Thu, 20 Aug 2026 16:34:32 +0300 Subject: [PATCH 3/3] Fix bar chart --- .../charts/bar-chart/bar-chart.component.spec.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/app/shared/components/charts/bar-chart/bar-chart.component.spec.ts diff --git a/src/app/shared/components/charts/bar-chart/bar-chart.component.spec.ts b/src/app/shared/components/charts/bar-chart/bar-chart.component.spec.ts new file mode 100644 index 00000000..f8ee79a0 --- /dev/null +++ b/src/app/shared/components/charts/bar-chart/bar-chart.component.spec.ts @@ -0,0 +1,11 @@ +import { BarChartComponent } from './bar-chart.component'; +import { DEFAULT_CONFIG } from './default-config'; + +describe('BarChartComponent', () => { + it('creates an independent config containing formatter functions', () => { + const component = new BarChartComponent(); + + expect(component.config).toEqual(DEFAULT_CONFIG); + expect(component.config).not.toBe(DEFAULT_CONFIG); + }); +});