import { CdkVirtualScrollViewport, ScrollingModule, } from '@angular/cdk/scrolling'; import { AsyncPipe } from '@angular/common'; import { ChangeDetectionStrategy, Component, Input, ViewChild, inject, } from '@angular/core'; import { DialogService } from '@ngneat/dialog'; import { BehaviorSubject, Observable, combineLatest, debounceTime, filter, map, mergeMap, scan, switchMap, tap, } from 'rxjs'; import { AccountingFilterArgs, AccountingState, Ticket, Tour, } from '../../../core/data-access/graphql/generated/generated'; import { AccountingTourViewComponent } from '../accounting-tour-view/accounting-tour-view.component'; import { DashboardAccountingService } from '../dashboard-accounting.service'; import { AccountingItemComponent } from './accounting-item.component'; import { AccountingLaneHeaderComponent } from './accounting-lane-header.component'; @Component({ selector: 'dks-accounting-lane', changeDetection: ChangeDetectionStrategy.OnPush, templateUrl: './accounting-lane.component.html', host: { class: 'rounded h-full w-full grid', }, styles: [ ` :host { box-shadow: var(--mat-sys-level1); background-color: var(--mat-sys-surface-container-low); grid-template-rows: auto 1fr auto; } `, ], imports: [ AccountingLaneHeaderComponent, AccountingItemComponent, AsyncPipe, ScrollingModule ], }) export class AccountingLaneComponent { private dialog = inject(DialogService); private accountingService = inject(DashboardAccountingService); private accountingState$ = new BehaviorSubject( undefined ); private accountingFilter$ = new BehaviorSubject({}); @Input() set accountingState(accountingState: AccountingState) { this.accountingState$.next(accountingState); } get accountingState() { return this.accountingState$.getValue() as AccountingState; } @ViewChild(CdkVirtualScrollViewport) viewport!: CdkVirtualScrollViewport; AccountingState = AccountingState; take = 20; hasNextPage = true; nextPage = ''; cursor$ = new BehaviorSubject(''); accountingStateMeta$ = this.accountingState$.pipe( filter(Boolean), switchMap((accountingState) => this.accountingService.fetchAccountingStateMeta(accountingState) ) ); tours$: Observable<(Tour & { ticket: Ticket })[]>; constructor() { const batchMap: Observable> = combineLatest([this.cursor$, this.accountingFilter$]).pipe( debounceTime(200), mergeMap(([cursor, filter]) => this.fetchTours(cursor, filter).pipe( map((tours) => ({ tours, cursor })) ) ), scan((acc, { tours, cursor }) => { const normalizedCursor = this.getNormalizedCursor(cursor); const existingTours = Object.entries(acc) .filter( ([id]) => id.split('_').at(0) !== normalizedCursor || Object.keys(tours).includes(id.split('_').at(1) ?? '') ) .reduce( (acc, [id, tour]) => ({ ...acc, [`${normalizedCursor}_${id}`]: tour, }), {} as Record ); return { ...existingTours, ...tours }; }, {} as Record) ); this.tours$ = batchMap.pipe( map((tourNodes) => Object.values(tourNodes).sort( ({ createdAt: aCreated }, { createdAt: bCreated }) => new Date(bCreated).getTime() - new Date(aCreated).getTime() ) ) ); } openTourView(tour: Tour & { ticket: Ticket }): void { const dialogRef = this.dialog.open(AccountingTourViewComponent, { data: tour, width: '60vw', minHeight: '400px', }); dialogRef.afterClosed$ .pipe(tap((result) => result && this.openTourView(result))) .subscribe(); } trackByFn(index: number, tour: Tour): string { return tour.id; } fetchNextPage() { if (!this.hasNextPage) { return; } const start = this.viewport.getRenderedRange().start; const end = this.viewport.getRenderedRange().end; const total = this.viewport.getDataLength(); if (this.cursor$.getValue() !== '' && start === 0) { this.cursor$.next(''); } else if (total - end < 5) { this.cursor$.next(this.nextPage); } } private fetchTours( cursor: string, filter: AccountingFilterArgs ): Observable> { const normalizedCursor = this.getNormalizedCursor(cursor); return this.accountingService .fetchToursByState(this.accountingState, cursor, filter, this.take) .pipe( tap(({ endCursor, hasNextPage }) => { this.nextPage = endCursor; this.hasNextPage = hasNextPage; }), map(({ nodes }) => nodes?.reduce( (acc, cur) => ({ ...acc, [`${normalizedCursor}_${cur.id}`]: cur as Tour & { ticket: Ticket; }, }), {} as Record ) ) ); } private getNormalizedCursor(cursor: string) { return cursor === '' ? 'MA==' : cursor; } filterChanged(filter: AccountingFilterArgs) { this.accountingFilter$.next(filter); this.cursor$.next(''); } }