This commit is contained in:
Lorenz Hilpert
2021-06-24 13:19:22 +02:00
14 changed files with 190 additions and 136 deletions

View File

@@ -151,7 +151,13 @@
<button *ngIf="!(store.isDownload$ | async)" class="cta-availabilities" (click)="showAvailabilities()">
weitere Verfügbarkeiten
</button>
<button class="cta-continue" (click)="showPurchasingModal()" [disabled]="fetchingAvailabilities$ | async">In den Warenkorb</button>
<button
class="cta-continue"
(click)="showPurchasingModal()"
[disabled]="!(isAvailable$ | async) || (fetchingAvailabilities$ | async)"
>
In den Warenkorb
</button>
</div>
<div class="product-formats" *ngIf="item.family?.length > 0">

View File

@@ -39,6 +39,14 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
this.store.fetchingTakeAwayAvailability$,
]).pipe(map((values) => values.some((v) => v)));
isAvailable$ = combineLatest([
this.store.isDeliveryAvailabilityAvailable$,
this.store.isDeliveryDigAvailabilityAvailable$,
this.store.isDeliveryB2BAvailabilityAvailable$,
this.store.isPickUpAvailabilityAvailable$,
this.store.isTakeAwayAvailabilityAvailable$,
]).pipe(map((values) => values.some((v) => v)));
showDeliveryTruck$ = combineLatest([this.store.isDeliveryAvailabilityAvailable$, this.store.isDeliveryDigAvailabilityAvailable$]).pipe(
map(([delivery, digDelivery]) => delivery || digDelivery)
);

View File

@@ -69,7 +69,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.scrollTop(Number(queryParams.scrollPos ?? 0));
// Fügt Breadcrumb hinzu falls dieser noch nicht vorhanden ist
this.breadcrumb.addBreadcrumbIfNotExists({
await this.breadcrumb.addBreadcrumbIfNotExists({
key: processId,
name: `${this.store.query} (Lade Ergebnisse)`,
path: '/product/search/results',

View File

@@ -233,6 +233,7 @@ export class CustomerCreateOnlineComponent extends CustomerCreateComponentBase i
} catch (error) {
if (error?.error?.invalidProperties?.Email) {
this.addInvalidDomain(this.control.value.communicationDetails?.email);
this.enableControl();
} else {
this.setValidationError(error.error?.invalidProperties, this.control);
}

View File

@@ -1,41 +0,0 @@
import { ActivatedRoute } from '@angular/router';
import { StringDictionary } from '@cmf/core';
import { isEqual } from 'lodash';
import { Subscription } from 'rxjs';
import { finalize } from 'rxjs/operators';
export abstract class ActivatedRouteConnector {
private connectedRouteSubscription: Subscription;
connect(route: ActivatedRoute, options?: { connected?: (params: StringDictionary<string>) => void; disconnected?: () => void }) {
this.disconnect();
let connected = false;
this.connectedRouteSubscription = route.queryParams.pipe(finalize(() => options?.disconnected?.call(undefined))).subscribe((params) => {
const current = this.getQueryParams();
if (!isEqual(current, params)) {
this.setQueryParams({ params });
}
if (!connected) {
connected = true;
setTimeout(() => options?.connected?.call(undefined, params), 0);
}
});
return {
disconnect: () => {
this.disconnect();
},
};
}
disconnect() {
this.connectedRouteSubscription?.unsubscribe();
}
abstract setQueryParams({ params }: { params: StringDictionary<string> }): void;
abstract getQueryParams(): StringDictionary<string>;
}

View File

@@ -1,18 +1,15 @@
import { Component, ChangeDetectionStrategy, forwardRef, NgZone, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Location } from '@angular/common';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { CrmCustomerService } from '@domain/crm';
import { FilterOption, SelectFilterOption, UiFilterMappingService } from '@ui/filter';
import { FilterOption, UiFilterMappingService } from '@ui/filter';
import { NativeContainerService } from 'native-container';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { delay, map, switchMap } from 'rxjs/operators';
import { CustomerSearch } from './customer-search.service';
import { isSelectFilterOption } from 'apps/ui/filter/src/lib/type-guards';
import { CacheService } from 'apps/core/cache/src/public-api';
import { StringDictionary } from '@cmf/core';
@Component({
selector: 'page-customer-search',

View File

@@ -13,16 +13,25 @@ import { NativeContainerService } from 'native-container';
import { StringDictionary } from '@cmf/core';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { ActivatedRouteConnector } from './activated-route-connector';
@Injectable()
export abstract class CustomerSearch extends ActivatedRouteConnector implements OnInit, OnDestroy {
export abstract class CustomerSearch implements OnInit, OnDestroy {
protected abstract customerSearch: CrmCustomerService;
private queryParams: StringDictionary<string> = this.route.snapshot.queryParams;
private queryParams: StringDictionary<string>;
private destroy$ = new Subject();
private _processId: number;
get processId(): number {
return this._processId;
}
set processId(processId: number) {
this._processId = processId;
}
filtersLoaded$ = new BehaviorSubject<boolean>(false);
initQueryFilter$ = new BehaviorSubject<{ initialFilter: Filter[] }>({
@@ -64,6 +73,8 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
return this.searchState$.value;
}
hits$ = new BehaviorSubject<number>(undefined);
get searchResult(): PagedResult<CustomerSearchType> {
return this.searchResult$.value;
}
@@ -76,7 +87,7 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
return this.searchResult$.value.result.length;
}
private get totalResults(): number {
public get totalResults(): number {
if (!this.searchResult$ || !this.searchResult$.value || !this.searchResult$.value.hits) {
return 0;
}
@@ -93,9 +104,7 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
private environmentService: EnvironmentService,
private filterMapping: UiFilterMappingService,
private nativeContainer: NativeContainerService
) {
super();
}
) {}
ngOnInit() {
this.initAvailableFilters();
@@ -110,21 +119,21 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
setQueryParams({ params }: { params: StringDictionary<string> }): void {
this.queryParams = params;
this.parseQueryParams();
}
getQueryParams(): StringDictionary<string> {
return this.queryParams;
return this.queryParams || {};
}
parseQueryParams() {
const params = new URL(this.router.url, window.location.origin).searchParams;
const params = this.getQueryParams();
this.setQuery(params.get('query'));
this.setQuery(params.query);
const filters = this.queryFilter.filters.map(cloneFilter);
filters.forEach((f) => {
const values = params.get(String(f.key))?.split(';');
const values = params[f.key]?.split(';');
if (values?.length > 0) {
f.options.forEach((o) => {
@@ -136,7 +145,6 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
});
}
});
this.setFilter(filters);
}
@@ -240,7 +248,6 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
dict[filter.key] = selected.map((o) => o.id).join(';');
}
}
return dict;
}
@@ -255,7 +262,6 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
}
if (this.searchState !== 'fetching') {
this.searchState$.next('fetching');
this.searchResult$
.pipe(
take(1),
@@ -301,7 +307,7 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
)
.subscribe((r) => {
const hits = r.hits || r.result.length;
this.hits$.next(hits);
this.searchResult$.next(this.removeLoadingProducts(this.searchResult));
if (options.isNewSearch) {
this.searchResult$.next({
@@ -325,6 +331,7 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
}
if (hits > 0) {
this.searchState$.next('result');
this.filterActive$.next(false);
}
});
@@ -384,7 +391,7 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
)
.subscribe((r) => {
const hits = r.hits || r.result.length;
this.hits$.next(hits);
this.searchResult$.next(this.removeLoadingProducts(this.searchResult));
if (options.isNewSearch) {
this.searchResult$.next({
@@ -408,6 +415,7 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
}
if (hits > 0) {
this.searchState$.next('result');
this.filterActive$.next(false);
}
});
@@ -463,9 +471,10 @@ export abstract class CustomerSearch extends ActivatedRouteConnector implements
currentResult: PagedResult<CustomerSearchType>,
numberOfLoadingProducts: number = 10
): PagedResult<CustomerSearchType> {
const res = currentResult.result ? currentResult.result : [];
return {
...currentResult,
result: [...currentResult.result, ...new Array(numberOfLoadingProducts).fill({ loaded: false })],
result: [...res, ...new Array(numberOfLoadingProducts).fill({ loaded: false })],
};
}

View File

@@ -3,9 +3,9 @@ import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { CacheService } from 'apps/core/cache/src/public-api';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { isEqual } from 'lodash';
import { combineLatest, Subject, Subscription } from 'rxjs';
import { debounceTime, first } from 'rxjs/operators';
import { CustomerSearch } from '../customer-search.service';
@Component({
@@ -19,38 +19,51 @@ export class CustomerSearchMainComponent implements OnInit, OnDestroy {
protected isMobile: boolean;
subscriptions = new Subscription();
constructor(
public search: CustomerSearch,
public cdr: ChangeDetectorRef,
public environmentService: EnvironmentService,
public application: ApplicationService,
private breadcrumb: BreadcrumbService,
private route: ActivatedRoute,
private cache: CacheService
private route: ActivatedRoute
) {}
ngOnInit() {
this.detectDevice();
this.initBreadcrumb();
this.search.connect(this.route);
}
this.subscriptions.add(
combineLatest([this.application.activatedProcessId$, this.route.queryParams])
.pipe(debounceTime(0))
.subscribe(async ([processId, queryParams]) => {
// Setzen des aktuellen Prozesses
if (this.search?.processId !== processId) {
this.search.processId = processId;
}
initBreadcrumb() {
this.application.activatedProcessId$.pipe(takeUntil(this.destroy$)).subscribe((key) => {
this.breadcrumb.addBreadcrumbIfNotExists({
key,
name: 'Kundensuche',
path: '/customer/search',
tags: ['customer', 'search', 'main', 'filter'],
params: this.search.createQueryParams(),
});
});
// Updaten der QueryParams wenn diese sich ändern
if (!isEqual(this.search.getQueryParams(), queryParams)) {
const params = { ...queryParams };
delete params.scrollPos;
this.search.setQueryParams({ params });
}
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(processId, ['customer', 'search', 'main', 'filter'])
.pipe(first())
.toPromise();
for (const crumb of crumbs) {
this.breadcrumb.removeBreadcrumbsAfter(crumb.id, ['customer']);
}
})
);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
this.search.disconnect();
this.subscriptions.unsubscribe();
}
async detectDevice() {

View File

@@ -1,10 +1,11 @@
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
import { BreadcrumbService } from '@core/breadcrumb';
import { CacheService } from 'apps/core/cache/src/public-api';
import { Observable, Subscription } from 'rxjs';
import { filter, map, take, tap } from 'rxjs/operators';
import { debounce, isEqual } from 'lodash';
import { combineLatest, Observable, Subscription } from 'rxjs';
import { debounceTime, first, map } from 'rxjs/operators';
import { CustomerSearch } from '../customer-search.service';
import { CustomerSearchType } from '../defs';
@@ -14,16 +15,18 @@ import { CustomerSearchType } from '../defs';
styleUrls: ['./search-results.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerSearchResultComponent implements OnInit, OnDestroy, AfterViewInit {
export class CustomerSearchResultComponent implements OnInit, OnDestroy {
@ViewChild('scrollContainer', { static: false }) scrollContainer: ElementRef<HTMLDivElement>;
private _breadcrumb: Breadcrumb;
customers$: Observable<CustomerSearchType[]>;
cacheSubscription: Subscription;
subscriptions = new Subscription();
protected readonly viewportEnterOptions: IntersectionObserverInit = {
threshold: 0.75,
};
triggerSearchDebounce = debounce(() => this.triggerSearch(), 1000);
constructor(
private application: ApplicationService,
private breadcrumb: BreadcrumbService,
@@ -33,61 +36,93 @@ export class CustomerSearchResultComponent implements OnInit, OnDestroy, AfterVi
) {}
ngOnInit() {
this.search.connect(this.route, {
connected: () => {
this.route.queryParams.subscribe((params) => {
const cachedItems = this.cache.get(params);
if (cachedItems) {
this.search.searchResult$.next(cachedItems);
}
});
},
disconnected: () => {
this.cache.set(this.search.getQueryParams(), this.search.searchResult);
},
});
this.customers$ = this.search.searchResult$.pipe(map((response) => response.result));
this.initBreadcrumb();
this.subscriptions.add(
combineLatest([this.application.activatedProcessId$, this.route.queryParams])
.pipe(debounceTime(0))
.subscribe(async ([processId, queryParams]) => {
// Wenn ein Prozess bereits zugewiesen ist und der Prozess sich ändert
// Speicher Ergebnisse in den Cache und Update Breadcrumb Params
if (this.search.processId !== processId) {
this.cacheCurrentItems();
await this.updateBreadcrumbs();
}
// Setzen des aktuellen Prozesses
this.search.processId = processId;
// Updaten der QueryParams wenn diese sich ändern
// scrollPos muss entfernt werden um die items anhand der QueryParams zu cachen
if (!isEqual(this.search.getQueryParams(), queryParams)) {
const params = { ...queryParams };
delete params.scrollPos;
const items = this.cache.get<CustomerSearchType[]>(params);
this.search.setQueryParams({ params });
this.setItems(items);
this.triggerSearchDebounce();
}
// Nach dem setzen der Items im store an die letzte Position scrollen
setTimeout(() => this.scrollContainer.nativeElement.scrollTo(0, Number(queryParams.scrollPos ?? 0)), 0);
// Fügt Breadcrumb hinzu falls dieser noch nicht vorhanden ist
await this.breadcrumb.addBreadcrumbIfNotExists({
key: processId,
name: `${this.search.queryFilter.query} (Lade Ergebnisse)`,
path: '/customer/search/result',
params: queryParams,
tags: ['customer', 'search', 'results', 'filter'],
});
await this.removeDetailBreadcrumb();
// await this.updateBreadcrumbs();
})
);
this.subscriptions.add(
this.search.hits$.pipe(debounceTime(0)).subscribe(() => {
this.updateBreadcrumbs();
})
);
}
ngAfterViewInit() {}
async removeDetailBreadcrumb() {
const processId = this.search.processId;
const crumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['customer', 'details']).pipe(first()).toPromise();
async initBreadcrumb() {
await this.search.filtersLoaded$
.pipe(
filter((isLoaded) => !!isLoaded),
take(1)
)
for (const crumb of crumbs) {
this.breadcrumb.removeBreadcrumb(crumb.id);
}
}
async updateBreadcrumbs() {
const scrollPos = this.scrollContainer.nativeElement.scrollTop;
const processId = this.search.processId;
const queryParams = { ...this.search.getQueryParams() };
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(processId, ['customer', 'search', 'results', 'filter'])
.pipe(first())
.toPromise();
this._breadcrumb = await this.breadcrumb.addBreadcrumbIfNotExists({
key: this.application.activatedProcessId,
name: `${this.search?.queryFilter?.query}`,
path: '/customer/search/result',
tags: ['customer', 'search', 'results', 'filter'],
params: this.search.createQueryParams(),
});
const params = { ...queryParams, scrollPos };
this.search.searchResult$.subscribe(() => {
let name = this.search?.queryFilter?.query;
if (this.search?.searchResult?.hits > 1) {
name += ` (${this.search?.searchResult?.hits} Ergebnisse)`;
}
const hits = await this.search.hits$.pipe(first()).toPromise();
this.breadcrumb.patchBreadcrumb(this._breadcrumb.id, { name });
});
if (!this.search?.searchResult?.result?.length) {
this.triggerSearch();
for (const crumb of crumbs) {
this.breadcrumb.patchBreadcrumb(crumb.id, {
params,
name:
hits !== undefined
? `${this.search.queryFilter.query} (${hits} Ergebnisse)`
: `${this.search.queryFilter.query} (Lade Ergebnisse)`,
});
}
}
ngOnDestroy() {
this.search.disconnect();
if (!!this.cacheSubscription) {
this.cacheSubscription.unsubscribe();
}
this.subscriptions.unsubscribe();
this.cacheCurrentItems();
this.updateBreadcrumbs();
}
checkIfReload(target: HTMLElement): void {
@@ -99,4 +134,14 @@ export class CustomerSearchResultComponent implements OnInit, OnDestroy, AfterVi
triggerSearch() {
this.search.search({ isNewSearch: false, take: 10 });
}
setItems(items: CustomerSearchType[]) {
this.search.searchResult$.next({
result: items,
});
}
cacheCurrentItems() {
this.cache.set(this.search.getQueryParams(), this.search.searchResult.result);
}
}

View File

@@ -110,6 +110,11 @@ export class TaskListComponent {
return 1;
}
}
if (statusB.includes('Completed')) {
return -1;
}
return 0;
} else if (aHasStatus && !bHasStatus) {
return -1;

View File

@@ -6,6 +6,9 @@
>{{ group.items[0].firstName }} {{ group.items[0].lastName }}</span
>
</h3>
<h4 class="sub-heading">
{{ group.items[0].buyerNumber }}
</h4>
<ng-container *ngFor="let byOrderNumber of group.items | groupBy: byOrderNumberFn; let lastOrder = last">
<ng-container *ngFor="let byProcessingStatus of byOrderNumber.items | groupBy: byProcessingStatusFn; let lastStatus = last">

View File

@@ -7,7 +7,11 @@
.heading {
margin: 0;
margin-bottom: 18px;
margin-bottom: 9px;
}
.sub-heading {
@apply m-0 mb-1 text-regular font-bold;
}
}

View File

@@ -44,7 +44,7 @@ export class ShellBreadcrumbComponent implements OnInit {
const activated = crumbs.find((crumb) => this.router.url.split('?')[0].endsWith(crumb.path));
if (activated) {
this.breadcrumbService.removeBreadcrumbsAfter(activated.id);
// this.breadcrumbService.removeBreadcrumbsAfter(activated.id);
return activated;
}
}),

View File

@@ -34,6 +34,8 @@ export class UiRangeFilterComponent implements OnInit, OnChanges {
return `Der Wert darf nicht kleiner als ${errors[key].min} sein.`;
case 'max':
return `Der Wert darf nicht größer als ${errors[key].max} sein.`;
case 'pattern':
return `Es werden nur ganzzahlige Werte akzeptiert.`;
default:
return errors[key];
}
@@ -47,11 +49,13 @@ export class UiRangeFilterComponent implements OnInit, OnChanges {
start: this.fb.control(this.filter.options[0].value, [
Validators.min(0),
Validators.max(99),
Validators.pattern('^[0-9]+$'), // Only numbers
maxValueRelativeTo((c) => c?.parent?.get('stop')),
]),
stop: this.fb.control(this.filter.options[1].value, [
Validators.min(0),
Validators.max(99),
Validators.pattern('^[0-9]+$'), // Only numbers
minValueRelativeTo((c) => c?.parent?.get('start')),
]),
});