mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
387 lines
11 KiB
TypeScript
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;
|
|
}
|
|
}
|