mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Merged PR 215: #816 Remove HitsOnly Call & Retrieve first Order Results on SearchPage
Fix Order Service Call to Get Warenausgabe Results Related work items: #816
This commit is contained in:
@@ -31,7 +31,9 @@
|
||||
*ngSwitchDefault
|
||||
#submitButton
|
||||
class="isa-input-submit"
|
||||
[class.scan]="!errorMessage && !(searchQuery$ | async) && isIPad"
|
||||
[class.scan]="
|
||||
isIPad | appShowScanButton: (searchQuery$ | async):errorMessage
|
||||
"
|
||||
type="submit"
|
||||
(click)="handleBtnClick(submitButton)"
|
||||
></button>
|
||||
|
||||
@@ -3,7 +3,11 @@ import { CommonModule } from '@angular/common';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { SearchInputModule } from '@libs/ui';
|
||||
import { FocusDirective } from '../../shared/directives';
|
||||
import { TruncateTextPipe, ShowSearchResetPipe } from '../../shared/pipes';
|
||||
import {
|
||||
TruncateTextPipe,
|
||||
ShowSearchResetPipe,
|
||||
ShowScanButtonPipe,
|
||||
} from '../../shared/pipes';
|
||||
import { ShelfSearchbarComponent } from './searchbar.component';
|
||||
|
||||
@NgModule({
|
||||
@@ -12,12 +16,14 @@ import { ShelfSearchbarComponent } from './searchbar.component';
|
||||
ShelfSearchbarComponent,
|
||||
ShowSearchResetPipe,
|
||||
TruncateTextPipe,
|
||||
ShowScanButtonPipe,
|
||||
FocusDirective,
|
||||
],
|
||||
declarations: [
|
||||
ShelfSearchbarComponent,
|
||||
ShowSearchResetPipe,
|
||||
TruncateTextPipe,
|
||||
ShowScanButtonPipe,
|
||||
FocusDirective,
|
||||
],
|
||||
providers: [],
|
||||
|
||||
@@ -156,7 +156,6 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
|
||||
type: 'search',
|
||||
value: searchbarValue,
|
||||
bypassValidation: true,
|
||||
closeOverlay: () => this.close(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
OnInit,
|
||||
ViewChild,
|
||||
ElementRef,
|
||||
ChangeDetectionStrategy,
|
||||
} from '@angular/core';
|
||||
import { SearchStateFacade } from 'apps/sales/src/app/store/customer';
|
||||
import { first, takeUntil, map } from 'rxjs/operators';
|
||||
@@ -15,7 +16,7 @@ import { groupBy } from 'apps/sales/src/app/utils';
|
||||
selector: 'app-shelf-search-results',
|
||||
templateUrl: './shelf-search-results.component.html',
|
||||
styleUrls: ['./shelf-search-results.component.scss'],
|
||||
// changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
|
||||
@ViewChild('scroll', { static: true })
|
||||
@@ -23,17 +24,17 @@ export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
|
||||
|
||||
destroy$ = new Subject();
|
||||
|
||||
grouped$ = this.searchStateFacade.result$.pipe(map((results) => groupBy(results, (item) => item.buyerNumber)));
|
||||
grouped$ = this.searchStateFacade.result$.pipe(
|
||||
map((results) => groupBy(results, (item) => item.buyerNumber))
|
||||
);
|
||||
|
||||
fetching$ = this.searchStateFacade.fetching$;
|
||||
|
||||
constructor(private searchStateFacade: SearchStateFacade) {
|
||||
this.searchStateFacade.clearResult();
|
||||
}
|
||||
constructor(private searchStateFacade: SearchStateFacade) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.initScrollContainer();
|
||||
this.fetch(true);
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
initScrollContainer() {
|
||||
@@ -66,7 +67,7 @@ export class ShelfSearchResultsComponent implements OnInit, OnDestroy {
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
if (force || (hits > result.length && !fetching)) {
|
||||
if (force || !hits || (!result.length && !fetching)) {
|
||||
this.searchStateFacade.fetchResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<div class="search-container">
|
||||
<app-shelf-searchbar
|
||||
[isFetchingData]="isFetchingData$ | async"
|
||||
[isFetchingData]="isFetching$ | async"
|
||||
[errorMessage]="errorMessage$ | async"
|
||||
[isIPad]="allowScan"
|
||||
[mode]="searchMode$ | async"
|
||||
|
||||
@@ -7,15 +7,19 @@ import {
|
||||
ViewChild,
|
||||
AfterViewInit,
|
||||
} from '@angular/core';
|
||||
import { Subject, BehaviorSubject, Observable, of, NEVER } from 'rxjs';
|
||||
import {
|
||||
Subject,
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
of,
|
||||
NEVER,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
ShelfSearchFacadeService,
|
||||
ShelfNavigationService,
|
||||
} from '../../../shared/services';
|
||||
import {
|
||||
ListResponseArgsOfOrderItemListItemDTO,
|
||||
AutocompleteDTO,
|
||||
} from '@swagger/oms';
|
||||
import { AutocompleteDTO, OrderItemListItemDTO } from '@swagger/oms';
|
||||
import {
|
||||
distinctUntilChanged,
|
||||
debounceTime,
|
||||
@@ -23,10 +27,10 @@ import {
|
||||
map,
|
||||
takeUntil,
|
||||
filter,
|
||||
first,
|
||||
shareReplay,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
startWith,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
import { SHELF_SCROLL_INDEX } from 'apps/sales/src/app/core/utils/app.constants';
|
||||
import { ShelfSearchbarComponent } from '../../../components';
|
||||
@@ -34,7 +38,6 @@ import { ShelfFilterService } from '../../../services/shelf-filter.service';
|
||||
import { SearchStateFacade } from '@shelf-store';
|
||||
import { CollectingShelfScannerScanditComponent } from 'shared/public_api';
|
||||
import { AutocompleteOptions } from '../../../defs';
|
||||
import { ClickOutsideDirective } from 'apps/sales/src/app/shared/directives';
|
||||
|
||||
@Component({
|
||||
selector: 'app-shelf-search-input',
|
||||
@@ -44,6 +47,9 @@ import { ClickOutsideDirective } from 'apps/sales/src/app/shared/directives';
|
||||
})
|
||||
export class ShelfSearchInputComponent
|
||||
implements OnInit, AfterViewInit, OnDestroy {
|
||||
NO_RESULT_ERROR_MESSAGE = 'Ergibt keine Suchergebnisse';
|
||||
GENERAL_ERROR_MESSAGE = 'Ein Fehler ist aufgetreten';
|
||||
|
||||
@Input() allowScan = false;
|
||||
@Input() hasAutocomplete = true;
|
||||
@Input() searchCallback: () => void;
|
||||
@@ -55,9 +61,9 @@ export class ShelfSearchInputComponent
|
||||
|
||||
destroy$ = new Subject();
|
||||
|
||||
isFetchingData$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
errorMessage$ = new BehaviorSubject<string>('');
|
||||
isFetching$: Observable<boolean>;
|
||||
errorMessage$: Observable<string>;
|
||||
hits$ = new Observable();
|
||||
|
||||
autocompleteQueryString$ = new BehaviorSubject<string>('');
|
||||
autocompleteResult$: Observable<AutocompleteDTO[]>;
|
||||
@@ -79,12 +85,14 @@ export class ShelfSearchInputComponent
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.clearPreviousResults();
|
||||
if (this.hasAutocomplete) {
|
||||
this.setupAutocompletion();
|
||||
}
|
||||
|
||||
this.setUpSearchMode();
|
||||
this.initSelectedFilters();
|
||||
this.initStore();
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
@@ -97,9 +105,8 @@ export class ShelfSearchInputComponent
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setIsFetchingData(false);
|
||||
this.resetQueryString();
|
||||
this.resetError();
|
||||
this.searchStateFacade.clearError();
|
||||
}
|
||||
|
||||
triggerBarcodeSearch(barcode: string) {
|
||||
@@ -111,70 +118,27 @@ export class ShelfSearchInputComponent
|
||||
type,
|
||||
value,
|
||||
bypassValidation,
|
||||
closeOverlay,
|
||||
}: {
|
||||
type: 'search' | 'scan';
|
||||
value: string;
|
||||
selectedFilters?: { [key: string]: string };
|
||||
bypassValidation?: boolean;
|
||||
closeOverlay?: () => void;
|
||||
} = {
|
||||
type: 'search',
|
||||
value: '',
|
||||
bypassValidation: false,
|
||||
selectedFilters: {},
|
||||
}
|
||||
) {
|
||||
let result$: Observable<ListResponseArgsOfOrderItemListItemDTO>;
|
||||
let searchQuery = value;
|
||||
this.setIsFetchingData(true);
|
||||
this.resetError();
|
||||
|
||||
if (this.isAutocompleteSelected()) {
|
||||
searchQuery = this.selectedItem$.value.query;
|
||||
result$ = this.selectedFilters$.pipe(
|
||||
debounceTime(250),
|
||||
first(),
|
||||
switchMap((selectedFilters) =>
|
||||
this.shelfSearchService.search(searchQuery, {
|
||||
hitsOnly: true,
|
||||
bypassValidation,
|
||||
selectedFilters,
|
||||
})
|
||||
)
|
||||
);
|
||||
this.shelfSearchService.search(searchQuery, { bypassValidation });
|
||||
this.updateSearchbarValue(searchQuery);
|
||||
} else if (type === 'search') {
|
||||
result$ = this.selectedFilters$.pipe(
|
||||
debounceTime(250),
|
||||
first(),
|
||||
switchMap((selectedFilters) =>
|
||||
this.shelfSearchService.search(value, {
|
||||
hitsOnly: true,
|
||||
bypassValidation,
|
||||
selectedFilters,
|
||||
})
|
||||
)
|
||||
);
|
||||
this.shelfSearchService.search(value, { bypassValidation });
|
||||
} else if (type === 'scan') {
|
||||
result$ = this.shelfSearchService.searchWithBarcode(value);
|
||||
this.shelfSearchService.searchWithBarcode(value);
|
||||
}
|
||||
|
||||
result$
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
first(),
|
||||
tap(() => this.searchStateFacade.setInput(searchQuery))
|
||||
)
|
||||
.subscribe(
|
||||
(result) =>
|
||||
this.handleSearchResult(
|
||||
result,
|
||||
searchQuery,
|
||||
closeOverlay || this.searchCallback
|
||||
),
|
||||
() => this.handleSearchResultError()
|
||||
);
|
||||
}
|
||||
|
||||
openScanner() {
|
||||
@@ -198,11 +162,6 @@ export class ShelfSearchInputComponent
|
||||
handleAutocompleteAction(selectItem: AutocompleteDTO & AutocompleteOptions) {
|
||||
const oldValue = this.selectedItem$.value && this.selectedItem$.value.query;
|
||||
const newValue = selectItem.query;
|
||||
console.log({
|
||||
oldValue,
|
||||
newValue,
|
||||
shouldOverwrite: this.isAutocompleteOverwritten(oldValue, newValue),
|
||||
});
|
||||
|
||||
if (this.isAutocompleteOverwritten(oldValue, newValue)) {
|
||||
return this.setSelectedAutocompleteItem(null);
|
||||
@@ -214,52 +173,14 @@ export class ShelfSearchInputComponent
|
||||
}
|
||||
}
|
||||
|
||||
private handleSearchResult(
|
||||
result: ListResponseArgsOfOrderItemListItemDTO,
|
||||
value: string,
|
||||
successCB?: () => void
|
||||
) {
|
||||
this.setIsFetchingData(false);
|
||||
if (this.hasNoResult(result)) {
|
||||
return this.setErrorMessage('Ergibt keine Suchergebnisse');
|
||||
} else if (this.hasMultipleSearchResults(result)) {
|
||||
this.shelfNavigationService.navigateToResultList({
|
||||
searchQuery: value,
|
||||
numberOfHits: result.hits,
|
||||
});
|
||||
this.resetSessionStorage();
|
||||
} else {
|
||||
this.shelfNavigationService.navigateToDetails(this.getDetails(result));
|
||||
}
|
||||
|
||||
if (successCB) {
|
||||
successCB();
|
||||
}
|
||||
}
|
||||
|
||||
private hasNoResult(result: ListResponseArgsOfOrderItemListItemDTO): boolean {
|
||||
return !!result.result && !result.result.length;
|
||||
}
|
||||
|
||||
private hasMultipleSearchResults(
|
||||
result: ListResponseArgsOfOrderItemListItemDTO
|
||||
): boolean {
|
||||
return !!result.result && result.result.length > 1;
|
||||
private clearPreviousResults() {
|
||||
this.searchStateFacade.clearResult();
|
||||
}
|
||||
|
||||
private resetSessionStorage(key: string = SHELF_SCROLL_INDEX) {
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
|
||||
private handleSearchResultError(errorCB?: () => void) {
|
||||
this.setErrorMessage('Ein Fehler ist aufgetreten');
|
||||
this.setIsFetchingData(false);
|
||||
|
||||
if (errorCB) {
|
||||
errorCB();
|
||||
}
|
||||
}
|
||||
|
||||
private setupAutocompletion() {
|
||||
this.autocompleteResult$ = this.autocompleteQueryString$.pipe(
|
||||
debounceTime(250),
|
||||
@@ -297,16 +218,70 @@ export class ShelfSearchInputComponent
|
||||
this.selectedFilters$ = this.searchStateFacade.selectedFilter$;
|
||||
}
|
||||
|
||||
private initStore() {
|
||||
this.isFetching$ = this.searchStateFacade.fetching$.pipe(debounceTime(25));
|
||||
this.errorMessage$ = this.searchStateFacade.showNoResultError$.pipe(
|
||||
map((showError) => (showError ? this.NO_RESULT_ERROR_MESSAGE : '')),
|
||||
startWith('')
|
||||
);
|
||||
|
||||
this.setUpNavigation();
|
||||
this.setUpErrorCleaner();
|
||||
}
|
||||
|
||||
private setUpNavigation() {
|
||||
this.searchStateFacade.hasResults$
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
debounceTime(25),
|
||||
filter((hasResults) => !!hasResults),
|
||||
withLatestFrom(
|
||||
this.searchStateFacade.hits$,
|
||||
this.searchStateFacade.input$,
|
||||
this.searchStateFacade.result$
|
||||
),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(([_, numberOfHits, searchQuery, result]) => {
|
||||
if (this.shouldNavigateToResultList(numberOfHits)) {
|
||||
this.resetSessionStorage();
|
||||
|
||||
this.shelfNavigationService.navigateToResultList({
|
||||
searchQuery,
|
||||
numberOfHits,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.shouldNavigateToDetails(numberOfHits)) {
|
||||
this.shelfNavigationService.navigateToDetails(
|
||||
this.getDetails(result)
|
||||
);
|
||||
}
|
||||
|
||||
if (this.searchCallback) {
|
||||
this.searchCallback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setUpErrorCleaner() {
|
||||
this.autocompleteQueryString$
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
withLatestFrom(this.isFetching$, this.errorMessage$),
|
||||
filter(([_, isFetching, errorMessage]) =>
|
||||
this.shouldClearError(isFetching, errorMessage)
|
||||
)
|
||||
)
|
||||
.subscribe(() => this.searchStateFacade.clearError());
|
||||
}
|
||||
|
||||
private setUpInputFocus() {
|
||||
this.filterService.overlayClosed$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(() => this.setFocus());
|
||||
}
|
||||
|
||||
private getDetails(result: ListResponseArgsOfOrderItemListItemDTO) {
|
||||
return result.result && result.result[0];
|
||||
}
|
||||
|
||||
private setFocus() {
|
||||
if (this.searchbar) {
|
||||
this.searchbar.setFocus('input');
|
||||
@@ -317,18 +292,6 @@ export class ShelfSearchInputComponent
|
||||
this.autocompleteQueryString$.next('');
|
||||
}
|
||||
|
||||
private setIsFetchingData(isFetching: boolean) {
|
||||
this.isFetchingData$.next(isFetching);
|
||||
}
|
||||
|
||||
private resetError() {
|
||||
this.setErrorMessage('');
|
||||
}
|
||||
|
||||
private setErrorMessage(message: string) {
|
||||
this.errorMessage$.next(message);
|
||||
}
|
||||
|
||||
private setSelectedAutocompleteItem(
|
||||
item: (AutocompleteDTO & AutocompleteOptions) | null
|
||||
) {
|
||||
@@ -382,4 +345,20 @@ export class ShelfSearchInputComponent
|
||||
this.setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private shouldNavigateToResultList(hits: number): boolean {
|
||||
return hits > 1;
|
||||
}
|
||||
|
||||
private shouldNavigateToDetails(hits: number): boolean {
|
||||
return hits === 1;
|
||||
}
|
||||
|
||||
private shouldClearError(isFetching: boolean, errorMessage: string): boolean {
|
||||
return !isFetching && !!errorMessage;
|
||||
}
|
||||
|
||||
private getDetails(result: OrderItemListItemDTO[]) {
|
||||
return result && result[0];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// start:ng42.barrel
|
||||
export * from './show-search-reset.pipe';
|
||||
export * from './truncate.pipe';
|
||||
export * from './show-scan-button.pipe';
|
||||
// end:ng42.barrel
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ShowScanButtonPipe } from './show-scan-button.pipe';
|
||||
|
||||
fdescribe('#ShowScanButtonPipe', () => {
|
||||
let pipe: ShowScanButtonPipe;
|
||||
|
||||
let isIpad: boolean;
|
||||
let searchQuery: string;
|
||||
let errorMessage: string;
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new ShowScanButtonPipe();
|
||||
});
|
||||
|
||||
it('it should not be shown on desktop', () => {
|
||||
isIpad = false;
|
||||
searchQuery = '';
|
||||
errorMessage = '';
|
||||
|
||||
const result = pipe.transform(isIpad, searchQuery, errorMessage);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('it should shown on iPad if no input is yet made and no error is present', () => {
|
||||
isIpad = true;
|
||||
searchQuery = '';
|
||||
errorMessage = '';
|
||||
|
||||
const result = pipe.transform(isIpad, searchQuery, errorMessage);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('it should not be shown on iPad if an input is made', () => {
|
||||
isIpad = true;
|
||||
searchQuery = 'Testkunde';
|
||||
errorMessage = '';
|
||||
|
||||
const result = pipe.transform(isIpad, searchQuery, errorMessage);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('it should not be shown on iPad if an input is made', () => {
|
||||
isIpad = true;
|
||||
searchQuery = '';
|
||||
errorMessage = 'Unbekannter Fehler';
|
||||
|
||||
const result = pipe.transform(isIpad, searchQuery, errorMessage);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'appShowScanButton',
|
||||
})
|
||||
export class ShowScanButtonPipe implements PipeTransform {
|
||||
transform(
|
||||
isIpad: boolean,
|
||||
searchQuery: string,
|
||||
errorMessage: string
|
||||
): boolean {
|
||||
if (!isIpad) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!!searchQuery || !!errorMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,14 @@ import { Injectable } from '@angular/core';
|
||||
import { Select } from '@ngxs/store';
|
||||
import { BranchSelectors } from 'apps/sales/src/app/core/store/selectors/branch.selector';
|
||||
import { Observable } from 'rxjs';
|
||||
import { filter, switchMap, map } from 'rxjs/operators';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { CollectingShelfService } from 'apps/sales/src/app/core/services/collecting-shelf.service';
|
||||
import {
|
||||
ListResponseArgsOfOrderItemListItemDTO,
|
||||
AutocompleteTokenDTO,
|
||||
ResponseArgsOfIEnumerableOfAutocompleteDTO,
|
||||
} from '@swagger/oms/lib';
|
||||
import { SearchStateFacade } from '@shelf-store';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ShelfSearchFacadeService {
|
||||
@@ -17,77 +17,63 @@ export class ShelfSearchFacadeService {
|
||||
string
|
||||
>;
|
||||
|
||||
constructor(private collectingShelfService: CollectingShelfService) {}
|
||||
constructor(
|
||||
private collectingShelfService: CollectingShelfService,
|
||||
private searchStateFacade: SearchStateFacade
|
||||
) {}
|
||||
|
||||
search(
|
||||
queryString: string,
|
||||
options: {
|
||||
hitsOnly: boolean;
|
||||
bypassValidation: boolean;
|
||||
selectedFilters: { [key: string]: string };
|
||||
} = {
|
||||
hitsOnly: false,
|
||||
bypassValidation: false,
|
||||
selectedFilters: {},
|
||||
}
|
||||
) {
|
||||
const searchParams = queryString.trim();
|
||||
const { hitsOnly, bypassValidation, selectedFilters } = options;
|
||||
const searchQuery = queryString.trim();
|
||||
const { bypassValidation } = options;
|
||||
|
||||
if (!bypassValidation && !this.isValidSearchQuery(searchParams)) {
|
||||
if (!bypassValidation && !this.isValidSearchQuery(searchQuery)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.requestSearch(searchParams, {
|
||||
hitsOnly,
|
||||
selectedFilters,
|
||||
}).pipe(map(this.handleSearchResult));
|
||||
this.searchStateFacade.setInput(searchQuery);
|
||||
return this.requestSearch();
|
||||
}
|
||||
|
||||
searchWithBarcode(barcode: string) {
|
||||
const searchParams = this.getBarcodeSearchParams(barcode);
|
||||
const searchQuery = this.getBarcodeSearchQuery(barcode);
|
||||
|
||||
if (!this.isValidSearchQuery(searchParams)) {
|
||||
if (!this.isValidSearchQuery(searchQuery)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.requestSearch(searchParams).pipe(map(this.handleSearchResult));
|
||||
this.searchStateFacade.setInput(searchQuery);
|
||||
return this.requestSearch();
|
||||
}
|
||||
|
||||
searchForAutocomplete(
|
||||
queryString: string,
|
||||
options: { selectedFilters?: { [key: string]: string } } = {}
|
||||
) {
|
||||
const searchParams = queryString.trim();
|
||||
const searchQuery = queryString.trim();
|
||||
const autoCompleteQuery: AutocompleteTokenDTO = this.generateAutocompleteToken(
|
||||
{ queryString, filter: options.selectedFilters || {}, take: 5 }
|
||||
{
|
||||
queryString: searchQuery,
|
||||
filter: options.selectedFilters || {},
|
||||
take: 5,
|
||||
}
|
||||
);
|
||||
|
||||
if (!this.isValidSearchQuery(searchParams)) {
|
||||
if (!this.isValidSearchQuery(searchQuery)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.requestAutocompleteSearch(autoCompleteQuery);
|
||||
}
|
||||
|
||||
private requestSearch(
|
||||
input: string,
|
||||
options?: {
|
||||
hitsOnly: boolean;
|
||||
selectedFilters?: { [key: string]: string };
|
||||
}
|
||||
): Observable<ListResponseArgsOfOrderItemListItemDTO> {
|
||||
return this.currentUserBranchId$.pipe(
|
||||
filter((branchNumber) => !isNullOrUndefined(branchNumber)),
|
||||
switchMap((branchNumber) =>
|
||||
this.collectingShelfService.searchWarenausgabe({
|
||||
input,
|
||||
branchNumber,
|
||||
selectedFilters: options.selectedFilters,
|
||||
hitsOnly: options.hitsOnly,
|
||||
})
|
||||
)
|
||||
);
|
||||
private requestSearch() {
|
||||
return this.searchStateFacade.fetchResult();
|
||||
}
|
||||
|
||||
private generateAutocompleteToken(params: {
|
||||
@@ -116,21 +102,11 @@ export class ShelfSearchFacadeService {
|
||||
);
|
||||
}
|
||||
|
||||
private handleSearchResult(result: ListResponseArgsOfOrderItemListItemDTO) {
|
||||
if (!result) {
|
||||
return {
|
||||
result: [],
|
||||
} as ListResponseArgsOfOrderItemListItemDTO;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private isValidSearchQuery(queryString: string): boolean {
|
||||
return !!queryString && !!queryString.length;
|
||||
}
|
||||
|
||||
private getBarcodeSearchParams(barcode: string) {
|
||||
private getBarcodeSearchQuery(barcode: string) {
|
||||
return barcode.replace('ORD:', '').trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import { Dictionary } from '@ngrx/entity';
|
||||
import { Observable } from 'rxjs';
|
||||
import * as selectors from './history.selectors';
|
||||
import * as actions from './history.actions';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HistoryStateFacade {
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './search-process';
|
||||
export * from './search-process.state';
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export enum SearchProcessState {
|
||||
INIT,
|
||||
FETCHING,
|
||||
FETCHED,
|
||||
ERROR,
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { OrderItemListItemDTO } from '@swagger/oms';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { SearchProcessState } from '@shelf-store';
|
||||
|
||||
export interface SearchProcess {
|
||||
id: number; // Prozess ID;
|
||||
@@ -11,5 +12,6 @@ export interface SearchProcess {
|
||||
};
|
||||
result: OrderItemListItemDTO[];
|
||||
hits?: number;
|
||||
fetching: boolean;
|
||||
state: SearchProcessState;
|
||||
timestamp?: number;
|
||||
}
|
||||
|
||||
@@ -48,11 +48,21 @@ export const setInput = createAction(
|
||||
props<{ id: number; input: string }>()
|
||||
);
|
||||
|
||||
export const clearError = createAction(
|
||||
`${prefix} Clear Error`,
|
||||
props<{ id: number }>()
|
||||
);
|
||||
|
||||
export const clearResults = createAction(
|
||||
`${prefix} Clear Results`,
|
||||
props<{ id: number }>()
|
||||
);
|
||||
|
||||
export const reloadResults = createAction(
|
||||
`${prefix} Reload Results`,
|
||||
props<{ id: number }>()
|
||||
);
|
||||
|
||||
export const addResult = createAction(
|
||||
`${prefix} Add Result`,
|
||||
props<{ id: number; result: OrderItemListItemDTO[] }>()
|
||||
@@ -68,9 +78,14 @@ export const setHits = createAction(
|
||||
props<{ id: number; hits: number }>()
|
||||
);
|
||||
|
||||
export const setTimestamp = createAction(
|
||||
`${prefix} Set Timestamp`,
|
||||
props<{ id: number }>()
|
||||
);
|
||||
|
||||
export const fetchResult = createAction(
|
||||
`${prefix} Fetch Result`,
|
||||
props<{ id: number }>()
|
||||
props<{ id: number; skip?: number; take?: number }>()
|
||||
);
|
||||
|
||||
export const fetchResultDone = createAction(
|
||||
|
||||
@@ -38,13 +38,19 @@ export class SearchEffects {
|
||||
private ngxsActions$: NgxsActions,
|
||||
private store: Store<any>,
|
||||
private searchStateFacade: SearchStateFacade,
|
||||
private orderService: OrderService,
|
||||
private branchService: BranchService
|
||||
private orderService: OrderService
|
||||
) {
|
||||
this.initAddProcess();
|
||||
this.initRemoveProcess();
|
||||
}
|
||||
|
||||
reloadResults$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(actions.reloadResults),
|
||||
map((a) => actions.fetchResult({ id: a.id, skip: 0 }))
|
||||
)
|
||||
);
|
||||
|
||||
fetchResults$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(actions.fetchResult),
|
||||
@@ -56,16 +62,15 @@ export class SearchEffects {
|
||||
),
|
||||
flatMap(([_, process, filter]) =>
|
||||
this.orderService
|
||||
.OrderQueryOrderItemResponse({
|
||||
branchNumber: this.branchService.getCurrentBranchNumber(),
|
||||
.OrderWarenausgabeResponse({
|
||||
input: {
|
||||
qs: process.input,
|
||||
},
|
||||
filter: (filter as unknown) as {
|
||||
[key: string]: string;
|
||||
},
|
||||
skip: process.result.length || 0,
|
||||
take: 20,
|
||||
skip: a.skip || process.result.length || 0,
|
||||
take: a.take || 20,
|
||||
})
|
||||
.pipe(
|
||||
catchError((err) =>
|
||||
@@ -89,7 +94,14 @@ export class SearchEffects {
|
||||
flatMap((action) => {
|
||||
if (action.response.ok) {
|
||||
const result = action.response.body;
|
||||
const shouldSetTimestamp = !!result.skip;
|
||||
return [
|
||||
...(shouldSetTimestamp
|
||||
? [
|
||||
actions.setTimestamp({ id: action.id }),
|
||||
actions.clearResults({ id: action.id }),
|
||||
]
|
||||
: []),
|
||||
actions.setHits({ id: action.id, hits: result.hits }),
|
||||
actions.addResult({ id: action.id, result: result.result }),
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Store } from '@ngrx/store';
|
||||
import { Store as NgxsStore } from '@ngxs/store';
|
||||
import * as actions from './search.actions';
|
||||
import * as selectors from './search.selectors';
|
||||
import { switchMap, filter, map, first } from 'rxjs/operators';
|
||||
import { switchMap, filter, map, first, withLatestFrom } from 'rxjs/operators';
|
||||
import { SharedSelectors } from 'apps/sales/src/app/core/store/selectors/shared.selectors';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import {
|
||||
@@ -14,6 +14,7 @@ import { Observable } from 'rxjs';
|
||||
import { isNullOrUndefined } from 'util';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { SearchProcessState } from './defs';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SearchStateFacade {
|
||||
@@ -40,6 +41,29 @@ export class SearchStateFacade {
|
||||
return this.getProcessId$().pipe(switchMap((id) => this.getFetching$(id)));
|
||||
}
|
||||
|
||||
get state$() {
|
||||
return this.getProcessId$().pipe(switchMap((id) => this.getState$(id)));
|
||||
}
|
||||
|
||||
get hasError$() {
|
||||
return this.getProcessId$().pipe(switchMap((id) => this.getHasError$(id)));
|
||||
}
|
||||
|
||||
get hasResults$() {
|
||||
return this.getProcessId$().pipe(switchMap((id) => this.getHasResult$(id)));
|
||||
}
|
||||
|
||||
get showNoResultError$() {
|
||||
return this.getProcessId$().pipe(
|
||||
switchMap((id) =>
|
||||
combineLatest([this.getState$(id), this.getHasResult$(id)])
|
||||
),
|
||||
map(([state, hasResults]) =>
|
||||
!hasResults && state === SearchProcessState.FETCHED ? true : false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get primaryFilters$(): Observable<PrimaryFilterOption[]> {
|
||||
return this.process$.pipe(
|
||||
switchMap((process) => this.getPrimaryFilters(process.id))
|
||||
@@ -122,10 +146,22 @@ export class SearchStateFacade {
|
||||
return this.store.select(selectors.selectHits, id);
|
||||
}
|
||||
|
||||
getState$(id: number) {
|
||||
return this.store.select(selectors.selectState, id);
|
||||
}
|
||||
|
||||
getFetching$(id: number) {
|
||||
return this.store.select(selectors.selectFetching, id);
|
||||
}
|
||||
|
||||
getHasResult$(id: number) {
|
||||
return this.store.select(selectors.selectHasResults, id);
|
||||
}
|
||||
|
||||
getHasError$(id: number) {
|
||||
return this.store.select(selectors.selectHasError, id);
|
||||
}
|
||||
|
||||
getPrimaryFilters(id: number): Observable<PrimaryFilterOption[]> {
|
||||
return this.store.select(selectors.selectPrimaryFilters, id);
|
||||
}
|
||||
@@ -189,6 +225,14 @@ export class SearchStateFacade {
|
||||
this.store.dispatch(actions.fetchResult({ id: processId }));
|
||||
}
|
||||
|
||||
async clearError(id?: number) {
|
||||
let processId = id;
|
||||
if (typeof processId !== 'number') {
|
||||
processId = await this.getProcessId();
|
||||
}
|
||||
this.store.dispatch(actions.clearError({ id: processId }));
|
||||
}
|
||||
|
||||
async clearResult(id?: number) {
|
||||
let processId = id;
|
||||
if (typeof processId !== 'number') {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
INITIAL_SEARCH_PROCESS,
|
||||
} from './search.state';
|
||||
import * as actions from './search.actions';
|
||||
import { SearchProcessState } from './defs';
|
||||
|
||||
const _searchReducer = createReducer(
|
||||
INITIAL_SEARCH_STATE,
|
||||
@@ -43,14 +44,31 @@ const _searchReducer = createReducer(
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.clearError, (s, a) =>
|
||||
searchStateAdapter.updateOne(
|
||||
{
|
||||
id: a.id,
|
||||
changes: {
|
||||
state: SearchProcessState.INIT,
|
||||
},
|
||||
},
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.clearResults, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { result: [] } }, s)
|
||||
searchStateAdapter.updateOne(
|
||||
{ id: a.id, changes: { result: [], state: SearchProcessState.INIT } },
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.addResult, (s, a) =>
|
||||
searchStateAdapter.updateOne(
|
||||
{
|
||||
id: a.id,
|
||||
changes: { result: [...s.entities[a.id].result, ...a.result] },
|
||||
changes: {
|
||||
result: [...s.entities[a.id].result, ...a.result],
|
||||
state: SearchProcessState.FETCHED,
|
||||
},
|
||||
},
|
||||
s
|
||||
)
|
||||
@@ -61,11 +79,26 @@ const _searchReducer = createReducer(
|
||||
on(actions.setHits, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { hits: a.hits } }, s)
|
||||
),
|
||||
on(actions.setTimestamp, (s, a) =>
|
||||
searchStateAdapter.updateOne(
|
||||
{
|
||||
id: a.id,
|
||||
changes: { timestamp: Date.now() },
|
||||
},
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.fetchResult, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { fetching: true } }, s)
|
||||
searchStateAdapter.updateOne(
|
||||
{ id: a.id, changes: { state: SearchProcessState.FETCHING } },
|
||||
s
|
||||
)
|
||||
),
|
||||
on(actions.fetchResultDone, (s, a) =>
|
||||
searchStateAdapter.updateOne({ id: a.id, changes: { fetching: false } }, s)
|
||||
searchStateAdapter.updateOne(
|
||||
{ id: a.id, changes: { state: SearchProcessState.FETCHED } },
|
||||
s
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createSelector } from '@ngrx/store';
|
||||
import { selectShelfState } from '../shelf.selectors';
|
||||
import { searchStateAdapter } from './search.state';
|
||||
import { Dictionary } from '@ngrx/entity';
|
||||
import { SearchProcess } from './defs';
|
||||
import { SearchProcess, SearchProcessState } from './defs';
|
||||
|
||||
export const selectSearchState = createSelector(
|
||||
selectShelfState,
|
||||
@@ -36,10 +36,30 @@ export const selectHits = createSelector(
|
||||
entities[id] && entities[id].hits
|
||||
);
|
||||
|
||||
export const selectState = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].state
|
||||
);
|
||||
|
||||
export const selectFetching = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].fetching
|
||||
entities[id] && entities[id].state === SearchProcessState.FETCHING
|
||||
);
|
||||
|
||||
export const selectHasError = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] && entities[id].state === SearchProcessState.ERROR
|
||||
);
|
||||
|
||||
export const selectHasResults = createSelector(
|
||||
selectEntities,
|
||||
(entities: Dictionary<SearchProcess>, id: number) =>
|
||||
entities[id] &&
|
||||
entities[id].state === SearchProcessState.FETCHED &&
|
||||
!!entities[id].hits
|
||||
);
|
||||
|
||||
export const selectFilters = createSelector(
|
||||
|
||||
@@ -2,6 +2,7 @@ import { EntityState, createEntityAdapter } from '@ngrx/entity';
|
||||
import { SearchProcess } from './defs';
|
||||
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
|
||||
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
|
||||
import { SearchProcessState } from './defs';
|
||||
|
||||
export interface SearchState extends EntityState<SearchProcess> {}
|
||||
|
||||
@@ -23,6 +24,6 @@ export const INITIAL_SEARCH_PROCESS: SearchProcess = {
|
||||
id: undefined,
|
||||
result: [],
|
||||
input: '',
|
||||
fetching: false,
|
||||
state: SearchProcessState.INIT,
|
||||
filters: INITIAL_FILTERS,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user