Files
ISA-Frontend/apps/page/customer/src/lib/customer-search/customer-search.service.ts
2021-01-13 15:34:05 +01:00

387 lines
11 KiB
TypeScript

import { HttpErrorResponse } from '@angular/common/http';
import { Injectable, NgZone, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { EnvironmentService } from '@core/environment';
import { CrmCustomerService } from '@domain/crm';
import { PagedResult } from '@domain/defs';
import { AutocompleteDTO, CustomerInfoDTO } from '@swagger/crm';
import { BehaviorSubject, Observable, of, Subject } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, filter, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
import { CustomerSearchType, QueryFilter, ResultState } from './defs';
import { cloneFilter, Filter, SelectFilterOption, UiFilterMappingService } from '@ui/filter';
import { NativeContainerService } from 'native-container';
import { StringDictionary } from '@cmf/core';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
@Injectable()
export abstract class CustomerSearch implements OnInit, OnDestroy {
protected abstract customerSearch: CrmCustomerService;
private destroy$ = new Subject();
filtersLoaded$ = new BehaviorSubject<boolean>(false);
queryFilter$ = new BehaviorSubject<QueryFilter>({
query: '',
filters: [],
});
queryFilterEmpty$ = this.queryFilter$.pipe(
map((qf) => {
return !qf.query && Object.entries(this.getSelecteFiltersAsDictionary()).length === 0;
})
);
get queryFilter() {
return this.queryFilter$.value;
}
protected filterActive$ = new BehaviorSubject<boolean>(false);
autocompleteResult$: Observable<AutocompleteDTO[]>;
inputChange$ = new Subject<string>();
public searchResult$ = new BehaviorSubject<PagedResult<CustomerSearchType>>({
result: [],
});
public searchState$ = new BehaviorSubject<ResultState>('init');
get searchState(): ResultState {
return this.searchState$.value;
}
get searchResult(): PagedResult<CustomerSearchType> {
return this.searchResult$.value;
}
private get numberOfResultsFetched(): number {
if (!this.searchResult$ || !this.searchResult$.value || !Array.isArray(this.searchResult$.value.result)) {
return 0;
}
return this.searchResult$.value.result.length;
}
private get totalResults(): number {
if (!this.searchResult$ || !this.searchResult$.value || !this.searchResult$.value.hits) {
return 0;
}
return this.searchResult$.value.hits;
}
constructor(
private zone: NgZone,
private router: Router,
private application: ApplicationService,
private breadcrumb: BreadcrumbService,
private environmentService: EnvironmentService,
private filterMapping: UiFilterMappingService,
private nativeContainer: NativeContainerService
) {}
ngOnInit() {
this.initAvailableFilters();
this.initAutocomplete();
this.initStatusRefreshOnReset();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
parseQueryParams() {
const params = new URL(this.router.url, window.location.origin).searchParams;
this.setQuery(params.get('query'), { updateQuery: false });
const filters = this.queryFilter.filters.map(cloneFilter);
filters.forEach((f) => {
const values = params.get(String(f.key))?.split(';');
f.options.forEach((o) => {
if (values?.includes(o.id)) {
o.selected = true;
}
});
});
this.setFilter(filters, { updateQuery: false });
}
private initAvailableFilters() {
this.customerSearch
.getFilters()
.pipe(
map((result) => {
if (result.error) {
} else {
const filters = result.result.map((input) => this.filterMapping.fromInputDto(input));
this.setFilter(filters, { updateQuery: false });
this.parseQueryParams();
return filters;
}
})
)
.subscribe((_) => this.filtersLoaded$.next(true));
}
private initAutocomplete() {
this.autocompleteResult$ = this.inputChange$.pipe(
takeUntil(this.destroy$),
distinctUntilChanged(),
debounceTime(200),
switchMap((queryString) => {
if (queryString.length >= 3) {
return this.customerSearch.complete(queryString, this.getSelecteFiltersAsDictionary()).pipe(
map((response) => response.result),
catchError(() => {
// TODO Dialog Service impl. fur Anzeige der Fehlermeldung
return [];
})
);
} else {
return of([]);
}
}),
shareReplay()
);
}
private initStatusRefreshOnReset() {
this.queryFilter$
.pipe(
map((fltr) => fltr.query),
distinctUntilChanged(),
takeUntil(this.destroy$),
filter((qs) => !qs || !qs.length)
)
.subscribe(() => this.searchState$.next('init'));
}
createQueryParams(): StringDictionary<string> {
return {
query: this.queryFilter.query,
...this.getSelecteFiltersAsDictionary(),
};
}
async updateBreadcrumbParams() {
const queryParams = this.createQueryParams();
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.application.activatedProcessId, ['customer', 'filter'])
.pipe(take(1))
.toPromise();
crumbs.forEach((crumb) => {
this.breadcrumb.patchBreadcrumb(crumb.id, { params: queryParams });
});
}
updateUrlQueryParams() {
const queryParams = this.createQueryParams();
this.zone.run(() => {
this.router.navigate([], {
queryParams,
queryParamsHandling: 'merge',
});
});
}
async startSearch() {
if (!this.queryFilter.query && (await this.environmentService.isMobile())) {
return this.scan();
}
return this.search();
}
getSelecteFiltersAsDictionary(): StringDictionary<string> {
const dict: StringDictionary<string> = {};
for (const filter of this.queryFilter.filters) {
const selected = filter.options.filter((o) => o.selected);
if (selected.length > 0) {
dict[filter.key] = selected.map((o) => o.id).join(';');
}
}
return dict;
}
search(
options: { isNewSearch: boolean; take?: number } = {
isNewSearch: true,
take: 10,
}
): void {
if (!this.shouldFetchNewProducts(options)) {
return; // early exit because no new products need to be fetched
}
if (this.searchState !== 'fetching') {
this.searchState$.next('fetching');
this.searchResult$
.pipe(
take(1),
map((result) => {
const hitsTake = options.take ?? 10;
const effectiveTake = options.isNewSearch
? hitsTake
: this.numberOfResultsFetched + hitsTake > result.hits
? result.hits - this.numberOfResultsFetched
: hitsTake;
this.searchResult$.next(this.addLoadingProducts(this.searchResult, effectiveTake));
return [effectiveTake, hitsTake];
}),
switchMap(([effectiveTake, hitsTake]) => {
return this.customerSearch
.getCustomers(this.queryFilter.query, {
skip: options.isNewSearch ? 0 : this.numberOfResultsFetched - effectiveTake,
take: hitsTake,
filter: this.getSelecteFiltersAsDictionary(),
})
.pipe(
takeUntil(this.destroy$),
catchError((err: HttpErrorResponse) => {
return of<PagedResult<CustomerInfoDTO>>({
result: [],
error: true,
hits: 0,
message: err.message,
});
}),
tap((result) => {
if (result.error) {
this.searchState$.next('error');
} else if (result.hits === 0) {
this.searchState$.next('empty');
} else {
this.searchState$.next('result');
}
})
);
})
)
.subscribe((r) => {
this.searchResult$.next(this.removeLoadingProducts(this.searchResult));
if (options.isNewSearch) {
this.searchResult$.next({
...r,
result: r.result.map((a) => ({ ...a, loaded: true })),
});
if (this.searchState === 'result') {
if (r.hits === 1) {
this.navigateToDetails(r.result[0].id);
} else {
this.navigateToResults();
}
}
} else {
this.searchResult$.next({
...r,
result: [...this.searchResult$.value.result, ...r.result.map((a) => ({ ...a, loaded: true }))],
});
}
if (r.hits > 0) {
this.filterActive$.next(false);
}
});
}
}
scan() {
this.nativeContainer
.openScanner('scanCustomer')
.pipe(takeUntil(this.destroy$))
.subscribe((result) => {
this.setQuery(result.data);
this.search();
});
}
navigateToDetails(customerId: number) {
this.router.navigate(['customer', customerId]);
}
navigateToResults() {
this.router.navigate(['customer', 'search', 'result'], {
queryParams: this.createQueryParams(),
});
}
setQuery(query: string, options: { updateQuery?: boolean } = { updateQuery: true }) {
this.patchQueryFilter({ query });
}
setFilter(filters: Filter[], options: { updateQuery?: boolean } = { updateQuery: true }) {
this.patchQueryFilter({ filters });
}
patchQueryFilter(filter: Partial<QueryFilter>, options: { updateQuery?: boolean } = { updateQuery: true }) {
const current = this.queryFilter;
const next = { ...current, ...filter };
// TODO Check ob QueryFilter geändert wurde, falls gleich kein next Aufruf
let hasChanges = false;
if (current.query !== next.query) {
hasChanges = true;
}
if (!hasChanges) {
if (current.filters !== next.filters) {
hasChanges = true;
}
}
if (hasChanges) {
this.queryFilter$.next(next);
if (options?.updateQuery) {
this.updateUrlQueryParams();
this.updateBreadcrumbParams();
}
}
}
private addLoadingProducts(
currentResult: PagedResult<CustomerSearchType>,
numberOfLoadingProducts: number = 10
): PagedResult<CustomerSearchType> {
return {
...currentResult,
result: [...currentResult.result, ...new Array(numberOfLoadingProducts).fill({ loaded: false })],
};
}
private removeLoadingProducts(currentResult: PagedResult<CustomerSearchType>): PagedResult<CustomerSearchType> {
return {
...currentResult,
result: currentResult.result.filter((r) => !!r.loaded),
};
}
private shouldFetchNewProducts(options: { isNewSearch: boolean; take?: number }): boolean {
if (options.isNewSearch) {
return true;
}
if (this.totalResults > 0) {
return !(this.numberOfResultsFetched >= this.totalResults);
}
return true;
}
}