198 lines
5.4 KiB
TypeScript
198 lines
5.4 KiB
TypeScript
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<AccountingState | undefined>(
|
|
undefined
|
|
);
|
|
private accountingFilter$ = new BehaviorSubject<AccountingFilterArgs>({});
|
|
|
|
@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<string>('');
|
|
accountingStateMeta$ = this.accountingState$.pipe(
|
|
filter(Boolean),
|
|
switchMap((accountingState) =>
|
|
this.accountingService.fetchAccountingStateMeta(accountingState)
|
|
)
|
|
);
|
|
tours$: Observable<(Tour & { ticket: Ticket })[]>;
|
|
|
|
constructor() {
|
|
const batchMap: Observable<Record<string, Tour & { ticket: Ticket }>> =
|
|
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<string, Tour & { ticket: Ticket }>
|
|
);
|
|
|
|
return { ...existingTours, ...tours };
|
|
}, {} as Record<string, Tour & { ticket: Ticket }>)
|
|
);
|
|
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<Record<string, Tour & { ticket: Ticket }>> {
|
|
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<string, Tour & { ticket: Ticket }>
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
private getNormalizedCursor(cursor: string) {
|
|
return cursor === '' ? 'MA==' : cursor;
|
|
}
|
|
|
|
filterChanged(filter: AccountingFilterArgs) {
|
|
this.accountingFilter$.next(filter);
|
|
this.cursor$.next('');
|
|
}
|
|
}
|