Merge branch 'feature/189-Warenausgabe/276-Startseite/667-Autovervollstaendigung' into feature/189-Warenausgabe/276-Startseite/main

This commit is contained in:
Sebastian
2020-07-07 09:47:34 +02:00
45 changed files with 1026 additions and 248 deletions

View File

@@ -3,6 +3,7 @@ import { Observable } from 'rxjs';
import { RemissionSelectors } from '../../core/store/selectors/remission.selectors';
import { Select } from '@ngxs/store';
import { ContentHeaderService } from '../../core/services/content-header.service';
import { shareReplay } from 'rxjs/operators';
@Component({
selector: 'app-content-header',
@@ -22,9 +23,9 @@ export class ContentHeaderComponent implements OnInit {
constructor(private contentHeaderService: ContentHeaderService) {}
ngOnInit() {
this.isFilterActive$ = this.contentHeaderService.isFilterActive$;
this.showFilter$ = this.contentHeaderService.showFilter$;
this.showBreadCrumbs$ = this.contentHeaderService.showBreadcrumbs$;
this.isFilterActive$ = this.contentHeaderService.isFilterActive$;
this.activeModule$ = this.contentHeaderService.module$;
}

View File

@@ -101,11 +101,13 @@ export class CollectingShelfService {
skip = 0,
take = 10,
hitsOnly = false,
selectedFilters = {},
}): Observable<ListResponseArgsOfOrderItemListItemDTO> {
const params = <QueryTokenDTO>{
input: {
qs: input,
},
filter: selectedFilters,
skip,
take,
hitsOnly,

View File

@@ -1,15 +1,15 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map, switchMap, filter, startWith } from 'rxjs/operators';
import { map, switchMap, filter, tap, shareReplay } from 'rxjs/operators';
import { LocationService } from './location-service';
import { RemissionOverlayService } from '../../modules/remission/services';
import { Select } from '@ngxs/store';
import { isNullOrUndefined } from 'util';
import { ShelfOverlayService } from '../../modules/shelf/services';
import { RemissionSelectors } from '../store/selectors/remission.selectors';
import { SharedSelectors } from '../store/selectors/shared.selectors';
import { SearchStateFacade } from '../../store/customer';
import { isNullOrUndefined } from 'util';
@Injectable({ providedIn: 'root' })
export class ContentHeaderService {
@@ -40,29 +40,26 @@ export class ContentHeaderService {
}
get isFilterActive$(): Observable<boolean> {
return this.activeSection$.pipe(
return this.locationService.url$.pipe(
filter((url) => !isNullOrUndefined(url)),
switchMap((activeSection) => {
let filters$: Observable<string[]> = of([]);
switch (activeSection) {
case 'remission':
filters$ = this.remissionFilters$;
break;
case 'shelf':
filters$ = this.searchStore.selectedFilter$.pipe(
filter((filters) => !isNullOrUndefined(filters) && !filters.length),
// To ensure if no filter is set on process that filter is inactive
startWith([])
);
break;
default:
return of(false);
if (activeSection.includes('remission')) {
return this.remissionFilters$.pipe(
tap((selectedFilters) => console.log({ selectedFilters })),
map(
(selectedFilters) =>
selectedFilters && !!Object.entries(selectedFilters).length
)
);
}
return filters$.pipe(map((selectedFilters) => selectedFilters && !!Object.entries(selectedFilters).length));
})
if (activeSection.includes('shelf')) {
return this.searchStore.hasActiveFilters$;
}
return of(false);
}),
shareReplay()
);
}
@@ -115,12 +112,18 @@ export class ContentHeaderService {
const applicableBlacklist = this.blackList[type];
if (type === 'filter') {
const isOnWhiteList = !!applicableWhitelist.find((whitelistetUrl) => whitelistetUrl.includes(url) || url.includes(whitelistetUrl));
const isOnWhiteList = !!applicableWhitelist.find(
(whitelistetUrl) =>
whitelistetUrl.includes(url) || url.includes(whitelistetUrl)
);
return isOnWhiteList;
}
if (type === 'breadcrumbs') {
const isOnBlackList = !!applicableBlacklist.find((blacklistetUrl) => blacklistetUrl.includes(url) || url.includes(blacklistetUrl));
const isOnBlackList = !!applicableBlacklist.find(
(blacklistetUrl) =>
blacklistetUrl.includes(url) || url.includes(blacklistetUrl)
);
return !isOnBlackList;
}
}

View File

@@ -1,7 +1,19 @@
import { Injectable, OnInit } from '@angular/core';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/internal/Observable';
import { Router, RouterEvent, ActivatedRoute } from '@angular/router';
import { map, distinctUntilChanged, filter, share } from 'rxjs/operators';
import {
Router,
RouterEvent,
NavigationStart,
ActivationStart,
ActivatedRoute,
} from '@angular/router';
import {
map,
distinctUntilChanged,
filter,
switchMap,
tap,
} from 'rxjs/operators';
import { isNullOrUndefined } from 'util';
@Injectable({ providedIn: 'root' })
@@ -19,4 +31,12 @@ export class LocationService {
get url() {
return this.router.url;
}
get url$() {
return this.router.events.pipe(
map((routerEvent: RouterEvent) => routerEvent.url),
filter((url) => !isNullOrUndefined(url)),
distinctUntilChanged()
);
}
}

View File

@@ -1,3 +1,6 @@
@import 'variables';
@import 'mixins/media';
:host {
display: block;
}

View File

@@ -0,0 +1 @@
export type FilterType = 'select' | 'text' | 'date' | 'number' | 'checkbox';

View File

@@ -4,3 +4,4 @@ export * from './filter-base';
export * from './select-filter';
export * from './select-option';
export * from './filter';
export * from './filter-type';

View File

@@ -11,4 +11,6 @@
top: 61px;
left: 0;
right: 0;
z-index: 2;
}

View File

@@ -9,7 +9,7 @@ import {
Output,
EventEmitter,
} from '@angular/core';
import { OrderItemListItemDTO } from '@swagger/oms/lib';
import { AutocompleteDTO } from '@swagger/oms/lib';
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
import { ResultItemComponent } from './result-item';
@@ -22,9 +22,9 @@ import { ResultItemComponent } from './result-item';
export class AutocompleteResultsComponent implements AfterViewInit {
@ViewChildren(ResultItemComponent) items: QueryList<ResultItemComponent>;
@Input() results: OrderItemListItemDTO[];
@Input() results: AutocompleteDTO[];
@Output() selectItem = new EventEmitter<
OrderItemListItemDTO & { shouldNavigate?: boolean }
AutocompleteDTO & { shouldNavigate?: boolean }
>();
private keyManager: ActiveDescendantKeyManager<ResultItemComponent>;
@@ -44,8 +44,7 @@ export class AutocompleteResultsComponent implements AfterViewInit {
this.keyManager = new ActiveDescendantKeyManager(this.items).withWrap();
}
onItemClicked(item: OrderItemListItemDTO) {
console.log({ item });
onItemClicked(item: AutocompleteDTO) {
this.keyManager.setActiveItem(this.getItemIndex(item));
this.selectItem.emit({
...this.keyManager.activeItem.result,
@@ -53,7 +52,7 @@ export class AutocompleteResultsComponent implements AfterViewInit {
});
}
private getItemIndex(item: OrderItemListItemDTO): number {
private getItemIndex(item: AutocompleteDTO): number {
return this.results.indexOf(item);
}
}

View File

@@ -1,3 +1,3 @@
<div class="item isa-font-lightgrey" (click)="clicked.emit(result)">
{{ result.firstName }} {{ result.lastName }}
{{ result.display }}
</div>

View File

@@ -6,7 +6,7 @@ import {
Output,
EventEmitter,
} from '@angular/core';
import { OrderItemListItemDTO } from '@swagger/oms/lib';
import { AutocompleteDTO } from '@swagger/oms/lib';
import { Highlightable } from '@angular/cdk/a11y';
@Component({
@@ -16,8 +16,8 @@ import { Highlightable } from '@angular/cdk/a11y';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ResultItemComponent implements Highlightable {
@Input() result: OrderItemListItemDTO;
@Output() clicked = new EventEmitter<OrderItemListItemDTO>();
@Input() result: AutocompleteDTO;
@Output() clicked = new EventEmitter<AutocompleteDTO>();
private _isActive: boolean;

View File

@@ -106,6 +106,12 @@ export class ShelfSearchbarComponent implements OnInit, OnDestroy {
}
}
setInputValue(queryString: string) {
if (this.searchForm) {
this.searchForm.setValue(queryString);
}
}
private validateSearchInputBeforeSubmit(searchInput: string): boolean {
return !isNullOrUndefined(searchInput) && searchInput.length >= 1;
}

View File

@@ -1,6 +1,6 @@
export interface ShelfPrimaryFilterOptions {
allBranches: boolean;
customerName: boolean;
author: boolean;
title: boolean;
export interface PrimaryFilterOption {
name: string;
id: string;
key: string;
selected: boolean;
}

View File

@@ -1,34 +1,11 @@
<div class="container mb-40" *ngIf="primaryFilters$ | async as primaryFilters">
<button
*ngFor="let filter of primaryFilters"
class="isa-chip"
id="allBranches"
[id]="filter.id || filter.key"
(click)="handleClick($event.target)"
[class.selected]="primaryFilters['allBranches']"
[class.selected]="filter.selected"
>
Alle Filialen
</button>
<button
class="isa-chip"
id="customerName"
(click)="handleClick($event.target)"
[class.selected]="primaryFilters['customerName']"
>
Kundenname
</button>
<button
class="isa-chip"
id="author"
(click)="handleClick($event.target)"
[class.selected]="primaryFilters['author']"
>
Autor
</button>
<button
class="isa-chip"
id="title"
(click)="handleClick($event.target)"
[class.selected]="primaryFilters['title']"
>
Titel
{{ filter.name }}
</button>
</div>

View File

@@ -1,8 +1,14 @@
import { Component, OnInit, ChangeDetectionStrategy, Output, EventEmitter } from '@angular/core';
import { ShelfPrimaryFilterOptions } from '../../../defs';
import {
Component,
OnInit,
ChangeDetectionStrategy,
Output,
EventEmitter,
} from '@angular/core';
import { Observable } from 'rxjs';
import { SearchStateFacade } from 'apps/sales/src/app/store/customer';
import { take } from 'rxjs/operators';
import { PrimaryFilterOption } from '../../../defs';
@Component({
selector: 'app-shelf-primary-filters',
@@ -11,7 +17,7 @@ import { take } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShelfPrimaryFiltersComponent implements OnInit {
primaryFilters$: Observable<ShelfPrimaryFilterOptions>;
primaryFilters$: Observable<PrimaryFilterOption[]>;
@Output() change = new EventEmitter<void>();
@@ -32,14 +38,23 @@ export class ShelfPrimaryFiltersComponent implements OnInit {
private handleUpdate(params: { identifier: string }) {
this.primaryFilters$.pipe(take(1)).subscribe((currentFilters) => {
const updatedValue = !currentFilters[params.identifier];
const filterToUpdate = currentFilters.find(
(filter) => filter.id === params.identifier
);
this.updateSelectedFilters({ [params.identifier]: updatedValue });
if (!filterToUpdate) {
return;
}
this.updateSelectedFilters({
...filterToUpdate,
selected: !filterToUpdate.selected,
});
this.change.emit();
});
}
private updateSelectedFilters(changes: Partial<ShelfPrimaryFilterOptions>) {
private updateSelectedFilters(changes: PrimaryFilterOption) {
this.searchStateFacade.setPrimaryFilters(changes);
}

View File

@@ -11,7 +11,9 @@
<app-shelf-primary-filters
(change)="setSearchFocus()"
></app-shelf-primary-filters>
<app-shelf-search-input></app-shelf-search-input>
<app-shelf-search-input
[searchCallback]="close.bind(this)"
></app-shelf-search-input>
<div class="d-flex flex-column mt-40">
<ng-container *ngIf="pendingFilters$ | async as filters">

View File

@@ -10,6 +10,12 @@
overflow: scroll;
}
app-select-filter {
@include mq-desktop() {
position: relative;
}
}
app-selected-filter-options {
margin-bottom: 30px;
}
@@ -17,6 +23,14 @@ app-selected-filter-options {
.apply-filter-wrapper {
text-align: center;
padding-bottom: 5px;
@include mq-desktop() {
position: absolute;
bottom: 25px;
left: 0;
width: 100%;
margin: 0 auto;
}
}
.btn-apply-filters {

View File

@@ -13,9 +13,11 @@ import { slideIn } from 'apps/sales/src/app/core/overlay';
import { Observable, BehaviorSubject, interval } from 'rxjs';
import { Filter, SelectFilter } from '../../../filter';
import { ShelfFilterService } from '../../services/shelf-filter.service';
import { startWith } from 'rxjs/operators';
import { startWith, first } from 'rxjs/operators';
import { isNullOrUndefined } from 'util';
import { ShelfSearchInputComponent } from '../../pages/shelf-search/search';
import { cloneFilter } from '../../../filter/utils';
import { SearchStateFacade } from '@shelf-store';
@Component({
selector: 'app-shelf-filter',
@@ -34,7 +36,7 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
searchInput: ShelfSearchInputComponent;
checkHeightTimerSub = interval(100)
.pipe(startWith(0))
.pipe(startWith(true))
.subscribe(() => this.updateHeight());
filtersByGroup$: Observable<Filter[]>;
@@ -45,6 +47,7 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
constructor(
private overlayRef: IsaOverlayRef,
private filterService: ShelfFilterService,
private searchStateFacade: SearchStateFacade,
private renderer: Renderer2,
private cdr: ChangeDetectorRef
) {}
@@ -75,7 +78,7 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
}
onFiltersChange(updatedFilters: SelectFilter[]) {
this.pendingFilters$.next(updatedFilters);
this.pendingFilters$.next(updatedFilters.map(cloneFilter));
this.setSearchFocus();
}
@@ -93,7 +96,7 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
if (this.pendingFilters$.value) {
this.filterService.updateFilters(this.pendingFilters$.value);
}
this.close();
this.initiateSearchAndCloseOverlay();
}
close() {
@@ -142,4 +145,26 @@ export class ShelfFilterComponent implements OnInit, OnDestroy {
}
}
}
private initiateSearchAndCloseOverlay() {
if (this.searchInput) {
const searchbarValue =
(this.searchInput &&
this.searchInput.searchbar &&
this.searchInput.searchbar.searchForm.value) ||
'';
this.searchStateFacade.selectedFilter$
.pipe(first())
.subscribe((selectedFilters) =>
this.searchInput.triggerSearch({
type: 'search',
value: searchbarValue,
selectedFilters,
bypassValidation: true,
closeOverlay: () => this.close(),
})
);
}
}
}

View File

@@ -5,20 +5,17 @@ import {
Input,
OnDestroy,
ViewChild,
QueryList,
ViewChildren,
AfterViewInit,
} from '@angular/core';
import { Subject, BehaviorSubject, Observable, of } from 'rxjs';
import {
ShelfSearchFacadeService,
ShelfStoreFacadeService,
ShelfNavigationService,
} from '../../../shared/services';
import {
OrderItemListItemDTO,
ListResponseArgsOfOrderItemListItemDTO,
} from '@swagger/oms/lib';
AutocompleteDTO,
} from '@swagger/oms';
import {
distinctUntilChanged,
debounceTime,
@@ -27,11 +24,14 @@ import {
takeUntil,
filter,
first,
shareReplay,
tap,
withLatestFrom,
} from 'rxjs/operators';
import { isNullOrUndefined } from 'util';
import { SHELF_SCROLL_INDEX } from 'apps/sales/src/app/core/utils/app.constants';
import { ShelfSearchbarComponent } from '../../../components';
import { ShelfFilterService } from '../../../services/shelf-filter.service';
import { SearchStateFacade } from '@shelf-store';
@Component({
selector: 'app-shelf-search-input',
@@ -43,6 +43,7 @@ export class ShelfSearchInputComponent
implements OnInit, AfterViewInit, OnDestroy {
@Input() allowScan = false;
@Input() hasAutocomplete = true;
@Input() searchCallback: () => void;
@ViewChild(ShelfSearchbarComponent, { static: true })
searchbar: ShelfSearchbarComponent;
@@ -54,17 +55,17 @@ export class ShelfSearchInputComponent
errorMessage$ = new BehaviorSubject<string>('');
autocompleteQueryString$ = new BehaviorSubject<string>('');
autocompleteResult$: Observable<OrderItemListItemDTO[]>;
autocompleteResult$: Observable<AutocompleteDTO[]>;
searchMode$: Observable<'standalone' | 'autocomplete'>;
selectedItem$ = new BehaviorSubject<
OrderItemListItemDTO & { shouldNavigate?: boolean }
AutocompleteDTO & { shouldNavigate?: boolean }
>(null);
constructor(
private shelfSearchService: ShelfSearchFacadeService,
private shelfStoreFacade: ShelfStoreFacadeService,
private searchStateFacade: SearchStateFacade,
private shelfNavigationService: ShelfNavigationService,
private filterService: ShelfFilterService
) {}
@@ -75,7 +76,6 @@ export class ShelfSearchInputComponent
}
this.setUpSearchMode();
this.setUpNavigationToDetailsPage();
}
ngAfterViewInit() {
@@ -94,39 +94,66 @@ export class ShelfSearchInputComponent
}
triggerBarcodeSearch(barcode: string) {
this.triggerSearch({ type: 'scan', value: barcode, target: 'click' });
this.triggerSearch({ type: 'scan', value: barcode });
}
triggerSearch({
type,
value,
target,
}: {
type: 'search' | 'scan';
value: string;
target: 'click' | 'enter';
}) {
triggerSearch(
{
type,
value,
selectedFilters,
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.shouldNavigateToSelectedItem(target)) {
return this.shelfNavigationService.navigateToDetails(
this.selectedItem$.value
);
}
if (type === 'search') {
result$ = this.shelfSearchService.search(value);
if (this.isAutocompleteSelected()) {
searchQuery = this.selectedItem$.value.query;
result$ = this.shelfSearchService.search(searchQuery, {
hitsOnly: true,
bypassValidation,
selectedFilters,
});
this.updateSearchbarValue(searchQuery);
} else if (type === 'search') {
result$ = this.shelfSearchService.search(value, {
hitsOnly: true,
bypassValidation,
selectedFilters,
});
} else if (type === 'scan') {
result$ = this.shelfSearchService.searchWithBarcode(value);
}
result$
.pipe(takeUntil(this.destroy$), first())
.pipe(
takeUntil(this.destroy$),
first(),
tap(() => this.searchStateFacade.setInput(searchQuery))
)
.subscribe(
(result) => this.handleSearchResult(result, value),
this.handleSearchResultError
(result) =>
this.handleSearchResult(
result,
searchQuery,
closeOverlay || this.searchCallback
),
() => this.handleSearchResultError()
);
}
@@ -136,13 +163,13 @@ export class ShelfSearchInputComponent
private handleSearchResult(
result: ListResponseArgsOfOrderItemListItemDTO,
value: string
value: string,
successCB?: () => void
) {
this.setIsFetchingData(false);
if (this.hasNoResult(result)) {
this.setErrorMessage('Ergibt keine Suchergebnisse');
return this.setErrorMessage('Ergibt keine Suchergebnisse');
} else if (this.hasMultipleSearchResults(result)) {
this.shelfStoreFacade.setShelfSearch(value);
this.shelfNavigationService.navigateToResultList({
searchQuery: value,
numberOfHits: result.hits,
@@ -151,6 +178,10 @@ export class ShelfSearchInputComponent
} else {
this.shelfNavigationService.navigateToDetails(this.getDetails(result));
}
if (successCB) {
successCB();
}
}
private hasNoResult(result: ListResponseArgsOfOrderItemListItemDTO): boolean {
@@ -167,24 +198,33 @@ export class ShelfSearchInputComponent
sessionStorage.removeItem(key);
}
private handleSearchResultError() {
private handleSearchResultError(errorCB?: () => void) {
this.setErrorMessage('Ein Fehler ist aufgetreten');
this.setIsFetchingData(false);
if (errorCB) {
errorCB();
}
}
private setupAutocompletion() {
this.autocompleteResult$ = this.autocompleteQueryString$.pipe(
distinctUntilChanged(),
debounceTime(250),
switchMap((queryString: string) => {
filter((queryString) => typeof queryString === 'string'),
distinctUntilChanged(this.filterDistinctStrings),
withLatestFrom(this.searchStateFacade.selectedFilter$),
switchMap(([queryString, selectedFilters]) => {
if (this.isValidQuery(queryString)) {
return this.shelfSearchService
.searchForAutocomplete(queryString)
.pipe(map((result) => result.result && result.result.slice(0, 5)));
.searchForAutocomplete(queryString, {
selectedFilters: selectedFilters,
})
.pipe(map((result) => result.result));
}
return of([]);
})
}),
shareReplay()
);
}
@@ -212,18 +252,6 @@ export class ShelfSearchInputComponent
}
}
private setUpNavigationToDetailsPage() {
this.selectedItem$
.pipe(
takeUntil(this.destroy$),
filter((item) => !isNullOrUndefined(item)),
distinctUntilChanged(),
filter(({ shouldNavigate }) => !!shouldNavigate),
map(({ shouldNavigate, ...item }) => item)
)
.subscribe((item) => this.shelfNavigationService.navigateToDetails(item));
}
private resetQueryString() {
this.autocompleteQueryString$.next('');
}
@@ -240,11 +268,21 @@ export class ShelfSearchInputComponent
this.errorMessage$.next(message);
}
private isValidQuery(queryString): boolean {
return !!queryString && queryString.length > 3;
private isValidQuery(queryString: string): boolean {
return !!queryString && queryString.length >= 3;
}
private shouldNavigateToSelectedItem(target: 'click' | 'enter'): boolean {
return !!this.selectedItem$.value && target === 'enter';
private filterDistinctStrings(oldQuery: string, newQuery: string): boolean {
return (oldQuery || '').toLowerCase() === (newQuery || '').toLowerCase();
}
private isAutocompleteSelected() {
return !!this.selectedItem$.value;
}
private updateSearchbarValue(queryString: string) {
if (this.searchbar) {
this.searchbar.setInputValue(queryString);
}
}
}

View File

@@ -1,11 +1,11 @@
import { Injectable, OnDestroy } from '@angular/core';
import { Observable, BehaviorSubject, of, Subject, combineLatest } from 'rxjs';
import { Observable, BehaviorSubject, Subject, combineLatest } from 'rxjs';
import { SelectFilter, Filter, SelectFilterOption } from '../../filter';
import { mockFilters } from '../shared/mockdata/filters.mock';
import { tap, switchMap, startWith, map, withLatestFrom, debounceTime, filter, first } from 'rxjs/operators';
import { tap, startWith, map, filter } from 'rxjs/operators';
import { SearchStateFacade } from '../../../store/customer';
import { flatten } from '../../../shared/utils';
import { isNullOrUndefined } from 'util';
import { cloneFilter } from '../../filter/utils';
@Injectable({ providedIn: 'root' })
export class ShelfFilterService implements OnDestroy {
@@ -18,12 +18,14 @@ export class ShelfFilterService implements OnDestroy {
public overlayClosed$ = new Subject<void>();
constructor(private searchStateFacade: SearchStateFacade) {
this.filters$ = this.searchStateFacade.currentFilter$;
this.filters$ = this.searchStateFacade.selectFilters$;
this.initPendingFilters();
}
get hasSelectedFilters$(): Observable<boolean> {
return this.searchStateFacade.currentFilter$.pipe(map(this.computeHasSelectedFilters));
return this.searchStateFacade.selectFilters$.pipe(
map(this.computeHasSelectedFilters)
);
}
get hasSelectedPendingFilters$(): Observable<boolean> {
@@ -43,7 +45,7 @@ export class ShelfFilterService implements OnDestroy {
tap(() => this.pendingFilters$.next(null))
),
]).subscribe(([filters]) => {
this.pendingFilters$.next(filters);
this.pendingFilters$.next(filters.map(cloneFilter));
this.setInitialFilterGroupLastChanged(filters);
});
}
@@ -64,7 +66,10 @@ export class ShelfFilterService implements OnDestroy {
}
private computeHasSelectedFilters(filters: SelectFilter[]): boolean {
const flattenedFilters = (flatten(filters, 'options') as unknown) as SelectFilterOption[];
const flattenedFilters = (flatten(
filters,
'options'
) as unknown) as SelectFilterOption[];
if (flattenedFilters.some((f) => !!f.selected)) {
return true;

View File

@@ -0,0 +1,5 @@
// start:ng42.barrel
export * from './filters.mock';
export * from './primary-filters.mock';
// end:ng42.barrel

View File

@@ -0,0 +1,16 @@
import { PrimaryFilterOption } from '../../defs';
export const primaryFiltersMock: PrimaryFilterOption[] = [
{
name: 'Primary Filter One',
id: 'filter1',
key: 'One',
selected: false,
},
{
name: 'Primary Filter Two',
id: 'filter2',
key: 'Two',
selected: false,
},
];

View File

@@ -1,6 +1,4 @@
// start:ng42.barrel
export * from './shelf-navigation.service';
export * from './shelf-search.facade.service';
export * from './shelf-store.facade.service';
// end:ng42.barrel

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Store, Select } from '@ngxs/store';
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';
@@ -19,14 +19,29 @@ export class ShelfSearchFacadeService {
constructor(private collectingShelfService: CollectingShelfService) {}
search(queryString: string) {
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;
if (!this.isValidSearchQuery(searchParams)) {
if (!bypassValidation && !this.isValidSearchQuery(searchParams)) {
return;
}
return this.requestSearch(searchParams).pipe(map(this.handleSearchResult));
return this.requestSearch(searchParams, {
hitsOnly,
selectedFilters,
}).pipe(map(this.handleSearchResult));
}
searchWithBarcode(barcode: string) {
@@ -39,10 +54,13 @@ export class ShelfSearchFacadeService {
return this.requestSearch(searchParams).pipe(map(this.handleSearchResult));
}
searchForAutocomplete(queryString: string) {
searchForAutocomplete(
queryString: string,
options: { selectedFilters?: { [key: string]: string } } = {}
) {
const searchParams = queryString.trim();
const autoCompleteQuery: AutocompleteTokenDTO = this.generateAutocompleteToken(
{ queryString, filter: {}, take: 5 }
{ queryString, filter: options.selectedFilters || {}, take: 5 }
);
if (!this.isValidSearchQuery(searchParams)) {
@@ -53,12 +71,21 @@ export class ShelfSearchFacadeService {
}
private requestSearch(
input: string
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 })
this.collectingShelfService.searchWarenausgabe({
input,
branchNumber,
selectedFilters: options.selectedFilters,
hitsOnly: options.hitsOnly,
})
)
);
}

View File

@@ -1,31 +0,0 @@
import { Injectable } from '@angular/core';
import { Store as NgxsStore } from '@ngxs/store';
import { SetShelfSearch } from 'apps/sales/src/app/core/store/actions/process.actions';
import { ShelfSearch } from 'apps/sales/src/app/core/models/shelf-search.modal';
import { BranchSelectors } from 'apps/sales/src/app/core/store/selectors/branch.selector';
import { SearchStateFacade } from 'apps/sales/src/app/store/customer';
@Injectable({ providedIn: 'root' })
export class ShelfStoreFacadeService {
constructor(
private ngxsStore: NgxsStore,
private storeFacade: SearchStateFacade
) {}
get branchnumber(): string {
return this.ngxsStore.selectSnapshot(BranchSelectors.getUserBranch);
}
setShelfSearch(input: string) {
/** NGXXS Store */
this.ngxsStore.dispatch(
new SetShelfSearch(<ShelfSearch>{
input,
branchnumber: this.branchnumber,
})
);
/** NEW NgRx Store */
this.storeFacade.setInput(input);
}
}

View File

@@ -1,13 +1,13 @@
import { OrderItemListItemDTO } from '@swagger/oms';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
export interface SearchProcess {
id: number; // Prozess ID;
input?: string;
filters?: {
selectedFilters?: SelectFilter[];
primaryFilters?: ShelfPrimaryFilterOptions;
primaryFilters?: PrimaryFilterOption[];
};
result: OrderItemListItemDTO[];
hits?: number;

View File

@@ -1,2 +1,8 @@
// start:ng42.barrel
export * from './grouped-by-customer.mapper';
export * from './grouped-by-order.mapper';
export * from './input-dto-to-primary-filters.mapper';
export * from './input-dto-to-selected-filters.mapper';
export * from './primary-filters-to-filters-dictionary.mapper';
export * from './select-filters-to-filters-dictionary.mapper';
// end:ng42.barrel

View File

@@ -0,0 +1,13 @@
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
import { InputDTO } from '@swagger/oms';
export function inputDtoToPrimaryFilterOption(
inputDTO: InputDTO
): PrimaryFilterOption {
return {
id: inputDTO.key,
key: inputDTO.key,
name: inputDTO.label,
selected: false,
};
}

View File

@@ -0,0 +1,53 @@
import {
SelectFilter,
SelectFilterOption,
FilterType,
} from 'apps/sales/src/app/modules/filter';
import { InputDTO, OptionDTO, InputType } from '@swagger/oms';
export function inputDtoToSelectedFilters(inputDTO: InputDTO): SelectFilter {
return {
key: inputDTO.key,
name: inputDTO.label,
max: inputDTO.options && inputDTO.options.max,
type: inputTypeToType(inputDTO.type),
options:
inputDTO.options &&
inputDTO.options.values.map(optionDtoToSelectFilterOption),
} as SelectFilter;
}
export function optionDtoToSelectFilterOption(
optionDto: OptionDTO
): SelectFilterOption {
return {
name: optionDto.label,
id: optionDto.key || optionDto.value,
selected: optionDto.selected || false,
expanded: false,
options:
optionDto.values && optionDto.values.map(optionDtoToSelectFilterOption),
} as SelectFilterOption;
}
export function inputTypeToType(type: InputType): FilterType {
switch (type) {
case 1:
return 'text';
case 2:
return 'select';
case 4:
return 'checkbox';
case 8 || 16:
return 'date';
case 32 || 64:
return 'number';
default:
return 'select';
}
}

View File

@@ -0,0 +1,17 @@
import { Dictionary } from '@cmf/core';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
export function primaryFiltersToFiltersDictionary(
primaryFilters: PrimaryFilterOption[]
): { [key: string]: string[] } {
if (!primaryFilters) {
return {};
}
return primaryFilters.reduce((acc, curr) => {
if (!curr.selected) {
return acc;
}
return { ...acc, [curr.key]: String(curr.selected) };
}, {});
}

View File

@@ -0,0 +1,21 @@
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { flatten } from 'apps/sales/src/app/shared/utils';
export function selectFiltersToFiltersDictionary(
selectFilters: SelectFilter[]
): { [key: string]: string } {
if (!selectFilters) {
return {};
}
const result = selectFilters
.map((selectFilter) => {
const flattened = flatten(selectFilter.options, 'options').filter(
(o) => !!o.selected
);
return { [selectFilter.key]: flattened.map((f) => f.id).join(';') };
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {});
return result;
}

View File

@@ -3,9 +3,10 @@ import {
OrderItemListItemDTO,
StrictHttpResponse,
ListResponseArgsOfOrderItemListItemDTO,
ResponseArgsOfIEnumerableOfInputDTO,
} from '@swagger/oms';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
const prefix = '[CUSTOMER] [SHELF] [SEARCH]';
@@ -28,18 +29,18 @@ export const fetchFiltersDone = createAction(
`${prefix} Fetch Filters Done`,
props<{
id: number;
response: SelectFilter[];
response: StrictHttpResponse<ResponseArgsOfIEnumerableOfInputDTO>;
}>()
);
export const setSelectedFilters = createAction(
export const setSelectFilters = createAction(
`${prefix} Set Selected Filters`,
props<{ id: number; filters: SelectFilter[] }>()
);
export const setPrimaryFilters = createAction(
`${prefix} Set Primary Filters`,
props<{ id: number; filters: ShelfPrimaryFilterOptions }>()
props<{ id: number; filters: PrimaryFilterOption[] }>()
);
export const setInput = createAction(

View File

@@ -0,0 +1,200 @@
import { TestBed } from '@angular/core/testing';
import { SearchEffects } from './search.effects';
import { provideMockStore, MockStore } from '@ngrx/store/testing';
import { provideMockActions } from '@ngrx/effects/testing';
import { Observable, Subject } from 'rxjs';
import {
StrictHttpResponse,
ResponseArgsOfIEnumerableOfInputDTO,
OrderService,
InputDTO,
} from '@swagger/oms';
import { hot, cold } from 'jasmine-marbles';
import * as actions from './search.actions';
import * as processActions from 'apps/sales/src/app/core/store/actions/process.actions';
import { Store as NgxsStore, ofActionDispatched } from '@ngxs/store';
import { Store } from '@ngrx/store';
import { Actions, NgxsModule } from '@ngxs/store';
import { BranchService } from '@sales/core-services';
import { SearchStateFacade } from './search.facade';
import {
inputDtoToSelectedFilters,
inputDtoToPrimaryFilterOption,
} from './mappers';
import { Process } from 'apps/sales/src/app/core/models/process.model';
import { first } from 'rxjs/operators';
import { ProcessState } from 'apps/sales/src/app/core/store/state/process.state';
import { HttpClientTestingModule } from '@angular/common/http/testing';
fdescribe('#SearchEffects', () => {
let orderService: jasmine.SpyObj<OrderService>;
let searchEffects: SearchEffects;
let actions$: Observable<any>;
let store: MockStore<any>;
let ngxsStore: NgxsStore;
let ngxsActions$: Observable<any>;
let searchStateFacade: SearchStateFacade;
// Test Data
const id = 123;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
HttpClientTestingModule,
NgxsModule.forRoot([]),
NgxsModule.forFeature([ProcessState]),
],
providers: [
SearchEffects,
provideMockStore({}),
provideMockActions(() => actions$),
{
provide: OrderService,
useValue: jasmine.createSpyObj('orderService', [
'OrderGetWarenausgabeFilterResponse',
]),
},
{
provide: BranchService,
useValue: jasmine.createSpy('branchService'),
},
{
provide: SearchStateFacade,
useValue: jasmine.createSpy('searchStateFacade'),
},
Actions,
SearchStateFacade,
],
});
searchEffects = TestBed.get(SearchEffects);
orderService = TestBed.get(OrderService);
store = TestBed.get(Store);
ngxsStore = TestBed.get(NgxsStore);
ngxsActions$ = TestBed.get(Actions);
searchStateFacade = TestBed.get(SearchStateFacade);
});
it('should be created', () => {
expect(searchEffects).toBeTruthy();
});
describe('#FetchFilters', () => {
it('should fetch the filters and dispatch fetch filters done with filters http response', () => {
const mockHttpResponse = ({
ok: true,
body: [],
} as unknown) as StrictHttpResponse<ResponseArgsOfIEnumerableOfInputDTO>;
const fetchFiltersAction = actions.fetchFilters({ id });
const fetchFiltersDoneAction = actions.fetchFiltersDone({
id,
response: mockHttpResponse,
});
const response = cold('-a', { a: mockHttpResponse });
orderService.OrderGetWarenausgabeFilterResponse.and.returnValue(response);
actions$ = hot('--a', { a: fetchFiltersAction });
const expected = cold('---b', { b: fetchFiltersDoneAction });
expect(searchEffects.fetchFilters$).toBeObservable(expected);
});
});
describe('#FetchFiltersDone', () => {
it('should dispatch setSelectedFiters and setPrimaryFilters if the filters http response is ok ', () => {
const mockHttpResponse = ({
ok: true,
body: {
result: [
{ key: 'Key Mock 1', label: 'Label Mock 1' },
{
key: 'Key Mock 2',
label: 'Label Mock 2',
options: {
max: 3,
values: [
{ key: 'Sub 1 Key', label: 'Sub 1 Label', selected: false },
{ key: 'Sub 2 Key', label: 'Sub 2 Label', selected: true },
],
},
},
],
},
} as unknown) as StrictHttpResponse<ResponseArgsOfIEnumerableOfInputDTO>;
const fetchFiltersDoneAction = actions.fetchFiltersDone({
id,
response: mockHttpResponse,
});
const selectFilters = [
{
key: 'Key Mock 2',
label: 'Label Mock 2',
options: {
max: 3,
values: [
{ key: 'Sub 1 Key', label: 'Sub 1 Label', selected: false },
{ key: 'Sub 2 Key', label: 'Sub 2 Label', selected: true },
],
},
} as InputDTO,
].map((input) => inputDtoToSelectedFilters(input));
const primaryFilters = [
{ key: 'Key Mock 1', label: 'Label Mock 1' } as InputDTO,
].map((input) => inputDtoToPrimaryFilterOption(input));
const setSelectedFiltersAction = actions.setSelectFilters({
id,
filters: selectFilters,
});
const setPrimaryFiltersAction = actions.setPrimaryFilters({
id,
filters: primaryFilters,
});
actions$ = hot('-a', { a: fetchFiltersDoneAction });
const expected = cold('-(bc)', {
b: setSelectedFiltersAction,
c: setPrimaryFiltersAction,
});
expect(searchEffects.fetchFiltersDone$).toBeObservable(expected);
});
});
describe('#InitAddProcess', () => {
beforeEach(() => {
searchStateFacade = TestBed.get(SearchStateFacade);
});
it('should dispatch addSearchProcess and fetch filters', async () => {
spyOn(ngxsStore, 'dispatch').and.callThrough();
spyOn(store, 'dispatch').and.callThrough();
spyOn(searchStateFacade, 'fetchFilters').and.callFake(() => null);
const addProcessAction = new processActions.AddProcess({
id,
} as Process);
const addSearchProcessAction = actions.addSearchProcess({ id });
searchEffects.initAddProcess();
await ngxsStore
.dispatch(
new processActions.AddProcess({
id,
} as Process)
)
.pipe(first())
.toPromise();
expect(ngxsStore.dispatch).toHaveBeenCalledWith(addProcessAction);
expect(store.dispatch).toHaveBeenCalledWith(addSearchProcessAction);
expect(searchStateFacade.fetchFilters).toHaveBeenCalledWith(id);
});
});
});

View File

@@ -18,11 +18,18 @@ import {
OrderService,
StrictHttpResponse,
ListResponseArgsOfOrderItemListItemDTO,
ResponseArgsOfIEnumerableOfInputDTO,
} from '@swagger/oms';
import { BranchService } from '@sales/core-services';
import { SearchStateFacade } from './search.facade';
import { of, NEVER } from 'rxjs';
import { mockFilters } from 'apps/sales/src/app/modules/shelf/shared/mockdata/filters.mock';
import {
inputDtoToSelectedFilters,
inputDtoToPrimaryFilterOption,
} from './mappers';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
import { isNullOrUndefined } from 'util';
@Injectable()
export class SearchEffects {
@@ -43,14 +50,20 @@ export class SearchEffects {
ofType(actions.fetchResult),
switchMap((a) =>
of(a).pipe(
withLatestFrom(this.searchStateFacade.getProcess$(a.id)),
flatMap(([_, process]) =>
withLatestFrom(
this.searchStateFacade.getProcess$(a.id),
this.searchStateFacade.selectedFilter$
),
flatMap(([_, process, filter]) =>
this.orderService
.OrderQueryOrderItemResponse({
branchNumber: this.branchService.getCurrentBranchNumber(),
input: {
qs: process.input,
},
filter: (filter as unknown) as {
[key: string]: string;
},
skip: process.result.length || 0,
take: 20,
})
@@ -88,10 +101,16 @@ export class SearchEffects {
fetchFilters$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.addSearchProcess),
map((action) =>
// Placeholder for getting values from Backend
actions.fetchFiltersDone({ id: action.id, response: mockFilters })
ofType(actions.fetchFilters),
flatMap((action) =>
this.orderService.OrderGetWarenausgabeFilterResponse().pipe(
catchError((err) =>
of<StrictHttpResponse<ResponseArgsOfIEnumerableOfInputDTO>>(err)
),
map((response) =>
actions.fetchFiltersDone({ id: action.id, response })
)
)
)
)
);
@@ -99,9 +118,39 @@ export class SearchEffects {
fetchFiltersDone$ = createEffect(() =>
this.actions$.pipe(
ofType(actions.fetchFiltersDone),
map((action) =>
actions.setSelectedFilters({ id: action.id, filters: action.response })
)
flatMap((action) => {
if (action.response.ok) {
const result = action.response.body;
const selectedFiltersRaw = result.result.filter(
(filter) => !!filter.options
);
const primaryFiltersRaw = result.result.filter((filter) =>
isNullOrUndefined(filter.options)
);
const selectFilters: SelectFilter[] = selectedFiltersRaw.map(
(input) => inputDtoToSelectedFilters(input)
);
const primaryFilters: PrimaryFilterOption[] = primaryFiltersRaw.map(
(input) => inputDtoToPrimaryFilterOption(input)
);
return [
actions.setSelectFilters({
id: action.id,
filters: selectFilters,
}),
actions.setPrimaryFilters({
id: action.id,
filters: primaryFilters,
}),
];
}
return NEVER;
})
)
);
@@ -113,6 +162,8 @@ export class SearchEffects {
this.store.dispatch(
actions.addSearchProcess({ id: action.payload.id })
);
this.searchStateFacade.fetchFilters(action.payload.id);
}
});
}

View File

@@ -0,0 +1,120 @@
import { SearchStateFacade } from './search.facade';
import { TestBed } from '@angular/core/testing';
import { Store, StoreModule } from '@ngrx/store';
import { Store as NgxsStore } from '@ngxs/store';
import { primaryFiltersMock } from 'apps/sales/src/app/modules/shelf/shared/mockdata';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
import { of, Observable } from 'rxjs';
import * as actions from './search.actions';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { first } from 'rxjs/operators';
fdescribe('SearchFacade', () => {
let facade: SearchStateFacade;
let store: Store<any>;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [StoreModule.forRoot({})],
providers: [Store, { provide: NgxsStore, useValue: NgxsStore }],
});
});
beforeEach(() => {
facade = TestBed.get(SearchStateFacade);
store = TestBed.get(Store);
});
it('should be created', () => {
expect(facade).toBeTruthy();
});
describe('setPrimaryFilters', () => {
const primaryFilterOption: PrimaryFilterOption = {
...primaryFiltersMock[0],
selected: true,
};
const id = 123;
it('should dispatch setPrimary in Store', async () => {
spyOn(store, 'dispatch').and.callThrough();
spyOn(facade, 'setPrimaryFilters').and.callThrough();
spyOn(facade, 'getPrimaryFilters').and.returnValue(
of(primaryFiltersMock)
);
await facade.setPrimaryFilters(primaryFilterOption, id);
expect(store.dispatch).toHaveBeenCalled();
});
it('should keep the original order of filter when dispatching setPrimary in Store', async () => {
spyOn(store, 'dispatch').and.callThrough();
spyOn(facade, 'setPrimaryFilters').and.callThrough();
spyOn(facade, 'getPrimaryFilters').and.returnValue(
of(primaryFiltersMock)
);
await facade.setPrimaryFilters(primaryFilterOption, id);
const updatedFilters = [primaryFilterOption, primaryFiltersMock[1]];
expect(store.dispatch).toHaveBeenCalledWith(
actions.setPrimaryFilters({ filters: updatedFilters, id })
);
});
});
describe('selectedFilter$', () => {
it('should return all selected filters from store', async () => {
const currentFilters: Observable<SelectFilter[]> = of([
{
type: 'select',
key: 'key1',
name: 'Filter Name 1',
options: [
{
key: 'Option 1',
id: 'Option 1',
name: 'Option 1',
selected: true,
},
{
key: 'Option 2',
id: 'Option 2',
name: 'Option 2',
selected: false,
},
],
},
]);
const primaryFilters: Observable<PrimaryFilterOption[]> = of([
{
id: 'primary1',
key: 'primary1',
name: 'Primary Filter 1',
selected: true,
},
{
id: 'primary2',
key: 'primary2',
name: 'Primary Filter 2',
selected: false,
},
]);
spyOnProperty(facade, 'selectFilters$', 'get').and.returnValue(
currentFilters
);
spyOnProperty(facade, 'primaryFilters$', 'get').and.returnValue(
primaryFilters
);
const result = await facade.selectedFilter$.pipe(first()).toPromise();
expect(result).toEqual({
key1: 'Option 1',
primary1: 'true',
});
});
});
});

View File

@@ -5,12 +5,17 @@ import * as actions from './search.actions';
import * as selectors from './search.selectors';
import { switchMap, filter, map, first } from 'rxjs/operators';
import { SharedSelectors } from 'apps/sales/src/app/core/store/selectors/shared.selectors';
import { Observable } from 'rxjs';
import { Observable, combineLatest, of } from 'rxjs';
import { GroupedByCustomer } from './defs';
import { addOrCreateCustomerGroup } from './mappers';
import {
addOrCreateCustomerGroup,
selectFiltersToFiltersDictionary,
primaryFiltersToFiltersDictionary,
} from './mappers';
import { isNullOrUndefined } from 'util';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
import { Dictionary } from '@cmf/core';
@Injectable({ providedIn: 'root' })
export class SearchStateFacade {
@@ -30,7 +35,9 @@ export class SearchStateFacade {
}
get resultGroupedByCustomer$() {
return this.getProcessId$().pipe(switchMap((id) => this.getResultGroupedByCustomer$(id)));
return this.getProcessId$().pipe(
switchMap((id) => this.getResultGroupedByCustomer$(id))
);
}
get hits$() {
@@ -41,22 +48,55 @@ export class SearchStateFacade {
return this.getProcessId$().pipe(switchMap((id) => this.getFetching$(id)));
}
get primaryFilters$() {
return this.process$.pipe(switchMap((process) => this.getPrimaryFilters(process.id)));
get primaryFilters$(): Observable<PrimaryFilterOption[]> {
return this.process$.pipe(
switchMap((process) => this.getPrimaryFilters(process.id))
);
}
get currentFilter$(): Observable<SelectFilter[]> {
get selectFilters$(): Observable<SelectFilter[]> {
return this.process$.pipe(
filter((process) => !isNullOrUndefined(process) && !isNullOrUndefined(process.filters)),
filter(
(process) =>
!isNullOrUndefined(process) && !isNullOrUndefined(process.filters)
),
map((process) => process.filters.selectedFilters)
);
}
get selectedFilter$(): Observable<string[]> {
return this.currentFilter$.pipe(
map(() => {
// TODO extracting all selected filters possibly required for building search query
return [];
get hasActiveFilters$(): Observable<boolean> {
return this.selectedFilter$.pipe(
map((selectedFilters) => Object.values(selectedFilters)),
map((filterValues) =>
filterValues.reduce((acc, curr) => [...acc, curr], [])
),
map((flattenedFilters) => !!flattenedFilters.length)
);
}
get selectedFilter$(): Observable<{ [key: string]: string }> {
return combineLatest([this.selectFilters$, this.primaryFilters$]).pipe(
map(([selectFilters, primaryFilters]) => {
const selectFilterDictionary = selectFiltersToFiltersDictionary(
selectFilters
);
const primaryFiltersDictionary = primaryFiltersToFiltersDictionary(
primaryFilters
);
const mergedFilters = {
...selectFilterDictionary,
...primaryFiltersDictionary,
};
const selectedFilters = {};
for (const f in mergedFilters) {
if (!!mergedFilters[f]) {
selectedFilters[f] = mergedFilters[f];
}
}
return selectedFilters;
})
);
}
@@ -87,7 +127,9 @@ export class SearchStateFacade {
}
getResultGroupedByCustomer$(id: number): Observable<GroupedByCustomer[]> {
return this.getResult$(id).pipe(map((result) => result.reduce(addOrCreateCustomerGroup, [])));
return this.getResult$(id).pipe(
map((result) => result.reduce(addOrCreateCustomerGroup, []))
);
}
getHits$(id: number) {
@@ -98,7 +140,7 @@ export class SearchStateFacade {
return this.store.select(selectors.selectFetching, id);
}
getPrimaryFilters(id: number): Observable<ShelfPrimaryFilterOptions> {
getPrimaryFilters(id: number): Observable<PrimaryFilterOption[]> {
return this.store.select(selectors.selectPrimaryFilters, id);
}
@@ -114,30 +156,42 @@ export class SearchStateFacade {
async setSelectedFilters(filters: SelectFilter[], id?: number) {
if (id) {
return this.store.dispatch(actions.setSelectedFilters({ filters, id }));
return this.store.dispatch(actions.setSelectFilters({ filters, id }));
}
const processId = await this.getProcessId();
this.store.dispatch(actions.setSelectedFilters({ filters, id: processId }));
this.store.dispatch(actions.setSelectFilters({ filters, id: processId }));
}
async setPrimaryFilters(filters: Partial<ShelfPrimaryFilterOptions>, id?: number) {
let updatedFilters: ShelfPrimaryFilterOptions;
async setPrimaryFilters(primaryFilter: PrimaryFilterOption, id?: number) {
let updatedFilters: PrimaryFilterOption[];
let processId = id;
if (!id) {
processId = await this.getProcessId();
}
const currentPrimaryFilters = await this.getPrimaryFilters(processId).pipe(first()).toPromise();
const currentPrimaryFilters = await this.getPrimaryFilters(processId)
.pipe(first())
.toPromise();
updatedFilters = {
...currentPrimaryFilters,
...filters,
} as ShelfPrimaryFilterOptions;
const indexOfUpdatedFilter = currentPrimaryFilters.findIndex(
(f) => f.id === primaryFilter.id
);
return this.store.dispatch(actions.setPrimaryFilters({ filters: updatedFilters, id: processId }));
updatedFilters = [
...currentPrimaryFilters.slice(0, indexOfUpdatedFilter),
primaryFilter,
...currentPrimaryFilters.slice(
indexOfUpdatedFilter + 1,
currentPrimaryFilters.length + 1
),
];
return this.store.dispatch(
actions.setPrimaryFilters({ filters: updatedFilters, id: processId })
);
}
async fetchResult(id?: number) {
@@ -145,6 +199,7 @@ export class SearchStateFacade {
if (typeof processId !== 'number') {
processId = await this.getProcessId();
}
this.store.dispatch(actions.fetchResult({ id: processId }));
}
@@ -155,4 +210,14 @@ export class SearchStateFacade {
}
this.store.dispatch(actions.clearResults({ id: processId }));
}
async fetchFilters(id?: number) {
let processId = id;
if (typeof processId !== 'number') {
processId = await this.getProcessId();
}
this.store.dispatch(actions.fetchFilters({ id: processId }));
}
}

View File

@@ -0,0 +1,46 @@
import { SearchState, INITIAL_SEARCH_STATE } from './search.state';
import { TypedAction } from '@ngrx/store/src/models';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
import { primaryFiltersMock } from 'apps/sales/src/app/modules/shelf/shared/mockdata';
import * as actions from './search.actions';
import { searchReducer } from './search.reducer';
fdescribe('#SearchStateReducer', () => {
const id = 123;
let initialState: SearchState;
beforeEach(() => {
const action = actions.addSearchProcess({ id });
initialState = searchReducer(INITIAL_SEARCH_STATE, action);
});
describe('setPrimaryFilters', () => {
const filters = primaryFiltersMock;
let action: {
id: number;
filters: PrimaryFilterOption[];
} & TypedAction<string>;
beforeEach(() => {
action = actions.setPrimaryFilters({ id, filters });
});
it('should update the primary filters', () => {
const state = searchReducer(initialState, action);
const entity = state.entities[id];
expect(entity).toBeTruthy();
expect(entity.filters.primaryFilters).toEqual(primaryFiltersMock);
});
it('should keep the original order of primary filter items', () => {
const state = searchReducer(initialState, action);
const entity = state.entities[id];
expect(entity.filters.primaryFilters[0]).toEqual(primaryFiltersMock[0]);
expect(entity.filters.primaryFilters[1]).toEqual(primaryFiltersMock[1]);
});
});
});

View File

@@ -18,7 +18,7 @@ const _searchReducer = createReducer(
on(actions.setInput, (s, a) =>
searchStateAdapter.updateOne({ id: a.id, changes: { input: a.input } }, s)
),
on(actions.setSelectedFilters, (s, a) =>
on(actions.setSelectFilters, (s, a) =>
searchStateAdapter.updateOne(
{
id: a.id,

View File

@@ -4,28 +4,58 @@ import { searchStateAdapter } from './search.state';
import { Dictionary } from '@ngrx/entity';
import { SearchProcess } from './defs';
export const selectSearchState = createSelector(selectShelfState, (s) => s.search);
export const selectSearchState = createSelector(
selectShelfState,
(s) => s.search
);
export const { selectAll, selectEntities } = searchStateAdapter.getSelectors(selectSearchState);
export const { selectAll, selectEntities } = searchStateAdapter.getSelectors(
selectSearchState
);
export const selectProcess = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id]);
export const selectProcess = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) => entities[id]
);
export const selectInput = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].input);
export const selectInput = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].input
);
export const selectResult = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].result);
export const selectResult = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].result
);
export const selectHits = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].hits);
export const selectHits = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].hits
);
export const selectFetching = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].fetching);
export const selectFetching = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].fetching
);
export const selectFilters = createSelector(selectEntities, (entities: Dictionary<SearchProcess>, id: number) => entities[id].filters);
export const selectFilters = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].filters
);
export const selectSelectedFilters = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) => entities[id].filters.selectedFilters
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].filters.selectedFilters
);
export const selectPrimaryFilters = createSelector(
selectEntities,
(entities: Dictionary<SearchProcess>, id: number) => entities[id].filters.primaryFilters
(entities: Dictionary<SearchProcess>, id: number) =>
entities[id] && entities[id].filters.primaryFilters
);

View File

@@ -1,7 +1,7 @@
import { EntityState, createEntityAdapter } from '@ngrx/entity';
import { SearchProcess } from './defs';
import { SelectFilter } from 'apps/sales/src/app/modules/filter';
import { ShelfPrimaryFilterOptions } from 'apps/sales/src/app/modules/shelf/defs';
import { PrimaryFilterOption } from 'apps/sales/src/app/modules/shelf/defs';
export interface SearchState extends EntityState<SearchProcess> {}
@@ -11,19 +11,12 @@ export const INITIAL_SEARCH_STATE: SearchState = {
...searchStateAdapter.getInitialState(),
};
export const INITIAL_PRIMARY_FILTERS: ShelfPrimaryFilterOptions = {
allBranches: false,
customerName: false,
author: false,
title: false,
};
export const INITIAL_FILTERS: {
selectedFilters?: SelectFilter[];
primaryFilters?: ShelfPrimaryFilterOptions;
primaryFilters?: PrimaryFilterOption[];
} = {
selectedFilters: [],
primaryFilters: INITIAL_PRIMARY_FILTERS,
primaryFilters: [],
};
export const INITIAL_SEARCH_PROCESS: SearchProcess = {

View File

@@ -10,6 +10,11 @@
display: grid;
grid-template-rows: auto auto auto 1fr;
@include mq-desktop() {
position: inherit;
overflow: scroll;
}
.isa-filter-title {
text-align: center;
margin-top: 25px;

17
display-addressee-dto.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
import { Gender } from './dist/swagger/oms/lib/models/gender';
import { OrganisationDTO } from './dist/swagger/oms/lib/models/organisation-dto';
import { AddressDTO } from './dist/swagger/oms/lib/models/address-dto';
import { CommunicationDetailsDTO } from './dist/swagger/oms/lib/models/communication-details-dto';
import { ExternalReferenceDTO } from './dist/swagger/oms/lib/models/external-reference-dto';
export interface DisplayAddresseeDTO {
number?: string;
locale?: string;
gender: Gender;
title?: string;
firstName?: string;
lastName?: string;
organisation?: OrganisationDTO;
address?: AddressDTO;
communicationDetails?: CommunicationDetailsDTO;
externalReference?: ExternalReferenceDTO;
}

View File

@@ -4,6 +4,7 @@ import {
Output,
EventEmitter,
Input,
ChangeDetectorRef,
} from '@angular/core';
@Component({
@@ -13,7 +14,7 @@ import {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FilterButtonComponent {
@Input() active = false;
@Input() active = true;
@Input() module: 'Branch' | 'Customer' = 'Branch';
@Output() toggleFilter = new EventEmitter();

9
package-lock.json generated
View File

@@ -9137,6 +9137,15 @@
"integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=",
"dev": true
},
"jasmine-marbles": {
"version": "0.6.0",
"resolved": "https://pkgs.dev.azure.com/hugendubel/_packaging/hugendubel@Local/npm/registry/jasmine-marbles/-/jasmine-marbles-0.6.0.tgz",
"integrity": "sha1-943Bo7xFKXbeEO6LR8c9YWUyqVQ=",
"dev": true,
"requires": {
"lodash": "^4.5.0"
}
},
"jasmine-spec-reporter": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz",

View File

@@ -104,6 +104,7 @@
"@types/node": "~8.9.4",
"codelyzer": "~4.5.0",
"jasmine-core": "~2.99.1",
"jasmine-marbles": "^0.6.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "^4.0.1",
"karma-chrome-launcher": "~2.2.0",