init dashboard

This commit is contained in:
Marcel Arndt
2025-04-10 17:31:52 +02:00
parent 98aeb82ff6
commit 52c1a14609
178 changed files with 12306 additions and 401 deletions
@@ -0,0 +1,200 @@
import {
CdkVirtualScrollViewport,
ScrollingModule,
} from '@angular/cdk/scrolling';
import { AsyncPipe, CurrencyPipe, NgClass, NgFor, NgIf } 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 { AccountingLaneFooterComponent } from './accounting-lane-footer.component';
import { AccountingLaneHeaderComponent } from './accounting-lane-header.component';
@Component({
selector: 'dks-accounting-lane',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './accounting-lane.component.html',
styles: [
`
:host {
@apply rounded bg-gray-200 h-full w-full grid;
grid-template-rows: auto 1fr auto;
}
`,
],
imports: [
AccountingLaneHeaderComponent,
AccountingLaneFooterComponent,
AccountingItemComponent,
NgFor,
NgIf,
NgClass,
AsyncPipe,
CurrencyPipe,
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[]>;
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('');
}
}