Merged PR 1477: Branch Selector Changes

Branch Selector Changes
This commit is contained in:
Nino Righi
2023-02-03 14:31:13 +00:00
committed by Lorenz Hilpert
parent 6ba7c54089
commit 06fe8b3742
17 changed files with 213 additions and 52 deletions

View File

@@ -33,7 +33,7 @@ export class ApplicationService {
return this.activatedProcessIdSubject.asObservable();
}
constructor(private store: Store, private _availability: DomainAvailabilityService) {}
constructor(private store: Store) {}
getProcesses$(section?: 'customer' | 'branch') {
const processes$ = this.store.select(selectProcesses);
@@ -92,13 +92,6 @@ export class ApplicationService {
process.confirmClosing = true;
}
if (process.type === 'cart') {
const currentBranch = await this._availability.getDefaultBranch().pipe(first()).toPromise();
process.data = {
selectedBranch: currentBranch,
};
}
process.created = this._createTimestamp();
process.activated = 0;
this.store.dispatch(addProcess({ process }));

View File

@@ -607,4 +607,12 @@ export class DomainAvailabilityService {
)
);
}
getInStock({ itemIds, branchId }: { itemIds: number[]; branchId: number }): Observable<StockInfoDTO[]> {
return this.getStockByBranch({ id: branchId }).pipe(
mergeMap((stock) =>
this._stockService.StockInStock({ articleIds: itemIds, stockId: stock.id }).pipe(map((response) => response.result))
)
);
}
}

View File

@@ -0,0 +1,109 @@
import { Injectable } from '@angular/core';
import { StockInfoDTO } from '@swagger/remi';
import { groupBy } from 'lodash';
import { BehaviorSubject, combineLatest, Observable, Subject } from 'rxjs';
import { bufferTime } from 'rxjs/operators';
import { DomainAvailabilityService } from './availability.service';
export type ItemBranch = { itemId: number; branchId: number };
export type InStock = ItemBranch & { inStock: number; fetching: boolean };
@Injectable({ providedIn: 'root' })
export class DomainInStockService {
private _inStockQueue = new Subject<ItemBranch>();
private _inStockMap = new BehaviorSubject<Record<string, number>>({});
private _inStockFetchingMap = new BehaviorSubject<Record<string, boolean>>({});
constructor(private _availability: DomainAvailabilityService) {
// TODO: Approvement bufferWhen statt bufferTime
this._inStockQueue.pipe(bufferTime(1000)).subscribe((itemBranchData) => {
if (itemBranchData?.length > 0) {
this._fetchStockData(itemBranchData);
}
});
}
getKey({ itemId, branchId }: ItemBranch) {
return `${itemId}_${branchId}`;
}
getInStock$({ itemId, branchId }: ItemBranch): Observable<InStock> {
return new Observable<InStock>((obs) => {
const key = this.getKey({ itemId, branchId });
this.loadStock({ itemId, branchId });
const sub = combineLatest([this._inStockMap, this._inStockFetchingMap]).subscribe(([inStockMap, inStockFetchingMap]) => {
const inStock: InStock = {
itemId,
branchId,
inStock: inStockMap[key],
fetching: inStockFetchingMap[key] ?? false,
};
obs.next(inStock);
});
return () => {
sub.unsubscribe();
};
});
}
async loadStock({ itemId, branchId }: { itemId: number; branchId?: number }) {
let bId = branchId;
if (!bId) {
const defaultBranch = await this._availability.getDefaultBranch().toPromise();
bId = defaultBranch.id;
}
this._addToInStockQueue({ itemId, branchId: bId });
}
private _addToInStockQueue({ itemId, branchId }: ItemBranch): void {
this._inStockQueue.next({ itemId, branchId });
this._setInStockFetching({ itemId, branchId }, true);
}
private _setInStockFetching({ itemId, branchId }: ItemBranch, value: boolean) {
const key = this.getKey({ itemId, branchId });
const current = this._inStockFetchingMap.getValue();
this._inStockFetchingMap.next({ ...current, [key]: value });
}
private _setInStock({ itemId, branchId }: ItemBranch, value: number) {
const key = this.getKey({ itemId, branchId });
const current = this._inStockMap.getValue();
this._inStockMap.next({ ...current, [key]: value });
}
private _fetchStockData(itemBranchData: ItemBranch[]) {
const grouped = groupBy(itemBranchData, 'branchId');
Object.keys(grouped).forEach((key) => {
const branchId = Number(key);
const itemIds = itemBranchData.filter((itemBranch) => itemBranch.branchId === branchId).map((item) => item.itemId);
this._availability.getInStock({ itemIds, branchId }).subscribe(
(stockInfos) => this._fetchStockDataResponse({ itemIds, branchId })(stockInfos),
(error) => this._fetchStockDataError({ itemIds, branchId })(error)
);
});
}
private _fetchStockDataResponse = ({ itemIds, branchId }: { itemIds: number[]; branchId: number }) => (stockInfos: StockInfoDTO[]) => {
itemIds.forEach((itemId) => {
const stockInfo = stockInfos.find((stockInfo) => stockInfo.itemId === itemId && stockInfo.branchId === branchId);
let inStock = 0;
if (stockInfo) {
inStock = stockInfo.inStock;
}
this._setInStockFetching({ itemId, branchId }, false);
this._setInStock({ itemId, branchId }, inStock);
});
};
private _fetchStockDataError = ({ itemIds, branchId }: { itemIds: number[]; branchId: number }) => (error: Error) => {
itemIds.forEach((itemId) => {
this._setInStockFetching({ itemId, branchId }, false);
this._setInStock({ itemId, branchId }, 0);
});
console.error('DomainInStockService._fetchStockData()', error);
};
}

View File

@@ -3,5 +3,6 @@
*/
export * from './lib/availability.service';
export * from './lib/in-stock.service';
export * from './lib/availability.module';
export * from './lib/defs';

View File

@@ -39,7 +39,7 @@
<div class="shell-footer-wrapper">
<shell-footer *ngIf="section$ | async; let section">
<ng-container *ngIf="section === 'customer'">
<a [routerLink]="[customerBasePath$ | async, 'product']" routerLinkActive="active">
<a (click)="resetSelectedBranch()" [routerLink]="[customerBasePath$ | async, 'product']" routerLinkActive="active">
<ui-icon icon="catalog" size="30px"></ui-icon>
Artikelsuche
</a>

View File

@@ -164,6 +164,13 @@ export class ShellComponent {
});
}
async resetSelectedBranch() {
const processId = await this.activatedProcessId$.pipe(take(1)).toPromise();
if (!!processId) {
this._appService.patchProcessData(processId, { selectedBranch: undefined });
}
}
trackByIdFn: TrackByFunction<ApplicationProcess> = (_, process) => process.id;
fetchAndOpenPackages = () => this._wrongDestinationModalService.fetchAndOpen();

View File

@@ -56,3 +56,8 @@ body {
opacity: 0;
}
}
.skeleton {
@apply block bg-gray-300 h-6;
animation: load 1s ease-in-out infinite;
}

View File

@@ -33,7 +33,14 @@
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet>
</div>
<div class="item-stock"><ui-icon icon="home" size="1em"></ui-icon> {{ item?.stockInfos | stockInfos }} x</div>
<div class="item-stock">
<ui-icon icon="home" size="1em"></ui-icon>
<span *ngIf="inStock$ | async; let stock" [class.skeleton]="stock.inStock === undefined" class="min-w-[1rem] text-right inline-block">{{
stock?.inStock
}}</span>
x
</div>
<!-- <div class="item-stock"><ui-icon icon="home" size="1em"></ui-icon> {{ item?.stockInfos | stockInfos }} x</div> -->
<div class="item-ssc" [class.xs]="item?.catalogAvailability?.sscText?.length >= 60">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}

View File

@@ -1,10 +1,13 @@
import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core';
import { ApplicationService } from '@core/application';
import { DomainInStockService, InStock } from '@domain/availability';
import { ComponentStore } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { debounceTime, switchMap } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
export interface SearchResultItemComponentState {
@@ -74,11 +77,21 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return '';
}
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
switchMap((processId) => this.applicationService.getSelectedBranch$(processId))
);
inStock$ = combineLatest([this.item$, this.selectedBranchId$]).pipe(
debounceTime(100),
switchMap(([item, branch]) => this._stockService.getInStock$({ itemId: item.id, branchId: branch?.id }))
);
constructor(
private _dateAdapter: DateAdapter,
private _datePipe: DatePipe,
private _articleSearchService: ArticleSearchService,
public applicationService: ApplicationService
public applicationService: ApplicationService,
private _stockService: DomainInStockService
) {
super({
selected: false,

View File

@@ -67,13 +67,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
)
)
.subscribe(async ({ processId, queryParams, selectedBranch }) => {
const process = await this.application.getProcessById$(processId).pipe(first()).toPromise();
const processChanged = processId !== this.searchService.processId;
// Beim schließen vom aktuellen Prozess kommt die selectedBranch.id undefined zurück, da getSelectedBranch$ keinen Branch zurückliefert.
// Da die selectedBranch.id aber auch undefined sein kann (z.B. beim Clearen des Branch-Selectors) muss zusätzlich überprüft
// werden, ob der aktuelle Prozess noch existiert. Ansonsten wäre branchChanged true und search() würde gecallt werden
const branchChanged = !!process && selectedBranch?.id !== this.searchService?.selectedBranch?.id;
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
if (processChanged) {
if (!!this.searchService.processId && this.searchService.filter instanceof UiFilter) {
@@ -97,7 +93,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
const cleanQueryParams = this.cleanupQueryParams(queryParams);
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams())) || branchChanged) {
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
await this.searchService.setDefaultFilter(queryParams);
const data = this.getCachedData(processId, queryParams, selectedBranch?.id);
this.searchService.setItems(data.items);

View File

@@ -34,10 +34,6 @@ shared-branch-selector.shared-branch-selector-opend {
@apply pl-0 border-l-0;
}
::ng-deep .tablet page-catalog shared-branch-selector.shared-branch-selector-opend {
@apply w-[736px];
}
::ng-deep .tablet page-catalog shared-breadcrumb:focus-within .shared-breadcrumb__suffix {
@apply rounded-[5px];
}

View File

@@ -1,9 +1,12 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { BranchSelectorComponent } from '@shared/components/branch-selector';
import { BreadcrumbComponent } from '@shared/components/breadcrumb';
import { BranchDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { Observable } from 'rxjs';
import { first, map, switchMap } from 'rxjs/operators';
import { BehaviorSubject, from, fromEvent, Observable, Subject } from 'rxjs';
import { first, map, switchMap, takeUntil } from 'rxjs/operators';
@Component({
selector: 'page-catalog',
@@ -12,11 +15,24 @@ import { first, map, switchMap } from 'rxjs/operators';
providers: [],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PageCatalogComponent implements OnInit {
export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChild(BreadcrumbComponent, { read: ElementRef }) breadcrumbRef: ElementRef<HTMLElement>;
@ViewChild(BranchSelectorComponent, { read: ElementRef }) branchSelectorRef: ElementRef<HTMLElement>;
activatedProcessId$: Observable<string>;
selectedBranch$: Observable<BranchDTO>;
constructor(public application: ApplicationService, private _uiModal: UiModalService) {}
get branchSelectorWidth() {
return `${this.breadcrumbRef?.nativeElement?.clientWidth}px`;
}
_onDestroy$ = new Subject<boolean>();
constructor(
public application: ApplicationService,
private _uiModal: UiModalService,
private _environmentService: EnvironmentService,
private _renderer: Renderer2
) {}
ngOnInit() {
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
@@ -24,6 +40,27 @@ export class PageCatalogComponent implements OnInit {
this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId))));
}
ngAfterViewInit(): void {
if (this._environmentService.isTablet()) {
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
this._renderer.setStyle(this.branchSelectorRef?.nativeElement, 'width', this.branchSelectorWidth);
});
fromEvent(this.branchSelectorRef.nativeElement, 'focusout')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {
this._renderer.removeStyle(this.branchSelectorRef?.nativeElement, 'width');
});
}
}
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
async patchProcessData(selectedBranch: BranchDTO) {
try {
const processId = await this.activatedProcessId$.pipe(first()).toPromise();

View File

@@ -1,8 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { map, withLatestFrom } from 'rxjs/operators';
import { map } from 'rxjs/operators';
@Component({
selector: 'page-goods-out',
@@ -11,18 +9,7 @@ import { map, withLatestFrom } from 'rxjs/operators';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GoodsOutComponent {
// #3359 Set Default selectedBranch if navigate to goods-out
processId$ = this._activatedRoute.data.pipe(
withLatestFrom(this._availability.getDefaultBranch()),
map(([data, currentBranch]) => {
this._application.patchProcessData(Number(data.processId), { selectedBranch: currentBranch });
return String(data.processId);
})
);
processId$ = this._activatedRoute.data.pipe(map((data) => String(data.processId)));
constructor(
private _activatedRoute: ActivatedRoute,
private _application: ApplicationService,
private _availability: DomainAvailabilityService
) {}
constructor(private _activatedRoute: ActivatedRoute) {}
}

View File

@@ -1,4 +1,4 @@
<button class="shared-branch-selector-input-container" (click)="branchInput.focus(); openComplete()">
<div class="shared-branch-selector-input-container" (click)="branchInput.focus(); openComplete()">
<button class="shared-branch-selector-input-icon">
<ui-svg-icon class="text-[#AEB7C1]" icon="magnify" [size]="32"></ui-svg-icon>
</button>
@@ -16,7 +16,7 @@
<button class="shared-branch-selector-clear-input-icon" *ngIf="(query$ | async)?.length > 0" type="button" (click)="clear()">
<ui-svg-icon class="text-[#1F466C]" icon="close" [size]="32"></ui-svg-icon>
</button>
</button>
</div>
<ui-autocomplete class="shared-branch-selector-autocomplete z-modal w-full">
<hr class="ml-3 text-[#9CB1C6]" *ngIf="autocompleteComponent?.opend" uiAutocompleteSeparator />
<p *ngIf="(branches$ | async)?.length > 0" class="text-base p-4 font-normal" uiAutocompleteLabel>Filialvorschläge</p>

View File

@@ -13,7 +13,6 @@ import {
ViewChild,
} from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import { provideComponentStore } from '@ngrx/component-store';
import { BranchDTO, BranchType } from '@swagger/checkout';
import { UiAutocompleteComponent, UiAutocompleteModule } from '@ui/autocomplete';
import { UiCommonModule } from '@ui/common';
@@ -127,8 +126,11 @@ export class BranchSelectorComponent implements OnInit, AfterViewInit, ControlVa
branches?.sort((branchA, branchB) => branchA?.name?.localeCompare(branchB?.name));
onQueryChange(query: string) {
if (query.trim().length === 0) {
return;
}
this.store.setQuery(query);
this.complete.next(query);
this.complete.next(query.trim());
}
openComplete() {

View File

@@ -7,9 +7,9 @@
}
.shared-breadcrumb__prefix::after {
@apply block w-4 -right-4 inset-y-0 absolute;
@apply block w-6 -right-6 inset-y-0 absolute;
content: '';
background-image: linear-gradient(to right, white, transparent);
box-shadow: 16px 0px 24px 0px white inset;
}
.shared-breadcrumb__suffix {
@@ -17,9 +17,9 @@
}
.shared-breadcrumb__suffix::before {
@apply block w-4 -left-4 inset-y-0 absolute;
@apply block w-6 -left-6 inset-y-0 absolute;
content: '';
background-image: linear-gradient(to left, white, transparent);
box-shadow: -16px 0px 24px 0px white inset;
}
.shared-breadcrumb__crumbs {

View File

@@ -48,6 +48,7 @@ export class UiAutocompleteComponent implements AfterContentInit, OnDestroy {
this.subscriptions.add(
this.items.changes.subscribe(() => {
this.registerItemOnClick();
this.activateFirstItem();
})
);
}
@@ -80,14 +81,13 @@ export class UiAutocompleteComponent implements AfterContentInit, OnDestroy {
}
activateFirstItem() {
if (this.items.length > 0) {
if (this.items.length === 1) {
this.listKeyManager.setFirstItemActive();
}
}
open() {
this.opend = true;
this.activateFirstItem();
this.cdr.markForCheck();
}