Merge branch 'release/1.7'

This commit is contained in:
Michael Auer
2022-05-02 09:37:27 +02:00
163 changed files with 6438 additions and 3205 deletions

View File

@@ -3296,6 +3296,46 @@
}
}
}
},
"@ui/branch-dropdown": {
"projectType": "library",
"root": "apps/ui/branch-dropdown",
"sourceRoot": "apps/ui/branch-dropdown/src",
"prefix": "lib",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:ng-packagr",
"options": {
"tsConfig": "apps/ui/branch-dropdown/tsconfig.lib.json",
"project": "apps/ui/branch-dropdown/ng-package.json"
},
"configurations": {
"production": {
"tsConfig": "apps/ui/branch-dropdown/tsconfig.lib.prod.json"
}
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"main": "apps/ui/branch-dropdown/src/test.ts",
"tsConfig": "apps/ui/branch-dropdown/tsconfig.spec.json",
"karmaConfig": "apps/ui/branch-dropdown/karma.conf.js"
}
},
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"apps/ui/branch-dropdown/tsconfig.lib.json",
"apps/ui/branch-dropdown/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**"
]
}
}
}
}
},
"defaultProject": "sales"

View File

@@ -1,8 +1,14 @@
import { Injectable } from '@angular/core';
import { ItemDTO } from '@swagger/cat';
import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, StoreCheckoutService, SupplierDTO } from '@swagger/checkout';
import { Observable } from 'rxjs';
import { AvailabilityService as SwaggerAvailabilityService } from '@swagger/availability';
import { combineLatest, Observable, of } from 'rxjs';
import {
AvailabilityRequestDTO,
AvailabilityService as SwaggerAvailabilityService,
AvailabilityDTO as SwaggerAvailabilityDTO,
AvailabilityType,
} from '@swagger/availability';
import { AvailabilityDTO as CatAvailabilityDTO } from '@swagger/cat';
import { map, shareReplay, switchMap, withLatestFrom, mergeMap, timeout } from 'rxjs/operators';
import { isArray, memorize } from '@utils/common';
import { OrderService } from '@swagger/oms';
@@ -60,6 +66,14 @@ export class DomainAvailabilityService {
);
}
@memorize({})
getLogisticians() {
return this.orderService.OrderGetLogisticians({}).pipe(
map((response) => response.result?.find((l) => l.logisticianNumber === '2470')),
shareReplay()
);
}
getTakeAwayAvailabilityByBranches({
branchIds,
itemId,
@@ -117,8 +131,10 @@ export class DomainAvailabilityService {
price: PriceDTO;
quantity: number;
}): Observable<AvailabilityDTO> {
return this._stock.StockStockRequest({ stockRequest: { branchIds: [branch.id], itemId } }).pipe(
withLatestFrom(this.getTakeAwaySupplier()),
return combineLatest([
this._stock.StockStockRequest({ stockRequest: { branchIds: [branch.id], itemId } }),
this.getTakeAwaySupplier(),
]).pipe(
map(([response, supplier]) => {
return this._mapToTakeAwayAvailability({ response, supplier, branch, quantity, price });
}),
@@ -168,29 +184,7 @@ export class DomainAvailabilityService {
},
])
.pipe(
map((r) => {
const availabilities = r.result;
if (isArray(availabilities)) {
const preferred = availabilities.find((f) => f.preferred === 1);
const totalAvailable = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.at,
price: preferred?.price,
inStock: totalAvailable,
supplierProductNumber: preferred?.supplierProductNumber,
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}
}),
map((r) => this._mapToPickUpAvailability(r.result)?.find((_) => true)),
shareReplay()
);
}
@@ -208,23 +202,7 @@ export class DomainAvailabilityService {
])
.pipe(
timeout(5000),
map((r) => {
const availabilities = r.result;
const preferred = availabilities.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.at,
price: preferred?.price,
supplierProductNumber: preferred?.supplierProductNumber,
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
shareReplay()
);
}
@@ -244,7 +222,7 @@ export class DomainAvailabilityService {
timeout(5000),
map((r) => {
const availabilities = r.result;
const preferred = availabilities.find((f) => f.preferred === 1);
const preferred = availabilities?.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
@@ -252,7 +230,8 @@ export class DomainAvailabilityService {
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.at,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
estimatedDelivery: preferred?.estimatedDelivery,
price: preferred?.price,
logistician: { id: preferred?.logisticianId },
supplierProductNumber: preferred?.supplierProductNumber,
@@ -267,9 +246,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getB2bDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
const logistician$ = this.orderService
.OrderGetLogisticians({})
.pipe(map((response) => response.result.find((l) => l.logisticianNumber === '2470')));
const logistician$ = this.getLogisticians();
const currentBranch$ = this.getCurrentBranch();
@@ -298,7 +275,7 @@ export class DomainAvailabilityService {
.pipe(
map((r) => {
const availabilities = r.result;
const preferred = availabilities.find((f) => f.preferred === 1);
const preferred = availabilities?.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
@@ -306,7 +283,7 @@ export class DomainAvailabilityService {
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.at,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
price: preferred?.price,
supplierProductNumber: preferred?.supplierProductNumber,
logistician: { id: preferred?.logisticianId },
@@ -320,18 +297,83 @@ export class DomainAvailabilityService {
}
@memorize({ ttl: 10000 })
getStoreAvailabilities({ item, branch, quantity }: { item: ItemData; quantity: number; branch: BranchDTO }) {
return this.swaggerAvailabilityService
.AvailabilityStoreAvailability([
{
qty: quantity,
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
shopId: branch?.id,
price: item?.price,
},
])
.pipe(map((response) => response.result));
getTakeAwayAvailabilities(items: { id: number; price: PriceDTO }[], branchId: number) {
return this._stock.StockGetStocksByBranch({ branchId }).pipe(
map((req) => req.result?.find((_) => true)?.id),
switchMap((stockId) =>
stockId
? this._stock.StockInStock({ articleIds: items.map((i) => i.id), stockId })
: of({ result: [] } as ResponseArgsOfIEnumerableOfStockInfoDTO)
),
timeout(20000),
withLatestFrom(this.getTakeAwaySupplier()),
map(([response, supplier]) => {
return response.result?.map((stockInfo) =>
this._mapToTakeAwayAvailabilities({
stockInfo,
supplier,
quantity: 1,
price: items?.find((i) => i.id === stockInfo.itemId)?.price,
})
);
}),
shareReplay()
);
}
@memorize({ ttl: 10000 })
getPickUpAvailabilities(payload: AvailabilityRequestDTO[], preferred?: boolean) {
return this.swaggerAvailabilityService.AvailabilityStoreAvailability(payload).pipe(
timeout(20000),
map((response) => (preferred ? this._mapToPickUpAvailability(response.result) : response.result))
);
}
@memorize({ ttl: 10000 })
getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this.swaggerAvailabilityService.AvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result))
);
}
@memorize({ ttl: 10000 })
getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this.swaggerAvailabilityService.AvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result))
);
}
@memorize({ ttl: 10000 })
getB2bDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
const logistician$ = this.getLogisticians();
return this.getPickUpAvailabilities(payload, true).pipe(
timeout(20000),
switchMap((availability) =>
logistician$.pipe(map((logistician) => ({ availability: [...availability], logistician: { id: logistician.id } })))
),
shareReplay()
);
}
getPriceForAvailability(
purchasingOption: string,
catalogAvailability: CatAvailabilityDTO | AvailabilityDTO,
availability: AvailabilityDTO
): PriceDTO {
switch (purchasingOption) {
case 'take-away':
return availability?.price || availability?.retailPrice;
case 'delivery':
case 'dig-delivery':
if (catalogAvailability?.price?.value?.value < availability?.price?.value?.value) {
return catalogAvailability?.price;
}
return availability?.price || catalogAvailability?.price;
}
return availability?.price;
}
isAvailable({ availability }: { availability: AvailabilityDTO }) {
@@ -377,7 +419,7 @@ export class DomainAvailabilityService {
quantity: number;
price: PriceDTO;
}): AvailabilityDTO {
const stockInfo = response.result.find((si) => si.branchId === branch.id);
const stockInfo = response.result?.find((si) => si.branchId === branch.id);
const inStock = stockInfo?.inStock ?? 0;
const availability: AvailabilityDTO = {
availabilityType: quantity <= inStock ? 1024 : 1, // 1024 (=Available)
@@ -390,4 +432,73 @@ export class DomainAvailabilityService {
};
return availability;
}
private _mapToTakeAwayAvailabilities({
stockInfo,
quantity,
price,
supplier,
}: {
stockInfo: StockInfoDTO;
quantity: number;
price: PriceDTO;
supplier: SupplierDTO;
}) {
const inStock = stockInfo?.inStock ?? 0;
const availability = {
itemId: stockInfo.itemId,
availabilityType: quantity <= inStock ? (1024 as AvailabilityType) : (1 as AvailabilityType), // 1024 (=Available)
inStock: inStock,
supplierSSC: quantity <= inStock ? '999' : '',
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
price,
supplier: { id: supplier?.id },
};
return availability;
}
private _mapToPickUpAvailability(availabilities: SwaggerAvailabilityDTO[]) {
if (isArray(availabilities)) {
const preferred = availabilities.filter((f) => f.preferred === 1);
const totalAvailable = availabilities.reduce((sum, av) => sum + (av?.qty || 0), 0);
return preferred.map((p) => {
return {
availabilityType: p?.status,
ssc: p?.ssc,
sscText: p?.sscText,
supplier: { id: p?.supplierId },
isPrebooked: p?.isPrebooked,
estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at,
price: p?.price,
inStock: totalAvailable,
supplierProductNumber: p?.supplierProductNumber,
supplierInfo: p?.requestStatusCode,
lastRequest: p?.requested,
itemId: p.itemId,
};
});
}
}
private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]) {
const preferred = availabilities.filter((f) => f.preferred === 1);
return preferred.map((p) => {
return {
availabilityType: p?.status,
ssc: p?.ssc,
sscText: p?.sscText,
isPrebooked: p?.isPrebooked,
estimatedShippingDate: p?.requestStatusCode === '32' ? p?.altAt : p?.at,
estimatedDelivery: p?.estimatedDelivery,
price: p?.price,
supplierProductNumber: p?.supplierProductNumber,
supplierInfo: p?.requestStatusCode,
lastRequest: p?.requested,
itemId: p.itemId,
};
});
}
}

View File

@@ -20,11 +20,12 @@ import {
StoreCheckoutService,
UpdateShoppingCartItemDTO,
InputDTO,
ItemPayload,
} from '@swagger/checkout';
import { DisplayOrderDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
import { isNullOrUndefined, memorize } from '@utils/common';
import { combineLatest, Observable, of, concat, isObservable, throwError } from 'rxjs';
import { bufferCount, catchError, filter, first, map, mergeMap, shareReplay, tap, withLatestFrom } from 'rxjs/operators';
import { bufferCount, catchError, filter, first, map, mergeMap, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import * as DomainCheckoutSelectors from './store/domain-checkout.selectors';
import * as DomainCheckoutActions from './store/domain-checkout.actions';
@@ -183,6 +184,48 @@ export class DomainCheckoutService {
);
}
canAddItems({ processId, payload, orderType }: { processId: number; payload: ItemPayload[]; orderType: string }) {
return this.getShoppingCart({ processId }).pipe(
first(),
withLatestFrom(this.store.select(DomainCheckoutSelectors.selectCustomerFeaturesByProcessId, { processId })),
mergeMap(([shoppingCart, customerFeatures]) => {
payload = payload?.map((p) => {
return {
...p,
customerFeatures,
orderType,
};
});
return this.storeCheckoutService
.StoreCheckoutCanAddItems({
shoppingCartId: shoppingCart.id,
payload,
})
.pipe(
map((response) => {
return response.result;
})
);
})
);
}
updateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
}: {
shoppingCartId: number;
shoppingCartItemId: number;
availability: AvailabilityDTO;
}) {
return this.storeCheckoutService.StoreCheckoutUpdateShoppingCartItemAvailability({
shoppingCartId,
shoppingCartItemId,
availability,
});
}
updateItemInShoppingCart({
processId,
shoppingCartItemId,
@@ -280,6 +323,10 @@ export class DomainCheckoutService {
);
}
getOlaErrors({ processId }: { processId: number }): Observable<number[]> {
return this.store.select(DomainCheckoutSelectors.selectOlaErrorsByProcessId, { processId });
}
setPayment({ processId, paymentType }: { processId: number; paymentType: PaymentType }): Observable<CheckoutDTO> {
return this.getCheckout({ processId }).pipe(
first(),
@@ -349,6 +396,53 @@ export class DomainCheckoutService {
);
}
checkAvailabilities({ processId }: { processId: number }): Observable<any> {
const shoppingCart$ = this.getShoppingCart({ processId }).pipe(first());
const itemsToCheck$ = shoppingCart$.pipe(
map((cart) => cart?.items?.filter((item) => item?.data?.features?.orderType === 'Download' && !item.data.availability.lastRequest))
);
return itemsToCheck$.pipe(
withLatestFrom(shoppingCart$),
switchMap(async ([items, cart]) => {
const errorIds = [];
for (const item of items) {
const availability = await this.availabilityService
.getDownloadAvailability({
item: {
ean: item.data.product.ean,
itemId: Number(item.data.product.catalogProductNumber),
price: item.data.availability.price,
},
})
.toPromise();
if (!availability || !this.availabilityService.isAvailable({ availability })) {
errorIds.push(item.id);
} else {
await this.updateShoppingCartItemAvailability({
shoppingCartId: cart.id,
shoppingCartItemId: item.id,
availability: {
...availability,
lastRequest: new Date().toISOString(),
},
}).toPromise();
}
}
this.setOlaErrors({ processId, errorIds });
if (errorIds.length > 0) {
throw throwError(new Error(`Artikel nicht verfügbar`));
} else {
return of(undefined);
}
})
);
}
updateAvailabilities({ processId }: { processId: number }): Observable<any> {
const shoppingCart$ = this.getShoppingCart({ processId }).pipe(first());
const itemsToUpdate$ = shoppingCart$.pipe(
@@ -511,6 +605,8 @@ export class DomainCheckoutService {
})
);
const checkAvailabilities$ = this.checkAvailabilities({ processId });
const updateAvailabilities$ = this.updateAvailabilities({ processId });
const setPaymentType$ = itemOrderOptions$.pipe(
@@ -576,6 +672,7 @@ export class DomainCheckoutService {
mergeMap((_) => refreshShoppingCart$.pipe(tap(console.log.bind(window, 'refreshShoppingCart$')))),
mergeMap((_) => setSpecialComment$.pipe(tap(console.log.bind(window, 'setSpecialComment$')))),
mergeMap((_) => refreshCheckout$.pipe(tap(console.log.bind(window, 'refreshCheckout$')))),
mergeMap((_) => checkAvailabilities$.pipe(tap(console.log.bind(window, 'checkAvailabilities$')))),
mergeMap((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$')))),
mergeMap((_) => setBuyer$.pipe(tap(console.log.bind(window, 'setBuyer$')))),
mergeMap((_) => setNotificationChannels$.pipe(tap(console.log.bind(window, 'setNotificationChannels$')))),
@@ -731,6 +828,15 @@ export class DomainCheckoutService {
this.store.dispatch(DomainCheckoutActions.setCustomerFeatures({ processId, customerFeatures }));
}
setOlaErrors({ processId, errorIds }: { processId: number; errorIds: number[] }) {
this.store.dispatch(
DomainCheckoutActions.setOlaError({
processId,
olaErrorIds: errorIds,
})
);
}
getCustomerFeatures({ processId }: { processId: number }): Observable<{ [key: string]: string }> {
return this.store.select(DomainCheckoutSelectors.selectCustomerFeaturesByProcessId, { processId });
}

View File

@@ -12,4 +12,5 @@ export interface CheckoutEntity {
orders: DisplayOrderDTO[];
specialComment: string;
notificationChannels: NotificationChannel;
olaErrorIds: number[];
}

View File

@@ -57,3 +57,5 @@ export const setBuyer = createAction(`${prefix} Set Buyer`, props<{ processId: n
export const setPayer = createAction(`${prefix} Set Payer`, props<{ processId: number; payer: PayerDTO }>());
export const setSpecialComment = createAction(`${prefix} Set Agent Comment`, props<{ processId: number; agentComment: string }>());
export const setOlaError = createAction(`${prefix} Set Ola Error`, props<{ processId: number; olaErrorIds: number[] }>());

View File

@@ -72,7 +72,12 @@ const _domainCheckoutReducer = createReducer(
return storeCheckoutAdapter.setOne(entity, s);
}),
on(DomainCheckoutActions.removeProcess, (s, { processId }) => storeCheckoutAdapter.removeOne(processId, s)),
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({ ...s, orders }))
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({ ...s, orders })),
on(DomainCheckoutActions.setOlaError, (s, { processId, olaErrorIds }) => {
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
entity.olaErrorIds = olaErrorIds;
return storeCheckoutAdapter.setOne(entity, s);
})
);
export function domainCheckoutReducer(state, action) {
@@ -94,6 +99,7 @@ function getOrCreateCheckoutEntity({ entities, processId }: { entities: Dictiona
buyer: undefined,
specialComment: '',
notificationChannels: 0,
olaErrorIds: [],
};
}

View File

@@ -56,3 +56,8 @@ export const selectBuyerCommunicationDetails = createSelector(
);
export const selectOrders = createSelector(storeFeatureSelector, (s) => s.orders);
export const selectOlaErrorsByProcessId = createSelector(
selectEntities,
(entities: Dictionary<CheckoutEntity>, { processId }: { processId: number }) => entities[processId]?.olaErrorIds
);

View File

@@ -50,7 +50,7 @@ describe('ReorderModalComponent', () => {
mockProvider(DomainCheckoutService),
mockProvider(DomainAvailabilityService, {
getCurrentBranch: jasmine.createSpy().and.returnValue(of({})),
getStoreAvailabilities: jasmine.createSpy().and.returnValue(of(storeAvailabilitiesMock)),
getPickUpAvailabilities: jasmine.createSpy().and.returnValue(of(storeAvailabilitiesMock)),
getTakeAwayAvailability: jasmine.createSpy().and.returnValue(of(takeAwayAvailabiltyMock)),
}),
mockProvider(DomainOmsService),
@@ -76,11 +76,7 @@ describe('ReorderModalComponent', () => {
spectator.component.patchState({ orderItem });
await spectator.detectComponentChanges();
expect(domainAvailabilityServiceMock.getStoreAvailabilities).toHaveBeenCalledWith({
item: { ean: orderItem.product.ean, itemId: +orderItem.product.catalogProductNumber, price: orderItem.retailPrice },
quantity: 1,
branch: {},
});
expect(domainAvailabilityServiceMock.getPickUpAvailabilities).toHaveBeenCalled();
});
it('should set checkedAvailability to the preferred availability', async () => {

View File

@@ -69,11 +69,15 @@ export class ReorderModalComponent extends ComponentStore<GoodsInListReorderModa
readonly storeAvailabilities$ = combineLatest([this.orderItem$, this.currentBranch$]).pipe(
switchMap(([item, branch]) =>
this.domainAvailabilityService
.getStoreAvailabilities({
item: { ean: item.product.ean, itemId: +item.product?.catalogProductNumber, price: item.retailPrice },
branch,
quantity: item.quantity,
})
.getPickUpAvailabilities([
{
qty: item.quantity,
ean: item.product.ean,
itemId: item.product?.catalogProductNumber,
shopId: branch.id,
price: item.retailPrice,
},
])
.pipe(
catchError(() => {
this.patchState({ storeAvailabilityError: true });

View File

@@ -13,6 +13,7 @@ export interface ArticleSearchState {
searchState: '' | 'fetching' | 'empty' | 'error';
items: ItemDTO[];
hits: number;
selectedItemIds: number[];
}
@Injectable()
@@ -48,6 +49,12 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
searchboxHint$ = this.select((s) => (s.searchState === 'empty' ? 'Keine Suchergebnisse' : undefined));
selectedItemIds$ = this.select((s) => s.selectedItemIds);
get selectedItemIds() {
return this.get((s) => s.selectedItemIds);
}
hits$ = this.select((s) => s.hits);
get hits() {
@@ -61,6 +68,7 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
items: [],
processId: 0,
searchState: '',
selectedItemIds: [],
});
this.setDefaultFilter();
}
@@ -94,6 +102,20 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
this.patchState({ filter });
}
setSelected({ selected, itemId }: { selected: boolean; itemId: number }) {
const included = this.selectedItemIds.includes(itemId);
if (!included && selected) {
this.patchState({
selectedItemIds: [...this.selectedItemIds, itemId],
});
} else if (included && !selected) {
this.patchState({
selectedItemIds: this.selectedItemIds.filter((id) => id !== itemId),
});
}
}
search = this.effect((options$: Observable<{ clear?: boolean }>) =>
options$.pipe(
tap((options) => {

View File

@@ -0,0 +1,10 @@
<p class="can-add-message" *ngIf="ref.data.canAddMessage">{{ ref.data.canAddMessage }}</p>
<div class="actions">
<button (click)="continue()" class="cta cta-action-secondary">
Weiter Einkaufen
</button>
<button (click)="toCart()" class="cta cta-action-primary">
Zum Warenkorb
</button>
</div>

View File

@@ -0,0 +1,23 @@
.can-add-message {
@apply text-center text-xl text-dark-goldenrod font-semibold;
}
.actions {
@apply flex flex-row justify-center items-center pb-4;
.cta {
@apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap no-underline;
&:disabled {
@apply bg-inactive-branch border-inactive-branch text-white;
}
}
.cta-action-primary {
@apply bg-brand text-white ml-2;
}
.cta-action-secondary {
@apply bg-white text-brand mr-2;
}
}

View File

@@ -0,0 +1,22 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { UiModalRef } from '@ui/modal';
@Component({
selector: 'added-to-cart-modal',
templateUrl: 'added-to-cart-modal.component.html',
styleUrls: ['added-to-cart-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddedToCartModalComponent {
constructor(public ref: UiModalRef<any, any>, private readonly _router: Router) {}
continue() {
this._router.navigate(['/product/search']);
this.ref.close();
}
toCart() {
this._router.navigate(['/cart/review']);
this.ref.close();
}
}

View File

@@ -29,6 +29,10 @@
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
<div *ngIf="selectable" class="item-data-selector">
<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-ssc" [class.xs]="item?.catalogAvailability?.sscText?.length >= 60">

View File

@@ -5,6 +5,7 @@
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-title item-price'
'item-thumbnail item-title item-data-selector'
'item-thumbnail item-format item-stock'
'item-thumbnail item-misc item-ssc';
}
@@ -96,6 +97,15 @@
@apply font-bold text-xs;
}
.item-data-selector {
@apply w-full flex justify-end;
grid-area: item-data-selector;
}
ui-select-bullet {
@apply p-4 -m-4 z-dropdown;
}
@media (min-width: 1025px) {
.item-contributors {
max-width: 780px;

View File

@@ -1,7 +1,16 @@
import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core';
import { ComponentStore } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { ArticleSearchService } from '../article-search.store';
export interface SearchResultItemComponentState {
item?: ItemDTO;
selected: boolean;
selectable: boolean;
}
@Component({
selector: 'search-result-item',
@@ -9,9 +18,42 @@ import { DateAdapter } from '@ui/common';
styleUrls: ['search-result-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchResultItemComponent {
export class SearchResultItemComponent extends ComponentStore<SearchResultItemComponentState> {
@Input()
item: ItemDTO;
get item() {
return this.get((s) => s.item);
}
set item(item: ItemDTO) {
if (!isEqual(this.item, item)) {
this.patchState({ item });
}
}
readonly item$ = this.select((s) => s.item);
@Input()
get selected() {
return this.get((s) => s.selected);
}
set selected(selected: boolean) {
if (this.selected !== selected) {
this.patchState({ selected });
}
}
readonly selected$ = this.select((s) => s.selected);
@Input()
get selectable() {
return this.get((s) => s.selectable);
}
set selectable(selectable: boolean) {
if (this.selectable !== selectable) {
this.patchState({ selectable });
}
}
@Output()
selectedChange = new EventEmitter<boolean>();
get contributors() {
return this.item?.product?.contributors?.split(';').map((val) => val.trim());
@@ -30,5 +72,14 @@ export class SearchResultItemComponent {
return '';
}
constructor(private _dateAdapter: DateAdapter, private _datePipe: DatePipe) {}
constructor(private _dateAdapter: DateAdapter, private _datePipe: DatePipe, private _articleSearchService: ArticleSearchService) {
super({
selected: false,
selectable: false,
});
}
setSelected(selected: boolean) {
this._articleSearchService.setSelected({ selected, itemId: this.item?.id });
}
}

View File

@@ -12,7 +12,22 @@
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<div class="product-list-result" *cdkVirtualFor="let item of results$ | async" cdkVirtualForTrackBy="trackByItemId">
<search-result-item [item]="item"></search-result-item>
<search-result-item
[selected]="item | searchResultSelected: searchService.selectedItemIds"
[selectable]="isSelectable(item)"
[item]="item"
></search-result-item>
</div>
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport>
<div class="actions">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary"
(click)="addSelectedItemsToCart()"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>
</div>

View File

@@ -26,3 +26,21 @@
@apply mx-auto;
}
}
.actions {
@apply fixed bottom-28 inline-grid grid-flow-col gap-7;
left: 50%;
transform: translateX(-50%);
.cta-cart {
@apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap no-underline;
&:disabled {
@apply bg-inactive-branch border-inactive-branch text-white;
}
}
.cta-action-primary {
@apply bg-brand text-white;
}
}

View File

@@ -1,15 +1,20 @@
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild } from '@angular/core';
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewChildren, QueryList } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainCheckoutService } from '@domain/checkout';
import { ItemDTO } from '@swagger/cat';
import { AddToShoppingCartDTO } from '@swagger/checkout';
import { UiFilter } from '@ui/filter';
import { UiErrorModalComponent, UiModalRef, UiModalService } from '@ui/modal';
import { CacheService } from 'apps/core/cache/src/public-api';
import { isEqual } from 'lodash';
import { combineLatest, Subscription } from 'rxjs';
import { debounceTime, first } from 'rxjs/operators';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, tap } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { SearchResultItemComponent } from './search-result-item.component';
@Component({
selector: 'page-search-results',
@@ -18,6 +23,7 @@ import { ArticleSearchService } from '../article-search.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
@ViewChildren(SearchResultItemComponent) listItems: QueryList<SearchResultItemComponent>;
@ViewChild('scrollContainer', { static: true })
scrollContainer: CdkVirtualScrollViewport;
@@ -27,16 +33,28 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
filter$ = this.searchService.filter$;
trackByItemId = (item: ItemDTO) => item.id;
selectedItemIds$ = this.searchService.selectedItemIds$;
selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe(
map(([items, selectedItemIds]) => {
return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId));
})
);
loading$ = new BehaviorSubject<boolean>(false);
private subscriptions = new Subscription();
trackByItemId = (item: ItemDTO) => item.id;
constructor(
private searchService: ArticleSearchService,
public searchService: ArticleSearchService,
private route: ActivatedRoute,
private application: ApplicationService,
private breadcrumb: BreadcrumbService,
private cache: CacheService
private cache: CacheService,
private _uiModal: UiModalService,
private _checkoutService: DomainCheckoutService
) {}
ngOnInit() {
@@ -68,6 +86,12 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.search();
} else {
this.scrollTop(Number(queryParams.scroll_position ?? 0));
const selectedItemIds: Array<string> = queryParams?.selected_item_ids?.split(',') ?? [];
for (const id of selectedItemIds) {
if (id) {
this.searchService.setSelected({ selected: true, itemId: Number(id) });
}
}
}
}
@@ -83,6 +107,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
this.cacheCurrentData(this.searchService.processId, this.searchService.filter.getQueryParams());
this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
this.unselectAll();
}
search() {
@@ -119,6 +144,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
queryParams: Record<string, string> = this.searchService.filter?.getQueryParams()
) {
const scroll_position = this.scrollContainer.measureScrollOffset('top');
const selected_item_ids = this.searchService?.selectedItemIds?.toString();
if (queryParams) {
const crumbs = await this.breadcrumb
@@ -127,7 +153,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
.toPromise();
const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel';
const params = { ...queryParams, scroll_position };
const params = { ...queryParams, scroll_position, selected_item_ids };
for (const crumb of crumbs) {
this.breadcrumb.patchBreadcrumb(crumb.id, {
@@ -181,6 +207,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };
delete clean['scroll_position'];
delete clean['selected_item_ids'];
for (const key in clean) {
if (Object.prototype.hasOwnProperty.call(clean, key)) {
@@ -192,4 +219,108 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
return clean;
}
isSelectable(item: ItemDTO) {
// Zeige Select Radio Button nicht an wenn Item Archivartikel oder Fortsetzungsartikel ist
const isArchiv = item?.catalogAvailability?.status === 1;
const isFortsetzung = item?.features?.find((i) => i?.key === 'PFO');
return !(isArchiv || isFortsetzung);
}
unselectAll() {
this.listItems.forEach((listItem) => this.searchService.setSelected({ selected: false, itemId: listItem.item.id }));
this.searchService.patchState({ selectedItemIds: [] });
}
async addSelectedItemsToCart() {
this.loading$.next(true);
const selectedItems = await this.selectedItems$.pipe(first()).toPromise();
const items: AddToShoppingCartDTO[] = [];
const canAddItemsPayload = [];
for (const item of selectedItems) {
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL';
const shoppingCartItem: AddToShoppingCartDTO = {
quantity: 1,
availability: {
availabilityType: item?.catalogAvailability?.status,
price: item?.catalogAvailability?.price,
supplierProductNumber: item?.ids?.dig ? String(item.ids?.dig) : item?.product?.supplierProductNumber,
},
product: {
catalogProductNumber: String(item?.id),
...item?.product,
},
promotion: { points: item?.promoPoints },
};
if (isDownload) {
shoppingCartItem.destination = { data: { target: 16 } };
canAddItemsPayload.push({
availabilities: [{ ...item.catalogAvailability, format: 'DL' }],
id: item.product.catalogProductNumber,
});
}
items.push(shoppingCartItem);
}
if (canAddItemsPayload.length > 0) {
try {
const response = await this._checkoutService
.canAddItems({
processId: this.application.activatedProcessId,
payload: canAddItemsPayload,
orderType: 'Download',
})
.pipe(first())
.toPromise();
if (response) {
const cantAdd = (response as any)?.filter((r) => r.status >= 2);
if (cantAdd?.length > 0) {
this.openModal({ itemLength: cantAdd.length, canAddMessage: cantAdd[0].message });
return;
}
}
} catch (error) {
this.openModal({ itemLength: selectedItems?.length, error });
console.error(error);
return;
}
}
try {
await this._checkoutService
.addItemToShoppingCart({
processId: this.application.activatedProcessId,
items,
})
.toPromise();
this.openModal({ itemLength: selectedItems?.length });
} catch (error) {
this.openModal({ itemLength: selectedItems?.length, error });
console.error(error);
}
}
openModal({ itemLength, canAddMessage, error }: { itemLength: number; canAddMessage?: string; error?: Error }) {
const modal = this._uiModal.open({
title:
!error && !canAddMessage
? `${itemLength} Artikel ${itemLength > 1 ? 'wurden' : 'wurde'} in den Warenkorb gelegt`
: `Artikel ${itemLength > 1 ? 'konnten' : 'konnte'} nicht in den Warenkorb gelegt werden`,
content: !error ? AddedToCartModalComponent : UiErrorModalComponent,
data: error ? error : { canAddMessage },
config: { showScrollbarY: false },
});
this.subscriptions.add(
modal.afterClosed$.subscribe(() => {
if (!error) {
this.unselectAll();
}
this.loading$.next(false);
})
);
}
}

View File

@@ -1,20 +1,43 @@
import { ScrollingModule } from '@angular/cdk/scrolling';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router';
import { DomainCatalogModule } from '@domain/catalog';
import { UiCommonModule } from '@ui/common';
import { UiIconModule } from '@ui/icon';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiOrderByFilterModule } from 'apps/ui/filter/src/lib/next/order-by-filter/order-by-filter.module';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { StockInfosPipe } from './order-by-filter/stick-infos.pipe';
import { SearchResultItemLoadingComponent } from './search-result-item-loading.component';
import { SearchResultItemComponent } from './search-result-item.component';
import { ArticleSearchResultsComponent } from './search-results.component';
import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe';
@NgModule({
imports: [CommonModule, RouterModule, DomainCatalogModule, UiCommonModule, UiIconModule, UiOrderByFilterModule, ScrollingModule],
imports: [
CommonModule,
FormsModule,
RouterModule,
DomainCatalogModule,
UiCommonModule,
UiIconModule,
UiSelectBulletModule,
UiSpinnerModule,
UiOrderByFilterModule,
ScrollingModule,
],
exports: [ArticleSearchResultsComponent, SearchResultItemComponent],
declarations: [ArticleSearchResultsComponent, SearchResultItemComponent, StockInfosPipe, SearchResultItemLoadingComponent],
declarations: [
ArticleSearchResultsComponent,
SearchResultItemComponent,
StockInfosPipe,
SearchResultItemLoadingComponent,
SearchResultSelectedPipe,
AddedToCartModalComponent,
],
providers: [],
})
export class SearchResultsModule {}

View File

@@ -0,0 +1,11 @@
import { Pipe, PipeTransform } from '@angular/core';
import { ItemDTO } from '@swagger/cat';
@Pipe({
name: 'searchResultSelected',
})
export class SearchResultSelectedPipe implements PipeTransform {
transform(item: ItemDTO, selectedItemIds: number[]): any {
return selectedItemIds?.includes(item?.id);
}
}

View File

@@ -30,21 +30,33 @@
</div>
<h1 class="header">Warenkorb</h1>
<h5 class="sub-header">Überprüfen Sie die Details.</h5>
<hr />
<div class="row">
<div class="label">
Rechnungsadresse
<ng-container *ngIf="payer$ | async">
<hr />
<div class="row">
<ng-container *ngIf="showBillingAddress$ | async; else customerName">
<div class="label">
Rechnungsadresse
</div>
<div class="value">
{{ payer$ | async | payerAddress | trim: 55 }}
</div>
</ng-container>
<ng-template #customerName>
<div class="label">
Name, Vorname
</div>
<div class="value" *ngIf="payer$ | async; let payer">{{ payer.lastName }}, {{ payer.firstName }}</div>
</ng-template>
<div class="grow"></div>
<div>
<button *ngIf="payer$ | async" (click)="changeAddress()" class="cta-edit">
Ändern
</button>
</div>
</div>
<div class="value">
{{ payer$ | async | payerAddress | trim: 55 }}
</div>
<div class="grow"></div>
<div>
<button (click)="changeAddress()" class="cta-edit">
Ändern
</button>
</div>
</div>
</ng-container>
<hr />
<ng-container *ngIf="showNotificationChannels$ | async">
<div class="row">
@@ -57,32 +69,41 @@
(ngModelChange)="setAgentComment($event)"
(isDirtyChange)="specialCommentIsDirty = $event"
></page-special-comment>
<hr />
<ng-container *ngFor="let group of groupedItems$ | async; let lastGroup = last">
<div class="row item-group-header">
<ui-icon
*ngIf="group.orderType !== 'Dummy'"
class="icon-order-type"
[size]="group.orderType === 'B2B-Versand' ? '50px' : '25px'"
[icon]="
group.orderType === 'Abholung'
? 'box_out'
: group.orderType === 'Versand'
? 'truck'
: group.orderType === 'Rücklage'
? 'shopping_bag'
: group.orderType === 'B2B-Versand'
? 'truck_b2b'
: group.orderType === 'Download'
? 'download'
: 'truck'
"
></ui-icon>
<div class="label" [class.dummy]="group.orderType === 'Dummy'">
{{ group.orderType !== 'Dummy' ? group.orderType : 'Manuelle Anlage / Dummy Bestellung' }}
<button *ngIf="group.orderType === 'Dummy'" class="cta-secondary" (click)="openDummyModal()">Hinzufügen</button>
<ng-container *ngIf="group?.orderType !== undefined">
<hr />
<div class="row item-group-header">
<ui-icon
*ngIf="group.orderType !== 'Dummy'"
class="icon-order-type"
[size]="group.orderType === 'B2B-Versand' ? '50px' : '25px'"
[icon]="
group.orderType === 'Abholung'
? 'box_out'
: group.orderType === 'Versand'
? 'truck'
: group.orderType === 'Rücklage'
? 'shopping_bag'
: group.orderType === 'B2B-Versand'
? 'truck_b2b'
: group.orderType === 'Download'
? 'download'
: 'truck'
"
></ui-icon>
<div class="label" [class.dummy]="group.orderType === 'Dummy'">
{{ group.orderType !== 'Dummy' ? group.orderType : 'Manuelle Anlage / Dummy Bestellung' }}
<button *ngIf="group.orderType === 'Dummy'" class="cta-secondary" (click)="openDummyModal()">Hinzufügen</button>
</div>
<div class="grow"></div>
<div *ngIf="group.orderType !== 'Download' && group.orderType !== 'Dummy'">
<button class="cta-edit" (click)="showPurchasingListModal(group.items)">
Ändern
</button>
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="group.orderType === 'Versand' || group.orderType === 'B2B-Versand' || group.orderType === 'DIG-Versand'">
<hr />
<div class="row">
@@ -102,7 +123,9 @@
</ng-container>
<hr />
<ng-container *ngFor="let item of group.items; let lastItem = last; let i = index">
<ng-container *ngIf="item.features?.orderType === 'Abholung' || item.features?.orderType === 'Rücklage'">
<ng-container
*ngIf="group?.orderType !== undefined && (item.features?.orderType === 'Abholung' || item.features?.orderType === 'Rücklage')"
>
<ng-container *ngIf="item?.destination?.data?.targetBranch?.data; let targetBranch">
<ng-container *ngIf="i === 0 || targetBranch.id !== group.items[i - 1].destination?.data?.targetBranch?.data.id">
<div class="row">
@@ -114,78 +137,19 @@
</ng-container>
</ng-container>
<div class="row product-container">
<div class="product-image">
<img [src]="item?.product?.ean | productImage: 50:50:true" alt="product-image" />
</div>
<div class="product-name">
<a [routerLink]="['/product', 'details', 'ean', item?.product?.ean]">{{ item?.product?.name }}</a>
</div>
<div
class="product-misc-container"
[style]="group.orderType !== 'Rücklage' && group.orderType !== 'Download' ? 'margin-top: 1.25rem' : ''"
>
<div class="product-misc">
<img class="book-icon" [src]="'/assets/images/Icon_' + item?.product?.format + '.svg'" alt="book-icon" />
{{ item?.product?.manufacturer }}
<page-shopping-cart-item
(changeItem)="changeItem($event)"
(changeDummyItem)="changeDummyItem($event)"
(changeQuantity)="updateItemQuantity($event)"
[quantityError]="(quantityError$ | async)[item.product.catalogProductNumber]"
[item]="item"
[orderType]="group?.orderType"
[loadingOnItemChangeById]="loadingOnItemChangeById$ | async"
[loadingOnQuantityChangeById]="loadingOnQuantityChangeById$ | async"
></page-shopping-cart-item>
<ng-container *ngIf="item?.product?.contributors">
{{ ' | ' + item?.product?.contributors | trim: 30 }}
</ng-container>
</div>
<div
*ngIf="group.orderType !== 'Rücklage' && group.orderType !== 'Download' && group.orderType !== 'Dummy'"
class="product-delivery"
>
{{ group.orderType === 'DIG-Versand' ? 'Versand' : group.orderType }} {{ group.orderType === 'Abholung' ? 'ab' : '' }}
{{ item?.availability?.estimatedShippingDate | date }}
</div>
<div *ngIf="group.orderType === 'Dummy'" class="product-delivery">
Abholung {{ group.orderType === 'Dummy' ? 'ab' : '' }}
{{ item?.availability?.estimatedShippingDate ? (item?.availability?.estimatedShippingDate | date) : '-' }}
</div>
</div>
<div class="product-price">
{{ item?.unitPrice?.value?.value | currency: item?.unitPrice?.value?.currency:'code' }}
</div>
<div class="product-quantity">
<ui-quantity-dropdown
*ngIf="group.orderType !== 'Dummy'; else quantityDummy"
[ngModel]="item?.quantity"
(ngModelChange)="updateItemQuantity(item, $event)"
[showSpinner]="showQuantityControlSpinnerItemId === item.id"
>
</ui-quantity-dropdown>
<ng-template #quantityDummy>
{{ item?.quantity }}
</ng-template>
</div>
<div class="product-actions">
<button
*ngIf="group.orderType === 'Dummy'"
class="cta-edit"
(click)="changeDummyItem(item)"
[disabled]="showChangeButtonSpinnerItemId"
>
<ui-spinner [show]="showChangeButtonSpinnerItemId === item.id">
Ändern
</ui-spinner>
</button>
<button
*ngIf="group.orderType !== 'Download' && group.orderType !== 'Dummy'"
class="cta-edit"
(click)="changeItem(item)"
[disabled]="showChangeButtonSpinnerItemId"
>
<ui-spinner [show]="showChangeButtonSpinnerItemId === item.id">
Ändern
</ui-spinner>
</button>
</div>
</div>
<hr *ngIf="!lastItem" />
</ng-container>
<hr *ngIf="!lastGroup" />
</ng-container>
</div>
<div class="card footer row">
@@ -202,14 +166,9 @@
</strong>
<span class="shipping-cost-info">ohne Versandkosten</span>
</div>
<button
class="cta-primary"
(click)="order()"
[disabled]="showOrderButtonSpinner || specialCommentIsDirty"
[class.special-comment-dirty]="specialCommentIsDirty"
>
<button class="cta-primary" (click)="order()" [disabled]="showOrderButtonSpinner">
<ui-spinner [show]="showOrderButtonSpinner">
Bestellen
{{ primaryCtaLabel$ | async }}
</ui-spinner>
</button>
</div>

View File

@@ -9,6 +9,10 @@
max-height: calc(100vh - 390px);
}
button {
@apply p-0;
}
.card {
@apply bg-white rounded-card shadow-card;
}
@@ -54,26 +58,26 @@
}
.cta-print-wrapper {
@apply pl-4 pr-2 pt-4 text-right;
@apply pl-4 pr-4 pt-4 text-right;
}
.cta-print,
.cta-edit {
@apply bg-transparent text-brand font-bold text-xl outline-none border-none;
@apply bg-transparent text-brand font-bold text-lg outline-none border-none;
}
.cta-primary {
@apply bg-brand text-white font-bold text-lg outline-none border-brand border-solid border-2 rounded-full px-6 py-3;
&:disabled {
@apply bg-inactive-customer border-none;
}
}
.cta-secondary {
@apply bg-white text-brand border-none font-bold text-lg outline-none px-6 py-3 mt-4;
}
.cta-order.special-comment-dirty {
@apply bg-active-branch text-white;
}
.cta-edit {
@apply text-lg;
}
@@ -129,61 +133,22 @@ hr {
@apply text-regular overflow-hidden overflow-ellipsis ml-4;
}
.product-name {
@apply overflow-ellipsis whitespace-nowrap overflow-hidden text-active-customer text-base font-bold;
width: 130px;
a {
@apply text-active-customer no-underline;
}
}
.book-icon {
@apply mr-2 w-px-15;
height: 18px;
}
.product-misc-container {
@apply flex flex-col;
width: 210px;
}
.product-misc {
@apply overflow-ellipsis whitespace-nowrap overflow-hidden;
}
.product-delivery {
@apply font-bold;
}
.product-price {
@apply font-bold;
}
.product-image {
@apply w-px-50 h-px-50 text-center overflow-hidden;
}
.product-container {
@apply py-0 px-4 flex flex-row justify-between;
height: 80px;
}
.product-actions {
min-width: 80px;
}
.footer {
@apply absolute bottom-0 left-0 right-0 p-7;
box-shadow: 0px -2px 24px 0px #dce2e9;
}
.total-container {
@apply flex flex-col;
@apply flex flex-col ml-4;
}
.total-cta-container {
@apply flex flex-row;
@apply flex flex-row whitespace-nowrap;
}
.shipping-cost-info {
@@ -197,13 +162,3 @@ hr {
.total-item-reading-points {
@apply text-base font-bold text-ucla-blue;
}
@media (min-width: 1025px) {
.product-misc-container {
width: 300px;
}
.product-name {
width: 225px;
}
}

View File

@@ -3,19 +3,27 @@ import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, DestinationDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { AvailabilityDTO, DestinationDTO, ShoppingCartDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer';
import { first, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
import { first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
import { SsoService } from 'sso';
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from '../modals/purchasing-options-modal';
import { PurchasingOptions } from '../modals/purchasing-options-modal/purchasing-options-modal.store';
import { Subject, NEVER } from 'rxjs';
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component';
import { ResponseArgsOfItemDTO } from '@swagger/cat';
import { PurchasingOptionsListModalComponent } from '../modals/purchasing-options-list-modal';
import { PurchasingOptionsListModalData } from '../modals/purchasing-options-list-modal/purchasing-options-list-modal.data';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
export interface CheckoutReviewComponentState {
shoppingCart: ShoppingCartDTO;
shoppingCartItems: ShoppingCartItemDTO[];
}
@Component({
selector: 'page-checkout-review',
@@ -23,18 +31,29 @@ import { ResponseArgsOfItemDTO } from '@swagger/cat';
styleUrls: ['checkout-review.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReviewComponent implements OnInit {
export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewComponentState> implements OnInit {
private _orderCompleted = new Subject<void>();
shoppingCart$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
switchMap((processId) => this.domainCheckoutService.getShoppingCart({ processId, latest: true })),
shareReplay()
);
get shoppingCart() {
return this.get((s) => s.shoppingCart);
}
set shoppingCart(shoppingCart: ShoppingCartDTO) {
this.patchState({ shoppingCart });
}
readonly shoppingCart$ = this.select((s) => s.shoppingCart);
get shoppingCartItems() {
return this.get((s) => s.shoppingCartItems);
}
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
this.patchState({ shoppingCartItems });
}
readonly shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
payer$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
switchMap((processId) => this.domainCheckoutService.getPayer({ processId }))
switchMap((processId) => this.domainCheckoutService.getPayer({ processId })),
shareReplay()
);
shippingAddress$ = this.applicationService.activatedProcessId$.pipe(
@@ -42,12 +61,12 @@ export class CheckoutReviewComponent implements OnInit {
switchMap((processId) => this.domainCheckoutService.getShippingAddress({ processId }))
);
items$ = this.shoppingCart$.pipe(
shoppingCartItemsWithoutOrderType$ = this.shoppingCartItems$.pipe(
takeUntil(this._orderCompleted),
map((shoppingCart) => shoppingCart?.items?.map((item) => item.data) || [])
map((items) => items?.filter((item) => item?.features?.orderType === undefined))
);
groupedItems$ = this.items$.pipe(
groupedItems$ = this.shoppingCartItems$.pipe(
takeUntil(this._orderCompleted),
map((items) =>
items.reduce((grouped, item) => {
@@ -65,15 +84,18 @@ export class CheckoutReviewComponent implements OnInit {
orderType:
item?.availability?.supplyChannel === 'MANUALLY'
? 'Dummy'
: item.features.orderType === 'DIG-Versand'
: item?.features?.orderType === 'DIG-Versand'
? 'Versand'
: item.features.orderType,
destination: item.destination?.data,
: item?.features?.orderType,
destination: item?.destination?.data,
items: [],
};
}
group.items = [...group.items, item]?.sort((a, b) => a.destination?.data?.targetBranch?.id - b.destination?.data?.targetBranch?.id);
group.items = [...group.items, item]?.sort(
(a, b) =>
a.destination?.data?.targetBranch?.id - b.destination?.data?.targetBranch?.id || a.product?.name.localeCompare(b.product?.name)
);
if (index !== -1) {
grouped[index] = group;
@@ -81,26 +103,21 @@ export class CheckoutReviewComponent implements OnInit {
grouped.push(group);
}
return [...grouped];
return [...grouped].sort((a, b) => (a?.orderType === undefined ? -1 : b?.orderType === undefined ? 1 : 0));
}, [] as { orderType: string; destination: DestinationDTO; items: ShoppingCartItemDTO[] }[])
)
);
hasItemsForDelivery$ = this.items$.pipe(
takeUntil(this._orderCompleted),
map((items) => items.some((item) => item.features?.orderType === 'Versand' || item.features?.orderType === 'B2B-Versand'))
);
specialComment$ = this.applicationService.activatedProcessId$.pipe(
switchMap((processId) => this.domainCheckoutService.getSpecialComment({ processId }))
);
totalItemCount$ = this.items$.pipe(
totalItemCount$ = this.shoppingCartItems$.pipe(
takeUntil(this._orderCompleted),
map((items) => items.reduce((total, item) => total + item.quantity, 0))
);
totalReadingPoints$ = this.items$.pipe(
totalReadingPoints$ = this.shoppingCartItems$.pipe(
switchMap((displayOrders) => {
if (displayOrders.length === 0) {
return NEVER;
@@ -133,14 +150,56 @@ export class CheckoutReviewComponent implements OnInit {
switchMap((processId) => this.domainCheckoutService.getCustomerFeatures({ processId }))
);
showNotificationChannels$ = this.items$.pipe(
showBillingAddress$ = this.shoppingCartItems$.pipe(
takeUntil(this._orderCompleted),
map((items) => items.some((item) => item.features?.orderType === 'Rücklage' || item.features?.orderType === 'Abholung'))
withLatestFrom(this.customerFeatures$),
map(
([items, customerFeatures]) =>
items.some(
(item) =>
item.features?.orderType === 'Versand' ||
item.features?.orderType === 'B2B-Versand' ||
item.features?.orderType === 'DIG-Versand'
) || !!customerFeatures?.b2b
)
);
showQuantityControlSpinnerItemId: number;
showNotificationChannels$ = combineLatest([this.shoppingCartItems$, this.payer$]).pipe(
takeUntil(this._orderCompleted),
map(
([items, payer]) =>
!!payer && items.some((item) => item.features?.orderType === 'Rücklage' || item.features?.orderType === 'Abholung')
)
);
quantityError$ = new BehaviorSubject<{ [key: string]: string }>({});
primaryCtaLabel$ = combineLatest([this.payer$, this.shoppingCartItemsWithoutOrderType$]).pipe(
map(([payer, shoppingCartItemsWithoutOrderType]) => {
if (shoppingCartItemsWithoutOrderType?.length > 0) {
return 'Kaufoptionen';
}
if (!payer) {
return 'Weiter';
}
return 'Bestellen';
})
);
primaryCtaDisabled$ = this.quantityError$.pipe(
map((quantityError) => {
for (const key in quantityError) {
if (Object.prototype.hasOwnProperty.call(quantityError, key)) {
return true;
}
}
return this.showOrderButtonSpinner;
})
);
loadingOnItemChangeById$ = new Subject<number>();
loadingOnQuantityChangeById$ = new Subject<number>();
showOrderButtonSpinner: boolean;
showChangeButtonSpinnerItemId: number;
constructor(
private domainCheckoutService: DomainCheckoutService,
@@ -153,13 +212,54 @@ export class CheckoutReviewComponent implements OnInit {
private domainCatalogService: DomainCatalogService,
private breadcrumb: BreadcrumbService,
private domainPrinterService: DomainPrinterService
) {}
) {
super({
shoppingCart: undefined,
shoppingCartItems: [],
});
}
async ngOnInit() {
this.applicationService.activatedProcessId$.pipe(takeUntil(this._orderCompleted)).subscribe((_) => {
this.loadShoppingCart();
});
await this.removeBreadcrumbs();
await this.updateBreadcrumb();
}
loadShoppingCart = this.effect(($) =>
$.pipe(
withLatestFrom(this.applicationService.activatedProcessId$),
switchMap(([_, processId]) => {
return this.domainCheckoutService.getShoppingCart({ processId, latest: true }).pipe(
tapResponse(
(shoppingCart) => {
const shoppingCartItems = shoppingCart?.items?.map((item) => item.data) || [];
this.patchState({
shoppingCart,
shoppingCartItems,
});
this.checkQuantityErrors(shoppingCartItems);
},
(err) => {},
() => {}
)
);
})
)
);
checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) {
shoppingCartItems.forEach((item) => {
if (item.features?.orderType === 'Rücklage') {
this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock);
} else {
this.setQuantityError(item, item.availability, false);
}
});
}
async updateBreadcrumb() {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
@@ -187,7 +287,7 @@ export class CheckoutReviewComponent implements OnInit {
});
}
changeDummyItem(shoppingCartItem: ShoppingCartItemDTO) {
changeDummyItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
const data = {
price: shoppingCartItem?.availability?.price?.value?.value,
vat: shoppingCartItem?.availability?.price?.vat?.vatType,
@@ -202,8 +302,8 @@ export class CheckoutReviewComponent implements OnInit {
this.openDummyModal(data);
}
async changeItem(shoppingCartItem: ShoppingCartItemDTO) {
this.showChangeButtonSpinnerItemId = shoppingCartItem.id;
async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
this.loadingOnItemChangeById$.next(shoppingCartItem.id);
const quantity = shoppingCartItem.quantity;
@@ -329,11 +429,11 @@ export class CheckoutReviewComponent implements OnInit {
}
}
this.showChangeButtonSpinnerItemId = undefined;
this.loadingOnItemChangeById$.next(undefined);
this.cdr.markForCheck();
const itemId = Number(shoppingCartItem.product.catalogProductNumber);
this.uiModal.open({
const modal = this.uiModal.open({
content: PurchasingOptionsModalComponent,
data: {
availableOptions,
@@ -353,6 +453,10 @@ export class CheckoutReviewComponent implements OnInit {
availabilities,
} as PurchasingOptionsModalData,
});
modal.afterClosed$.pipe(takeUntil(this._orderCompleted)).subscribe(() => {
this.setQuantityError(shoppingCartItem, undefined, false);
});
}
setAgentComment(agentComment: string) {
@@ -374,8 +478,12 @@ export class CheckoutReviewComponent implements OnInit {
});
}
async updateItemQuantity(shoppingCartItem: ShoppingCartItemDTO, quantity: number) {
this.showQuantityControlSpinnerItemId = shoppingCartItem.id;
async updateItemQuantity({ shoppingCartItem, quantity }: { shoppingCartItem: ShoppingCartItemDTO; quantity: number }) {
if (quantity === shoppingCartItem.quantity) {
return;
}
this.loadingOnQuantityChangeById$.next(shoppingCartItem.id);
const shoppingCartItemPrice = shoppingCartItem?.availability?.price?.value?.value;
const orderType = shoppingCartItem?.features?.orderType;
@@ -397,6 +505,8 @@ export class CheckoutReviewComponent implements OnInit {
quantity,
})
.toPromise();
this.setQuantityError(shoppingCartItem, availability, availability?.inStock < quantity);
break;
case 'Abholung':
availability = await this.availabilityService
@@ -473,6 +583,7 @@ export class CheckoutReviewComponent implements OnInit {
},
})
.toPromise();
this.setQuantityError(shoppingCartItem, availability, false);
} else if (availability) {
await this.domainCheckoutService
.updateItemInShoppingCart({
@@ -485,9 +596,29 @@ export class CheckoutReviewComponent implements OnInit {
})
.toPromise();
} else {
// TODO: Set Prev Quantity
await this.domainCheckoutService
.updateItemInShoppingCart({
processId: this.applicationService.activatedProcessId,
shoppingCartItemId: shoppingCartItem.id,
update: {
quantity,
},
})
.toPromise();
}
this.loadingOnQuantityChangeById$.next(undefined);
}
setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) {
const quantityErrors: { [key: string]: string } = this.quantityError$.value;
if (error) {
quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`;
this.quantityError$.next({ ...quantityErrors });
} else {
delete quantityErrors[item.product.catalogProductNumber];
this.quantityError$.next({ ...quantityErrors });
}
this.showQuantityControlSpinnerItemId = undefined;
}
// Bei unbekannten Kunden und DIG Bestellung findet ein Vergleich der Preise statt
@@ -534,7 +665,28 @@ export class CheckoutReviewComponent implements OnInit {
}
}
async showPurchasingListModal(shoppingCartItems: ShoppingCartItemDTO[]) {
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
this.uiModal.open({
content: PurchasingOptionsListModalComponent,
title: 'Wie möchten Sie die Artikel erhalten?',
config: { showScrollbarY: false },
data: {
processId: this.applicationService.activatedProcessId,
shoppingCartItems: shoppingCartItems,
customerFeatures,
} as PurchasingOptionsListModalData,
});
}
async order() {
const shoppingCartItemsWithoutOrderType = await this.shoppingCartItemsWithoutOrderType$.pipe(first()).toPromise();
if (shoppingCartItemsWithoutOrderType?.length > 0) {
this.showPurchasingListModal(shoppingCartItemsWithoutOrderType);
return;
}
const processId = this.applicationService.activatedProcessId;
const customer = await this.domainCheckoutService.getBuyer({ processId }).pipe(first()).toPromise();
if (!customer) {

View File

@@ -17,10 +17,13 @@ import { NotificationEditComponent } from './notification-channels/notification-
import { UiCheckboxModule } from '@ui/checkbox';
import { SpecialCommentComponent } from './special-comment/special-comment.component';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { UiCommonModule } from '@ui/common';
import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-item.component';
@NgModule({
imports: [
CommonModule,
UiCommonModule,
RouterModule,
PageCheckoutPipeModule,
UiIconModule,
@@ -41,6 +44,7 @@ import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
NotificationEditComponent,
NotificationCheckboxComponent,
SpecialCommentComponent,
ShoppingCartItemComponent,
],
})
export class CheckoutReviewModule {}

View File

@@ -0,0 +1,97 @@
<div class="item-thumbnail">
<a [routerLink]="['/product', 'details', 'ean', item?.product?.ean]">
<img loading="lazy" *ngIf="item?.product?.ean | productImage; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.title" />
</a>
</div>
<div class="item-contributors">
<a
*ngFor="let contributor of contributors$ | async; let last = last"
[routerLink]="['/product/search/results']"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
</div>
<div
class="item-title"
[class.xl]="item?.product?.name?.length >= 35"
[class.lg]="item?.product?.name?.length >= 40"
[class.md]="item?.product?.name?.length >= 50"
[class.sm]="item?.product?.name?.length >= 60"
[class.xs]="item?.product?.name?.length >= 100"
>
<a [routerLink]="['/product', 'details', 'ean', item?.product?.ean]">{{ item?.product?.name }}</a>
</div>
<div class="item-format">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="item-info">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ item?.product?.publicationDate | date }}
<div class="item-date" *ngIf="orderType === 'Abholung'">Abholung ab {{ item?.availability?.estimatedShippingDate | date }}</div>
<div class="item-date" *ngIf="orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand'">
<ng-container *ngIf="item?.availability?.estimatedDelivery; else estimatedShippingDate">
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate> Versand {{ item?.availability?.estimatedShippingDate | date }} </ng-template>
</div>
<div class="item-availability-message" *ngIf="olaError$ | async">
Artikel nicht verfügbar
</div>
</div>
<div class="item-price-stock">
<div>{{ item?.availability?.price?.value?.value | currency: 'EUR':'code' }}</div>
<div>
<ui-quantity-dropdown
*ngIf="!(isDummy$ | async); else quantityDummy"
[ngModel]="item?.quantity"
(ngModelChange)="onChangeQuantity($event)"
[showSpinner]="(loadingOnQuantityChangeById$ | async) === item?.id"
[disabled]="(loadingOnItemChangeById$ | async) === item?.id"
[range]="quantityRange$ | async"
>
</ui-quantity-dropdown>
<ng-template #quantityDummy>
{{ item?.quantity }}
</ng-template>
</div>
<div class="quantity-error" *ngIf="quantityError">
{{ quantityError }}
</div>
</div>
<div class="actions" *ngIf="orderType !== 'Download'">
<button
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
(click)="onChangeItem()"
*ngIf="!(hasOrderType$ | async)"
>
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">
Auswählen
</ui-spinner>
</button>
<button
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
(click)="onChangeItem()"
*ngIf="(isDummy$ | async) || (hasOrderType$ | async)"
>
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">
Ändern
</ui-spinner>
</button>
</div>

View File

@@ -0,0 +1,158 @@
:host {
@apply text-black no-underline grid p-4;
grid-template-columns: 102px 50% auto;
grid-template-rows: auto;
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-title item-price-stock'
'item-thumbnail item-format item-price-stock'
'item-thumbnail item-info actions'
'item-thumbnail item-date actions'
'item-thumbnail item-ssc actions'
'item-thumbnail item-availability actions';
}
button {
@apply p-0;
}
.item-thumbnail {
grid-area: item-thumbnail;
width: 70px;
@apply mr-8;
img {
max-width: 100%;
max-height: 150px;
@apply rounded-card shadow-cta;
}
}
.item-contributors {
grid-area: item-contributors;
height: 22px;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
white-space: nowrap;
a {
@apply text-active-customer font-bold no-underline;
}
}
.item-title {
grid-area: item-title;
@apply font-bold text-lg mb-4;
max-height: 64px;
a {
@apply text-active-customer no-underline;
}
}
.item-title.xl {
@apply font-bold text-xl;
}
.item-title.lg {
@apply font-bold text-lg;
}
.item-title.md {
@apply font-bold text-base;
}
.item-title.sm {
@apply font-bold text-sm;
}
.item-title.xs {
@apply font-bold text-xs;
}
.item-format {
grid-area: item-format;
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
img {
@apply mr-2;
}
}
.item-price-stock {
grid-area: item-price-stock;
@apply font-bold text-xl text-right;
.quantity {
@apply flex flex-row justify-end items-center;
}
.quantity-error {
@apply text-dark-goldenrod font-bold text-sm whitespace-nowrap;
}
ui-quantity-dropdown {
@apply flex justify-end mt-2;
}
}
.item-stock {
grid-area: item-stock;
@apply flex flex-row justify-end items-baseline font-bold text-lg;
ui-icon {
@apply text-active-customer mr-2;
}
}
.item-info {
grid-area: item-info;
.item-date {
@apply font-bold;
}
.item-availability-message {
@apply text-dark-goldenrod font-bold text-sm whitespace-nowrap;
}
}
.item-availability {
@apply flex flex-row items-center mt-4;
grid-area: item-availability;
.fetching {
@apply w-52 h-px-20;
background-color: #e6eff9;
animation: load 0.75s linear infinite;
}
span {
@apply mr-4;
}
ui-icon {
@apply text-dark-cerulean mx-1;
}
div {
@apply ml-2 flex items-center;
}
.truck {
@apply -mb-px-5 -mt-px-5;
}
}
.actions {
@apply flex items-center justify-end;
grid-area: actions;
button {
@apply bg-transparent text-brand font-bold text-lg outline-none border-none;
&:disabled {
@apply text-disabled-customer;
}
}
}

View File

@@ -0,0 +1,122 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { ComponentStore } from '@ngrx/component-store';
import { ShoppingCartItemDTO } from '@swagger/checkout';
import { combineLatest } from 'rxjs';
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
export interface ShoppingCartItemComponentState {
item: ShoppingCartItemDTO;
orderType: string;
loadingOnItemChangeById?: number;
loadingOnQuantityChangeById?: number;
}
@Component({
selector: 'page-shopping-cart-item',
templateUrl: 'shopping-cart-item.component.html',
styleUrls: ['shopping-cart-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemComponentState> implements OnInit {
@Output() changeItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
@Output() changeDummyItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
@Output() changeQuantity = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO; quantity: number }>();
@Input()
get item() {
return this.get((s) => s.item);
}
set item(item: ShoppingCartItemDTO) {
if (this.item !== item) {
this.patchState({ item });
}
}
readonly item$ = this.select((s) => s.item);
readonly contributors$ = this.item$.pipe(map((item) => item?.product?.contributors?.split(';').map((val) => val.trim())));
@Input()
get orderType() {
return this.get((s) => s.orderType);
}
set orderType(orderType: string) {
if (this.orderType !== orderType) {
this.patchState({ orderType });
}
}
readonly orderType$ = this.select((s) => s.orderType);
@Input()
get loadingOnItemChangeById() {
return this.get((s) => s.loadingOnItemChangeById);
}
set loadingOnItemChangeById(loadingOnItemChangeById: number) {
if (this.loadingOnItemChangeById !== loadingOnItemChangeById) {
this.patchState({ loadingOnItemChangeById });
}
}
readonly loadingOnItemChangeById$ = this.select((s) => s.loadingOnItemChangeById).pipe(shareReplay());
@Input()
get loadingOnQuantityChangeById() {
return this.get((s) => s.loadingOnQuantityChangeById);
}
set loadingOnQuantityChangeById(loadingOnQuantityChangeById: number) {
if (this.loadingOnQuantityChangeById !== loadingOnQuantityChangeById) {
this.patchState({ loadingOnQuantityChangeById });
}
}
readonly loadingOnQuantityChangeById$ = this.select((s) => s.loadingOnQuantityChangeById).pipe(shareReplay());
@Input()
quantityError: string;
isDummy$ = this.item$.pipe(
map((item) => item?.availability?.supplyChannel === 'MANUALLY'),
shareReplay()
);
hasOrderType$ = this.orderType$.pipe(
map((orderType) => orderType !== undefined),
shareReplay()
);
quantityRange$ = combineLatest([this.orderType$, this.item$]).pipe(
map(([orderType, item]) => (orderType === 'Rücklage' ? item.availability?.inStock : 999))
);
isDownloadAvailable$ = combineLatest([this.item$, this.orderType$]).pipe(
filter(([_, orderType]) => orderType === 'Download'),
switchMap(([item]) =>
this.availabilityService.getDownloadAvailability({
item: { ean: item.product.ean, price: item.availability.price, itemId: +item.product.catalogProductNumber },
})
),
map((availability) => availability && this.availabilityService.isAvailable({ availability }))
);
olaError$ = this.checkoutService
.getOlaErrors({ processId: this.application.activatedProcessId })
.pipe(map((ids) => ids?.find((id) => id === this.item.id)));
constructor(
private availabilityService: DomainAvailabilityService,
private checkoutService: DomainCheckoutService,
private application: ApplicationService
) {
super({ item: undefined, orderType: '' });
}
ngOnInit() {}
async onChangeItem() {
const isDummy = await this.isDummy$.pipe(first()).toPromise();
isDummy ? this.changeDummyItem.emit({ shoppingCartItem: this.item }) : this.changeItem.emit({ shoppingCartItem: this.item });
}
onChangeQuantity(quantity: number) {
this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
}
}

View File

@@ -1,9 +1,17 @@
<label for="agent-comment">Anmerkung</label>
<textarea #input type="text" id="agent-comment" name="agent-comment" [(ngModel)]="value" [rows]="rows" (ngModelChange)="check()"></textarea>
<textarea
#input
type="text"
id="agent-comment"
name="agent-comment"
[(ngModel)]="value"
[rows]="rows"
(ngModelChange)="check()"
(blur)="save()"
></textarea>
<div class="action-wrapper">
<button type="button" *ngIf="!disabled && !!value" (click)="clear()">
<ui-icon icon="close" size="14px"></ui-icon>
</button>
<button type="button" *ngIf="!disabled && isDirty" (click)="save()">Speichern</button>
</div>

View File

@@ -17,7 +17,7 @@
{{ displayOrder?.buyer | buyerName }}
</div>
<div class="grow"></div>
<button [disabled]="displayOrder?.features?.orderType === 'B2B-Versand'" class="cta-print" (click)="openPrintModal(displayOrder.id)">
<button class="cta-print" (click)="openPrintModal(displayOrder.id)">
Drucken
</button>
</div>
@@ -82,7 +82,15 @@
<ng-container *ngSwitchCase="['Versand', 'B2B-Versand', 'DIG-Versand'].indexOf(order?.features?.orderType) > -1">
<span class="supplier">{{ (order?.subsetItems)[0].supplierLabel }}</span>
<span class="separator">|</span>
<span class="order-type">Versanddatum {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}</span>
<ng-container *ngIf="(order?.subsetItems)[0]?.estimatedDelivery; else estimatedShippingDate">
<span class="order-type"
>Zustellung zwischen {{ ((order?.subsetItems)[0]?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ ((order?.subsetItems)[0]?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}</span
>
</ng-container>
<ng-template #estimatedShippingDate>
<span class="order-type">Versanddatum {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}</span>
</ng-template>
</ng-container>
<ng-container *ngSwitchDefault>
<ng-container *ngIf="(order?.subsetItems)[0].supplierLabel; let supplierLabel">

View File

@@ -127,12 +127,16 @@
}
}
.quantity {
@apply w-px-30 text-right font-semibold;
}
.product-price {
@apply whitespace-nowrap ml-4;
.price {
@apply w-px-100 font-bold text-right ml-5;
.quantity {
@apply w-px-30 text-right font-semibold;
}
.price {
@apply w-px-100 font-bold text-right ml-4;
}
}
.footer {

View File

@@ -20,7 +20,16 @@ import { NEVER } from 'rxjs';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutSummaryComponent {
displayOrders$ = this.domainCheckoutService.getOrders();
displayOrders$ = this.domainCheckoutService.getOrders().pipe(
map((orders) =>
orders.map((order) => {
return {
...order,
items: [...order.items]?.sort((a, b) => a.product?.name.localeCompare(b.product?.name)),
};
})
)
);
totalItemCount$ = this.displayOrders$.pipe(
map((displayOrders) =>

View File

@@ -0,0 +1,13 @@
<div class="option-icon">
<ui-icon size="50px" icon="truck"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('delivery')"
[class.selected]="(selectedOption$ | async) === 'delivery'"
>
Versand
</button>
<p>Möchten Sie die Artikel<br />geliefert bekommen?</p>
<p>Versandkostenfrei</p>

View File

@@ -0,0 +1,23 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-delivery-option-list',
templateUrl: 'delivery-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeliveryOptionListComponent {
selectedOption$ = this._store.selectedFilterOption$;
optionChipDisabled$ = this._store.fetchingAvailabilities$;
constructor(private _store: PurchasingOptionsListModalStore) {}
optionChange(option: string) {
if (this._store.selectedFilterOption === option) {
this._store.selectedFilterOption = undefined;
} else {
this._store.selectedFilterOption = option;
}
}
}

View File

@@ -0,0 +1,3 @@
// start:ng42.barrel
export * from './delivery-option-list.component';
// end:ng42.barrel

View File

@@ -0,0 +1,7 @@
// start:ng42.barrel
export * from './delivery-option';
export * from './pick-up-option';
export * from './take-away-option';
export * from './purchasing-options-list-modal.component';
export * from './purchasing-options-list-modal.module';
// end:ng42.barrel

View File

@@ -0,0 +1,35 @@
:host {
@apply block w-72;
}
.option-icon {
@apply text-ucla-blue mx-auto;
width: 40px;
.truck-b2b {
margin-top: -21px;
margin-bottom: -12px;
width: 70px;
}
}
.option-chip {
@apply rounded-full text-base px-4 py-3 bg-glitter text-inactive-customer border-none font-bold;
&.selected {
@apply bg-active-customer text-white;
}
}
.option-description {
@apply my-2;
}
.option-select {
@apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6;
}
::ng-deep page-purchasing-options-list-modal ui-branch-dropdown .wrapper {
@apply mx-auto;
width: 80%;
}

View File

@@ -0,0 +1,3 @@
// start:ng42.barrel
export * from './pick-up-option-list.component';
// end:ng42.barrel

View File

@@ -0,0 +1,18 @@
<div class="option-icon">
<ui-icon size="50px" icon="box_out"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('pick-up')"
[class.selected]="(selectedOption$ | async) === 'pick-up'"
>
Abholung
</button>
<p>Möchten Sie die Artikel<br />in einer unserer Filialen<br />abholen?</p>
<ui-branch-dropdown
[branches]="branches$ | async"
[selected]="(selectedBranch$ | async)?.name"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>

View File

@@ -0,0 +1,36 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BranchDTO } from '@swagger/checkout';
import { first } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-pick-up-option-list',
templateUrl: 'pick-up-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PickUpOptionListComponent {
branches$ = this._store.branches$;
selectedBranch$ = this._store.selectedPickUpBranch$;
selectedOption$ = this._store.selectedFilterOption$;
optionChipDisabled$ = this._store.fetchingAvailabilities$;
constructor(private _store: PurchasingOptionsListModalStore) {}
optionChange(option: string) {
if (this._store.selectedFilterOption === option) {
this._store.selectedFilterOption = undefined;
} else {
this._store.selectedFilterOption = option;
}
}
async selectBranch(branch: BranchDTO) {
this._store.lastSelectedFilterOption$.next(undefined);
this._store.selectedPickUpBranch = branch;
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
shoppingCartItems.forEach((item) => this._store.loadPickUpAvailability({ item }));
}
}

View File

@@ -0,0 +1,115 @@
<div class="item-thumbnail">
<img loading="lazy" *ngIf="item?.product?.ean | productImage; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.title" />
</div>
<div class="item-contributors">
{{ item.product.contributors }}
</div>
<div
class="item-title"
[class.xl]="item?.product?.name?.length >= 35"
[class.lg]="item?.product?.name?.length >= 40"
[class.md]="item?.product?.name?.length >= 50"
[class.sm]="item?.product?.name?.length >= 60"
[class.xs]="item?.product?.name?.length >= 100"
>
{{ item?.product?.name }}
</div>
<ng-container *ngIf="canAdd$ | async; let canAdd">
<div class="item-can-add" *ngIf="canAdd !== true">
{{ canAdd }}
</div>
</ng-container>
<div class="item-format">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="item-info">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ item?.product?.publicationDate | date: 'dd. MMMM yyyy' }}
</div>
<div class="item-price-stock">
<div class="price">
<ng-container *ngIf="showTooltip$ | async">
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
i
</button>
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
Günstigerer Preis aus Hugendubel Katalog wird übernommen
</ui-tooltip>
</ng-container>
<div *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</div>
</div>
<div>
<ui-quantity-dropdown
[disabled]="fetchingAvailabilities$ | async"
[ngModel]="item.quantity"
(ngModelChange)="changeQuantity($event)"
[range]="quantityRange$ | async"
>
</ui-quantity-dropdown>
</div>
</div>
<div class="item-select">
<ui-select-bullet
*ngIf="selectVisible$ | async"
[disabled]="selectDisabled$ | async"
[ngModel]="isSelected$ | async"
(ngModelChange)="selected($event)"
></ui-select-bullet>
</div>
<div class="item-availability">
<div class="fetching" *ngIf="fetchingAvailabilities$ | async; else availabilities"></div>
<ng-template #availabilities>
<ng-container *ngIf="notAvailable$ | async; else available">
<span class="hint">Derzeit nicht bestellbar</span>
</ng-container>
<ng-template #available>
<span>Verfügbar als</span>
<div *ngIf="takeAwayAvailabilities$ | async; let takeAwayAvailabilites">
<ui-icon icon="shopping_bag" size="18px"></ui-icon>
<span class="instock">{{ takeAwayAvailabilites?.inStock }}x</span> ab sofort
</div>
<div *ngIf="!!(pickUpAvailabilities$ | async)">
<ui-icon icon="box_out" size="18px"></ui-icon>
{{ (pickUpAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</div>
<div *ngIf="!!(deliveryDigAvailabilities$ | async); else b2b">
<ui-icon class="truck" icon="truck" size="30px"></ui-icon>
<ng-container *ngIf="deliveryDigAvailabilities$ | async; let deliveryDigAvailabilities">
<ng-container *ngIf="deliveryDigAvailabilities?.estimatedDelivery; else estimatedShippingDate">
{{ (deliveryDigAvailabilities?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} -
{{ (deliveryDigAvailabilities?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate>
{{ deliveryDigAvailabilities.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</ng-template>
</ng-container>
</div>
<ng-template #b2b>
<div *ngIf="!!(deliveryB2bAvailabilities$ | async)">
<ui-icon class="truck-b2b" icon="truck_b2b" size="40px"></ui-icon>
{{ (deliveryB2bAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</div>
</ng-template>
</ng-template>
</ng-template>
</div>

View File

@@ -0,0 +1,180 @@
:host {
@apply text-black no-underline grid py-4;
grid-template-columns: 102px 60% auto;
grid-template-rows: auto;
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-title item-price-stock'
'item-thumbnail item-can-add item-price-stock'
'item-thumbnail item-format item-price-stock'
'item-thumbnail item-info item-select'
'item-thumbnail item-date item-select'
'item-thumbnail item-ssc item-select'
'item-thumbnail item-availability item-select';
}
.item-thumbnail {
grid-area: item-thumbnail;
width: 70px;
@apply mr-8;
img {
max-width: 100%;
max-height: 150px;
@apply rounded-card shadow-cta;
}
}
.item-contributors {
@apply font-bold no-underline;
grid-area: item-contributors;
height: 22px;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
white-space: nowrap;
}
.item-title {
grid-area: item-title;
@apply font-bold text-lg mb-4;
max-height: 64px;
}
.item-title.xl {
@apply font-bold text-xl;
}
.item-title.lg {
@apply font-bold text-lg;
}
.item-title.md {
@apply font-bold text-base;
}
.item-title.sm {
@apply font-bold text-sm;
}
.item-title.xs {
@apply font-bold text-xs;
}
.item-format {
grid-area: item-format;
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
img {
@apply mr-2;
}
}
.item-price-stock {
grid-area: item-price-stock;
@apply font-bold text-xl text-right;
.price {
@apply flex flex-row justify-end items-center;
}
.info-tooltip-button {
@apply border-font-customer bg-white rounded-full text-base font-bold mr-3;
border-style: outset;
width: 31px;
height: 31px;
margin-left: 10px;
}
.quantity-btn {
@apply flex flex-row items-center p-0 w-full text-right outline-none border-none bg-transparent text-lg;
}
.quantity-btn-icon {
@apply inline-flex ml-2;
transition: transform 200ms ease-in-out;
}
ui-quantity-dropdown {
@apply flex justify-end mt-2;
&.disabled {
@apply cursor-not-allowed bg-inactive-branch;
}
}
}
.item-stock {
grid-area: item-stock;
@apply flex flex-row justify-end items-baseline font-bold text-lg;
ui-icon {
@apply text-active-customer mr-2;
}
}
.item-info {
grid-area: item-info;
}
.item-availability {
@apply flex flex-row items-center mt-4 whitespace-nowrap text-sm;
grid-area: item-availability;
.fetching {
@apply w-52 h-px-20;
background-color: #e6eff9;
animation: load 0.75s linear infinite;
}
span {
@apply mr-4;
}
.instock {
@apply mr-2 font-bold;
}
ui-icon {
@apply text-dark-cerulean mx-2;
}
div {
@apply mr-4 flex items-center;
}
.truck {
@apply -mb-px-5 -mt-px-5;
}
.truck-b2b {
@apply -mb-px-10 -mt-px-10;
}
}
.item-can-add {
@apply text-xl text-dark-goldenrod font-semibold;
grid-area: item-can-add;
}
.item-select {
@apply flex items-center justify-end;
grid-area: item-select;
ui-select-bullet {
@apply cursor-pointer p-4 -m-4 z-dropdown;
&.disabled {
@apply cursor-not-allowed;
}
}
}
.hint {
@apply text-xl text-dark-goldenrod font-semibold;
}
@screen desktop {
.item-availability {
@apply text-base;
}
}

View File

@@ -0,0 +1,196 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { combineLatest } from 'rxjs';
import { filter, map, shareReplay, withLatestFrom } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-purchasing-options-list-item',
templateUrl: 'purchasing-options-list-item.component.html',
styleUrls: ['purchasing-options-list-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PurchasingOptionsListItemComponent {
@Input()
item: ShoppingCartItemDTO;
isSelected$ = this._store.selectedShoppingCartItems$.pipe(
map((selectedShoppingCartItems) => !!selectedShoppingCartItems?.find((item) => item.id === this.item.id))
);
fetchingAvailabilities$ = combineLatest([
this._store.takeAwayAvailabilities$,
this._store.pickUpAvailabilities$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$,
]).pipe(
map(
([takeAway, pickUp, delivery, digDelivery, b2bDelivery]) =>
!takeAway ||
takeAway[this.item.product.catalogProductNumber] === true ||
!pickUp ||
pickUp[this.item.product.catalogProductNumber] === true ||
!delivery ||
delivery[this.item.product.catalogProductNumber] === true ||
!digDelivery ||
digDelivery[this.item.product.catalogProductNumber] === true ||
!b2bDelivery ||
b2bDelivery[this.item.product.catalogProductNumber] === true
)
);
takeAwayAvailabilities$ = this._store.takeAwayAvailabilities$.pipe(
map((takeAwayAvailabilities) => (!!takeAwayAvailabilities ? takeAwayAvailabilities[this.item.product?.catalogProductNumber] : [])),
shareReplay()
);
pickUpAvailabilities$ = this._store.pickUpAvailabilities$.pipe(
map((pickUpAvailabilities) => (!!pickUpAvailabilities ? pickUpAvailabilities[this.item.product?.catalogProductNumber] : [])),
shareReplay()
);
deliveryAvailabilities$ = this._store.deliveryAvailabilities$.pipe(
map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])),
shareReplay()
);
deliveryDigAvailabilities$ = this._store.deliveryDigAvailabilities$.pipe(
map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])),
shareReplay()
);
deliveryB2bAvailabilities$ = this._store.deliveryB2bAvailabilities$.pipe(
map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])),
shareReplay()
);
notAvailable$ = combineLatest([
this.fetchingAvailabilities$,
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryDigAvailabilities$,
this.deliveryB2bAvailabilities$,
]).pipe(
map(
([fetching, takeAway, store, delivery, deliveryDig, deliveryB2b]) =>
!fetching && !takeAway && !store && !delivery && !deliveryDig && !deliveryB2b
)
);
showTooltip$ = this._store.selectedFilterOption$.pipe(
withLatestFrom(this.deliveryAvailabilities$, this.deliveryDigAvailabilities$),
map(([option, delivery, deliveryDig]) => {
if (option === 'delivery') {
const deliveryAvailability = (deliveryDig as AvailabilityDTO) || (delivery as AvailabilityDTO);
const shippingPrice = deliveryAvailability?.price?.value?.value;
const catalogPrice = this.item?.availability?.price?.value?.value;
return catalogPrice < shippingPrice;
}
return false;
})
);
price$ = this.fetchingAvailabilities$.pipe(
filter((fetching) => !fetching),
withLatestFrom(
this._store.selectedFilterOption$,
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryDigAvailabilities$,
this.deliveryB2bAvailabilities$
),
map(([_, option, takeAway, pickUp, delivery, deliveryDig, deliveryB2b]) => {
let availability;
switch (option) {
case 'take-away':
availability = takeAway;
break;
case 'pick-up':
availability = pickUp;
break;
case 'delivery':
availability = deliveryDig || delivery || deliveryB2b;
break;
default:
return this.item.availability?.price;
}
return this._availabilityService.getPriceForAvailability(option, this.item.availability, availability);
})
);
selectDisabled$ = this._store.selectedFilterOption$.pipe(map((selectedFilterOption) => !selectedFilterOption));
selectVisible$ = combineLatest([this._store.canAdd$, this._store.selectedShoppingCartItems$]).pipe(
withLatestFrom(
this._store.selectedFilterOption$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$,
this._store.fetchingAvailabilities$
),
map(([[canAdd, items], option, delivery, deliveryDig, deliveryB2b, fetching]) => {
if (!option || fetching) {
return false;
}
// Select immer sichtbar bei ausgewählten Items
if (items?.find((item) => item.product?.catalogProductNumber === this.item.product?.catalogProductNumber)) {
return true;
}
// Select nur anzeigen, wenn ein anderes ausgewähltes Item die gleiche Verfügbarkeit hat (B2B Versand z.B.)
if (items?.length > 0 && option === 'delivery' && canAdd[this.item.product.catalogProductNumber]?.status < 2) {
if (items.every((item) => delivery[item.product?.catalogProductNumber]) && delivery[this.item.product?.catalogProductNumber]) {
return true;
}
if (
items.every((item) => deliveryDig[item.product?.catalogProductNumber]) &&
deliveryDig[this.item.product?.catalogProductNumber]
) {
return true;
}
if (
items.every((item) => deliveryB2b[item.product?.catalogProductNumber]) &&
deliveryB2b[this.item.product?.catalogProductNumber]
) {
return true;
}
return false;
}
return canAdd && canAdd[this.item.product.catalogProductNumber]?.status < 2;
})
);
canAdd$ = this._store.canAdd$.pipe(
filter((canAdd) => !!this.item && !!canAdd),
map((canAdd) => canAdd[this.item.product.catalogProductNumber]?.message)
);
quantityRange$ = combineLatest([this._store.selectedFilterOption$, this.takeAwayAvailabilities$]).pipe(
map(([option, availability]) => (option === 'take-away' ? (availability as AvailabilityDTO)?.inStock : 999))
);
constructor(private _store: PurchasingOptionsListModalStore, private _availabilityService: DomainAvailabilityService) {}
selected(value: boolean) {
this._store.selectShoppingCartItem([this.item], value);
}
changeQuantity(quantity: number) {
if (quantity === 0) {
this._store.removeShoppingCartItem(this.item);
} else {
this._store.updateItemQuantity({ itemId: this.item.id, quantity });
this._store.loadAvailabilities({ items: [{ ...this.item, quantity }] });
}
}
}

View File

@@ -0,0 +1,49 @@
<div class="options">
<page-take-away-option-list></page-take-away-option-list>
<page-pick-up-option-list></page-pick-up-option-list>
<page-delivery-option-list></page-delivery-option-list>
</div>
<div class="items" *ngIf="shoppingCartItems$ | async; let shoppingCartItems">
<div class="item-actions">
<ng-container>
<button
*ngIf="!(allShoppingCartItemsSelected$ | async); else unselectAll"
class="cta-select-all"
[disabled]="selectAllCtaDisabled$ | async"
(click)="selectAll(shoppingCartItems, true)"
>
Alle auswählen
</button>
<ng-template #unselectAll>
<button class="cta-select-all" [disabled]="selectAllCtaDisabled$ | async" (click)="selectAll(shoppingCartItems, false)">
Alle abwählen
</button>
</ng-template>
</ng-container>
<br />
{{ (selectedShoppingCartItems$ | async)?.length || 0 }} von {{ shoppingCartItems?.length || 0 }} Artikeln
</div>
<div class="item-list scroll-bar" *ngIf="shoppingCartItems?.length > 0; else emptyMessage">
<hr />
<ng-container *ngFor="let item of shoppingCartItems">
<page-purchasing-options-list-item [item]="item"></page-purchasing-options-list-item>
<hr />
</ng-container>
</div>
<ng-template #emptyMessage>
<div class="empty-message">Keine Artikel für die ausgewählte Kaufoption verfügbar</div>
</ng-template>
</div>
<div class="actions">
<button class="cta-apply" [disabled]="applyCtaDisabled$ | async" (click)="apply()">
<ui-spinner [show]="addItemsLoader$ | async">
Übernehmen
</ui-spinner>
</button>
</div>

View File

@@ -0,0 +1,49 @@
:host {
@apply block box-border;
}
.options {
@apply flex flex-row box-border text-center justify-center mt-4;
}
.items {
min-height: 440px;
.item-actions {
@apply text-right;
.cta-select-all {
@apply text-brand bg-transparent text-base font-bold outline-none border-none px-4 py-4 -mr-4;
&:disabled {
@apply text-inactive-branch;
}
}
}
.item-list {
@apply overflow-y-scroll overflow-x-hidden -ml-4;
max-height: calc(100vh - 580px);
width: calc(100% + 2rem);
page-purchasing-options-list-item {
@apply px-4;
}
}
.empty-message {
@apply text-inactive-branch my-8 text-center font-bold;
}
}
.actions {
@apply flex justify-center mt-8;
.cta-apply {
@apply text-white border-2 border-solid border-brand bg-brand font-bold text-lg px-4 py-2 rounded-full;
&:disabled {
@apply bg-inactive-branch border-inactive-branch;
}
}
}

View File

@@ -0,0 +1,263 @@
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { ShoppingCartItemDTO, UpdateShoppingCartItemDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalRef, UiModalService } from '@ui/modal';
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators';
import { PurchasingOptionsListModalData } from './purchasing-options-list-modal.data';
import { PurchasingOptionsListModalStore } from './purchasing-options-list-modal.store';
@Component({
selector: 'page-purchasing-options-list-modal',
templateUrl: 'purchasing-options-list-modal.component.html',
styleUrls: ['purchasing-options-list-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [PurchasingOptionsListModalStore],
})
export class PurchasingOptionsListModalComponent implements OnInit {
private _onDestroy$ = new Subject();
addItemsLoader$ = new BehaviorSubject<boolean>(false);
shoppingCartItems$ = combineLatest([
this._store.fetchingAvailabilities$,
this._store.selectedFilterOption$,
this._store.shoppingCartItems$,
]).pipe(
withLatestFrom(
this._store.takeAwayAvailabilities$,
this._store.pickUpAvailabilities$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$
),
map(
([
[_, selectedFilterOption, shoppingCartItems],
takeAwayAvailability,
pickUpAvailability,
deliveryAvailability,
deliveryDigAvailability,
deliveryB2bAvailability,
]) => {
if (!!takeAwayAvailability && !!pickUpAvailability && !!deliveryAvailability) {
switch (selectedFilterOption) {
case 'take-away':
return shoppingCartItems.filter((item) => !!takeAwayAvailability[item.product?.catalogProductNumber]);
case 'pick-up':
return shoppingCartItems.filter((item) => !!pickUpAvailability[item.product?.catalogProductNumber]);
case 'delivery':
return shoppingCartItems.filter(
(item) =>
!!deliveryAvailability[item.product?.catalogProductNumber] ||
!!deliveryDigAvailability[item.product?.catalogProductNumber] ||
!!deliveryB2bAvailability[item.product?.catalogProductNumber]
);
}
}
return shoppingCartItems;
}
),
map((shoppingCartItems) => shoppingCartItems?.sort((a, b) => a.product?.name.localeCompare(b.product?.name))),
shareReplay()
);
selectedShoppingCartItems$ = this._store.selectedShoppingCartItems$;
allShoppingCartItemsSelected$ = combineLatest([this.shoppingCartItems$, this.selectedShoppingCartItems$]).pipe(
map(
([shoppingCartItems, selectedShoppingCartItems]) =>
shoppingCartItems.every((item) => selectedShoppingCartItems.find((i) => item.id === i.id)) && shoppingCartItems?.length > 0
)
);
canAddItems$ = this._store.canAdd$.pipe(
map((canAdd) => {
for (const key in canAdd) {
if (Object.prototype.hasOwnProperty.call(canAdd, key)) {
if (!!canAdd[key]?.message) {
return false;
}
}
}
return true;
}),
shareReplay()
);
selectAllCtaDisabled$ = combineLatest([this._store.selectedFilterOption$, this.canAddItems$]).pipe(
withLatestFrom(this.shoppingCartItems$),
map(([[selectedFilterOption, canAddItems], items]) => !selectedFilterOption || items?.length === 0 || !canAddItems)
);
applyCtaDisabled$ = combineLatest([this.addItemsLoader$, this._store.selectedFilterOption$, this._store.selectedShoppingCartItems$]).pipe(
withLatestFrom(this.shoppingCartItems$),
map(
([[addItemsLoader, selectedFilterOption, selectedShoppingCartItems], shoppingCartItems]) =>
addItemsLoader || !selectedFilterOption || shoppingCartItems?.length === 0 || selectedShoppingCartItems?.length === 0
)
);
constructor(
private _modalRef: UiModalRef<any, PurchasingOptionsListModalData>,
private _modal: UiModalService,
private _store: PurchasingOptionsListModalStore,
private _availability: DomainAvailabilityService,
private _checkout: DomainCheckoutService
) {
this._store.shoppingCartItems = _modalRef.data.shoppingCartItems;
this._store.customerFeatures = _modalRef.data.customerFeatures;
this._store.processId = _modalRef.data.processId;
}
ngOnInit() {
this._store.loadBranches();
// Beim Wechsel der ausgewählten Filteroption oder der Branches die Auswahl leeren
combineLatest([this._store.selectedFilterOption$, this._store.selectedTakeAwayBranch$, this._store.selectedPickUpBranch$])
.pipe(takeUntil(this._onDestroy$))
.subscribe(() => this._store.clearSelectedShoppingCartItems());
this._store.selectedFilterOption$
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.shoppingCartItems$))
.subscribe(([option, items]) => this.checkCanAdd(option, items));
this._store.fetchingAvailabilities$
.pipe(
takeUntil(this._onDestroy$),
debounceTime(250),
filter((fetching) => !fetching),
withLatestFrom(this.shoppingCartItems$, this._store.selectedFilterOption$)
)
.subscribe(([_, items, option]) => this.checkCanAdd(option, items));
this.canAddItems$
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.shoppingCartItems$, this._store.selectedFilterOption$))
.subscribe(([showSelectAll, items, option]) => {
if (items?.length > 0 && this._store.lastSelectedFilterOption$.value !== option) {
this.selectAll(items, showSelectAll && !!option);
}
// Nach dem Übernehmen von Items wird eine neue CanAdd Abfrage ausgeführt, in diesem Fall soll aber nicht alles ausgewählt werden
this._store.lastSelectedFilterOption$.next(option);
});
}
checkCanAdd(selectedFilterOption: string, items: ShoppingCartItemDTO[]) {
if (!!selectedFilterOption && items?.length > 0) {
this._store.checkCanAddItems(items);
} else {
this._store.patchState({ canAdd: {} });
}
}
async selectAll(items: ShoppingCartItemDTO[], value: boolean) {
this._store.selectShoppingCartItem(items, value);
}
async apply() {
this.addItemsLoader$.next(true);
try {
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
const items = await this._store.selectedShoppingCartItems$.pipe(first()).toPromise();
const takeAwayAvailabilities = await this._store.takeAwayAvailabilities$.pipe(first()).toPromise();
const pickupAvailabilities = await this._store.pickUpAvailabilities$.pipe(first()).toPromise();
const deliveryAvailabilities = await this._store.deliveryAvailabilities$.pipe(first()).toPromise();
const deliveryB2bAvailabilities = await this._store.deliveryB2bAvailabilities$.pipe(first()).toPromise();
const deliveryDigAvailabilities = await this._store.deliveryDigAvailabilities$.pipe(first()).toPromise();
const selectedTakeAwayBranch = await this._store.selectedTakeAwayBranch$.pipe(first()).toPromise();
const selectedPickUpBranch = await this._store.selectedPickUpBranch$.pipe(first()).toPromise();
for (const item of items) {
let availability;
switch (this._store.selectedFilterOption) {
case 'take-away':
availability = takeAwayAvailabilities[item.product.catalogProductNumber];
break;
case 'pick-up':
availability = pickupAvailabilities[item.product.catalogProductNumber];
break;
case 'delivery':
if (
deliveryDigAvailabilities[item.product.catalogProductNumber] &&
deliveryB2bAvailabilities[item.product.catalogProductNumber] &&
deliveryAvailabilities[item.product.catalogProductNumber]
) {
availability = deliveryAvailabilities[item.product.catalogProductNumber];
} else if (deliveryDigAvailabilities[item.product.catalogProductNumber]) {
availability = deliveryDigAvailabilities[item.product.catalogProductNumber];
} else if (deliveryB2bAvailabilities[item.product.catalogProductNumber]) {
availability = deliveryB2bAvailabilities[item.product.catalogProductNumber];
}
break;
}
const price = this._availability.getPriceForAvailability(this._store.selectedFilterOption, item.availability, availability);
// Negative Preise und nicht vorhandene Availability ignorieren
if (price?.value?.value < 0 || !availability) {
continue;
}
const updateItem: UpdateShoppingCartItemDTO = {
quantity: item.quantity,
availability: {
...availability,
price,
},
promotion: { points: item.promotion.points },
};
switch (this._store.selectedFilterOption) {
case 'take-away':
updateItem.destination = {
data: { target: 1, targetBranch: { id: selectedTakeAwayBranch.id } },
};
break;
case 'pick-up':
updateItem.destination = {
data: { target: 1, targetBranch: { id: selectedPickUpBranch.id } },
};
break;
case 'delivery':
case 'dig-delivery':
case 'b2b-delivery':
updateItem.destination = {
data: { target: 2, logistician: availability?.logistician },
};
break;
}
await this._checkout
.updateItemInShoppingCart({
processId: this._modalRef.data.processId,
shoppingCartItemId: item.id,
update: {
...updateItem,
},
})
.toPromise();
}
const remainingItems = shoppingCartItems.filter((i) => !items.find((j) => i.id === j.id));
this._store.shoppingCartItems = [...remainingItems];
this._store.clearSelectedShoppingCartItems();
if (remainingItems?.length === 0) {
this._modalRef.close();
}
} catch (error) {
this._modal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim Hinzufügen zum Warenkorb' });
} finally {
this.addItemsLoader$.next(false);
}
const shoppingCartItems = await this.shoppingCartItems$.pipe(first()).toPromise();
if (shoppingCartItems?.length > 0) {
this._store.checkCanAddItems(shoppingCartItems);
}
}
}

View File

@@ -0,0 +1,7 @@
import { ShoppingCartItemDTO } from '@swagger/checkout';
export interface PurchasingOptionsListModalData {
processId: number;
shoppingCartItems?: ShoppingCartItemDTO[];
customerFeatures: { [key: string]: string };
}

View File

@@ -0,0 +1,41 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PurchasingOptionsListModalComponent } from './purchasing-options-list-modal.component';
import { UiIconModule } from '@ui/icon';
import { ProductImageModule } from '@cdn/product-image';
import { UiCommonModule } from '@ui/common';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { PickUpOptionListComponent } from './pick-up-option/pick-up-option-list.component';
import { TakeAwayOptionListComponent } from './take-away-option/take-away-option-list.component';
import { DeliveryOptionListComponent } from './delivery-option/delivery-option-list.component';
import { PurchasingOptionsListItemComponent } from './purchasing-options-list-item/purchasing-options-list-item.component';
import { FormsModule } from '@angular/forms';
import { UiBranchDropdownModule } from '@ui/branch-dropdown';
import { UiTooltipModule } from '@ui/tooltip';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
UiCommonModule,
UiIconModule,
UiSelectBulletModule,
UiQuantityDropdownModule,
ProductImageModule,
UiBranchDropdownModule,
UiTooltipModule,
UiSpinnerModule,
],
exports: [PurchasingOptionsListModalComponent],
declarations: [
PurchasingOptionsListModalComponent,
PurchasingOptionsListItemComponent,
PickUpOptionListComponent,
TakeAwayOptionListComponent,
DeliveryOptionListComponent,
],
})
export class PurchasingOptionsListModalModule {}

View File

@@ -0,0 +1,574 @@
import { Injectable } from '@angular/core';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { AvailabilityDTO, BranchDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { map, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
import { DomainAvailabilityService } from '@domain/availability';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { DomainCheckoutService } from '@domain/checkout';
interface PurchasingOptionsListModalState {
processId: number;
shoppingCartItems: ShoppingCartItemDTO[];
selectedFilterOption: string;
takeAwayAvailabilities: { [key: string]: AvailabilityDTO | true };
pickUpAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryB2bAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryDigAvailabilities: { [key: string]: AvailabilityDTO | true };
customerFeatures: { [key: string]: string };
canAdd: { [key: string]: { message: string; status: number } };
selectedShoppingCartItems: ShoppingCartItemDTO[];
branches: BranchDTO[];
currentBranch: BranchDTO;
selectedTakeAwayBranch: BranchDTO;
selectedPickUpBranch: BranchDTO;
}
@Injectable()
export class PurchasingOptionsListModalStore extends ComponentStore<PurchasingOptionsListModalState> {
lastSelectedFilterOption$ = new BehaviorSubject<string>(undefined);
branches$ = this.select((s) => s.branches);
currentBranch$ = this.select((s) => s.currentBranch);
takeAwayAvailabilities$ = this.select((s) => s.takeAwayAvailabilities);
pickUpAvailabilities$ = this.select((s) => s.pickUpAvailabilities);
deliveryAvailabilities$ = this.select((s) => s.deliveryAvailabilities);
deliveryB2bAvailabilities$ = this.select((s) => s.deliveryB2bAvailabilities);
canAdd$ = this.select((s) => s.canAdd);
deliveryDigAvailabilities$ = this.select((s) => s.deliveryDigAvailabilities);
shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
shoppingCartItems = shoppingCartItems.sort((a, b) => a.product?.name.localeCompare(b.product.name));
this.patchState({ shoppingCartItems });
}
processId$ = this.select((s) => s.processId);
set processId(processId: number) {
this.patchState({ processId });
}
customerFeatures$ = this.select((s) => s.customerFeatures);
set customerFeatures(customerFeatures: { [key: string]: string }) {
this.patchState({ customerFeatures });
}
selectedFilterOption$ = this.select((s) => s.selectedFilterOption);
set selectedFilterOption(selectedFilterOption: string) {
this.patchState({ selectedFilterOption });
}
get selectedFilterOption() {
return this.get((s) => s.selectedFilterOption);
}
selectedShoppingCartItems$ = this.select((s) => s.selectedShoppingCartItems);
get selectedShoppingCartItems() {
return this.get((s) => s.selectedShoppingCartItems);
}
selectedTakeAwayBranch$ = this.select((s) => s.selectedTakeAwayBranch);
set selectedTakeAwayBranch(selectedTakeAwayBranch: BranchDTO) {
this.patchState({ selectedTakeAwayBranch });
}
selectedPickUpBranch$ = this.select((s) => s.selectedPickUpBranch);
set selectedPickUpBranch(selectedPickUpBranch: BranchDTO) {
this.patchState({ selectedPickUpBranch });
}
fetchingAvailabilities$ = combineLatest([this.takeAwayAvailabilities$, this.pickUpAvailabilities$, this.deliveryAvailabilities$]).pipe(
map(([takeAway, pickUp, delivery]) => {
const fetchingCheck = (obj) => {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const element = obj[key];
if (typeof element === 'boolean') {
return true;
}
}
}
return false;
};
return !takeAway || fetchingCheck(takeAway) || !pickUp || fetchingCheck(pickUp) || !delivery || fetchingCheck(delivery);
})
);
constructor(private _availabilityService: DomainAvailabilityService, private _checkoutService: DomainCheckoutService) {
super({
processId: undefined,
shoppingCartItems: [],
selectedFilterOption: undefined,
pickUpAvailabilities: undefined,
deliveryAvailabilities: undefined,
takeAwayAvailabilities: undefined,
deliveryB2bAvailabilities: undefined,
deliveryDigAvailabilities: undefined,
selectedShoppingCartItems: [],
branches: [],
currentBranch: undefined,
selectedTakeAwayBranch: undefined,
selectedPickUpBranch: undefined,
customerFeatures: undefined,
canAdd: undefined,
});
}
loadAvailabilities(options: { items?: ShoppingCartItemDTO[] }) {
const shoppingCartItems = options.items ?? this.get((s) => s.shoppingCartItems);
for (const item of shoppingCartItems) {
this.loadTakeAwayAvailability({ item });
this.loadPickUpAvailability({ item });
this.loadDeliveryAvailability({ item });
this.loadDeliveryB2bAvailability({ item });
this.loadDeliveryDigAvailability({ item });
}
}
readonly setAvailabilityFetching = this.updater((state, { name, id, fetching }: { name: string; id: string; fetching?: boolean }) => {
const availability = { ...state[name] };
if (fetching) {
availability[id] = fetching;
} else {
delete availability[id];
}
return {
...state,
[name]: {
...availability,
},
};
});
readonly setAvailability = this.updater((state, { name, availability }: { name: string; availability: any }) => {
const av = { ...state[name] };
if (this._availabilityService.isAvailable({ availability })) {
av[availability.itemId] = availability;
}
return {
...state,
[name]: av,
};
});
loadPickUpAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
withLatestFrom(this.selectedPickUpBranch$),
mergeMap(([options, branch]) => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getPickUpAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
branch,
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'pickUpAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'pickUpAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryB2bAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getB2bDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryB2bAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryB2bAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryDigAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getDigDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryDigAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryDigAvailabilities', availability: {} });
}
)
);
})
)
);
loadTakeAwayAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
withLatestFrom(this.selectedTakeAwayBranch$),
mergeMap(([options, branch]) => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getTakeAwayAvailabilityByBranch({
itemId: +options.item.product.catalogProductNumber,
price: options.item.availability.price,
quantity: options.item.quantity,
branch,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'takeAwayAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'takeAwayAvailabilities', availability: {} });
}
)
);
})
)
);
loadBranches = this.effect(($) =>
$.pipe(
switchMap(() =>
this._availabilityService.getBranches().pipe(
map((branches) =>
branches.filter(
(branch) => branch.status === 1 && branch.branchType === 1 && branch.isOnline === true && branch.isShippingEnabled === true
)
),
withLatestFrom(this._availabilityService.getCurrentBranch()),
tapResponse(
([branches, currentBranch]) => {
this.patchState({
branches,
selectedTakeAwayBranch: currentBranch,
selectedPickUpBranch: currentBranch,
currentBranch,
});
this.loadAvailabilities({});
},
() =>
this.patchState({
branches: [],
selectedTakeAwayBranch: undefined,
selectedPickUpBranch: undefined,
currentBranch: undefined,
})
)
)
)
)
);
checkCanAddItems = this.effect((items$: Observable<ShoppingCartItemDTO[]>) =>
items$.pipe(
withLatestFrom(
this.processId$,
this.selectedFilterOption$,
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryB2bAvailabilities$,
this.deliveryDigAvailabilities$
),
mergeMap(([items, processId, selectedOption, takeAway, pickUp, delivery, deliveryB2b, deliveryDig]) => {
let orderType: string;
const payload = items.map((item) => {
switch (selectedOption) {
case 'take-away':
orderType = 'Rücklage';
return {
availabilities: [this.getOlaAvailability(takeAway[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
case 'pick-up':
orderType = 'Abholung';
return {
availabilities: [this.getOlaAvailability(pickUp[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
case 'delivery':
orderType = 'Versand';
if (
deliveryDig[item.product.catalogProductNumber] &&
deliveryB2b[item.product.catalogProductNumber] &&
delivery[item.product.catalogProductNumber]
) {
return {
availabilities: [this.getOlaAvailability(delivery[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
} else if (deliveryDig[item.product.catalogProductNumber]) {
return {
availabilities: [this.getOlaAvailability(deliveryDig[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
} else if (deliveryB2b[item.product.catalogProductNumber]) {
return {
availabilities: [this.getOlaAvailability(deliveryB2b[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
}
break;
}
});
return this._checkoutService.canAddItems({ processId, payload, orderType }).pipe(
tapResponse(
(result: any) => {
const canAdd = {};
result?.forEach((r) => {
canAdd[r.id] = { message: r.message, status: r.status };
});
this.patchState({ canAdd });
},
(error: Error) => {
const canAdd = {};
items?.forEach((i) => {
canAdd[i.product?.catalogProductNumber] = { message: error?.message };
});
this.patchState({ canAdd });
}
)
);
})
)
);
getOlaAvailability(availability: AvailabilityDTO, item: ShoppingCartItemDTO) {
return {
qty: item.quantity,
ean: item.product.ean,
itemId: item.product.catalogProductNumber,
format: item.product.format,
at: availability?.estimatedShippingDate,
isPrebooked: availability?.isPrebooked,
status: availability?.availabilityType,
logisticianId: availability?.logistician?.id,
price: availability?.price,
ssc: availability?.ssc,
sscText: availability?.sscText,
supplierId: availability?.supplier?.id,
};
}
readonly updateItemQuantity = this.updater((state, value: { itemId: number; quantity: number }) => {
const itemToUpdate = state.shoppingCartItems.find((item) => item.id === value.itemId);
const otherItems = state.shoppingCartItems.filter((item) => item.id !== value.itemId);
const updatedItem = { ...itemToUpdate, quantity: value.quantity };
const shoppingCartItems = [...otherItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name));
// Ausgewählte Items auch aktualisieren
let selectedShoppingCartItems = state.selectedShoppingCartItems;
if (state.selectedShoppingCartItems.find((item) => item.id === value.itemId)) {
const selectedItems = state.selectedShoppingCartItems.filter((item) => item.id !== value.itemId);
selectedShoppingCartItems = [...selectedItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name));
}
return {
...state,
shoppingCartItems,
selectedShoppingCartItems,
};
});
async removeShoppingCartItem(item: ShoppingCartItemDTO) {
const items = this.get((s) => s.shoppingCartItems);
const processId = this.get((s) => s.processId);
await this._checkoutService
.updateItemInShoppingCart({
processId,
shoppingCartItemId: item.id,
update: {
quantity: 0,
availability: null,
},
})
.toPromise();
this.selectShoppingCartItem([item], false);
const shoppingCartItems = items.filter((i) => i.id !== item.id);
this.patchState({ shoppingCartItems });
}
selectShoppingCartItem(shoppingCartItems: ShoppingCartItemDTO[], selected: boolean) {
if (selected) {
this.patchState({
selectedShoppingCartItems: [
...this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)),
...shoppingCartItems,
],
});
} else {
this.patchState({
selectedShoppingCartItems: this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)),
});
}
}
clearSelectedShoppingCartItems() {
this.patchState({ selectedShoppingCartItems: [] });
}
}

View File

@@ -0,0 +1,3 @@
// start:ng42.barrel
export * from './take-away-option-list.component';
// end:ng42.barrel

View File

@@ -0,0 +1,18 @@
<div class="option-icon">
<ui-icon size="50px" icon="shopping_bag"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('take-away')"
[class.selected]="(selectedOption$ | async) === 'take-away'"
>
Rücklage
</button>
<p>Möchten Sie die Artikel<br />zurücklegen lassen oder<br />sofort mitnehmen?</p>
<ui-branch-dropdown
[branches]="branches$ | async"
[selected]="(selectedBranch$ | async)?.name"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>

View File

@@ -0,0 +1,36 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BranchDTO } from '@swagger/checkout';
import { first } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-take-away-option-list',
templateUrl: 'take-away-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TakeAwayOptionListComponent {
branches$ = this._store.branches$;
selectedBranch$ = this._store.selectedTakeAwayBranch$;
selectedOption$ = this._store.selectedFilterOption$;
optionChipDisabled$ = this._store.fetchingAvailabilities$;
constructor(private _store: PurchasingOptionsListModalStore) {}
optionChange(option: string) {
if (this._store.selectedFilterOption === option) {
this._store.selectedFilterOption = undefined;
} else {
this._store.selectedFilterOption = option;
}
}
async selectBranch(branch: BranchDTO) {
this._store.lastSelectedFilterOption$.next(undefined);
this._store.selectedTakeAwayBranch = branch;
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
shoppingCartItems.forEach((item) => this._store.loadTakeAwayAvailability({ item }));
}
}

View File

@@ -8,7 +8,7 @@
<p>
Als B2B Kunde können wir Ihnen den Artikel auch liefern.
</p>
<span class="price">{{ availability.price?.value?.value | currency: availability.price?.value?.currency:'code' }}</span>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<div class="grow"></div>
<span class="delivery">Versandkostenfrei</span>
<span class="date"

View File

@@ -1,4 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@@ -9,13 +11,17 @@ import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class B2BDeliveryOptionComponent {
readonly item$ = this.purchasingOptionsModalStore.selectItem;
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this.purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['b2b-delivery']));
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['b2b-delivery']));
constructor(private purchasingOptionsModalStore: PurchasingOptionsModalStore) {}
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('b2b-delivery', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this.purchasingOptionsModalStore.setOption('b2b-delivery');
this._purchasingOptionsModalStore.setOption('b2b-delivery');
}
}

View File

@@ -8,7 +8,7 @@
Möchten Sie den Artikel geliefert bekommen?
</p>
<div class="price-wrapper">
<span class="price">{{ price$ | async | currency: availability.price?.value?.currency:'code' }}</span>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<ng-container *ngIf="showTooltip$ | async">
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
i
@@ -20,9 +20,18 @@
</div>
<div class="grow"></div>
<span class="delivery">Versandkostenfrei</span>
<span class="date"
>Versanddatum <strong>{{ availability?.estimatedShippingDate | date }}</strong></span
>
<span *ngIf="availability?.estimatedDelivery; else estimatedShippingDate" class="date">
Zustellung zwischen <br />
<strong
>{{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}</strong
>
</span>
<ng-template #estimatedShippingDate>
<span class="date">
Versanddatum <strong>{{ estimatedShippingDate | date }}</strong>
</span>
</ng-template>
<div>
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
Auswählen

View File

@@ -1,5 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { DomainAvailabilityService } from '@domain/availability';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@@ -10,26 +11,25 @@ import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeliveryOptionComponent {
readonly item$ = this.purchasingOptionsModalStore.selectItem;
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this.purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['delivery']));
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['delivery']));
showTooltip$ = new BehaviorSubject<boolean>(false);
price$ = combineLatest([this.availability$, this.item$]).pipe(
readonly showTooltip$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => {
const shippingPrice = availability?.price?.value?.value;
const catalogPrice = item?.catalogAvailability?.price?.value?.value;
if (catalogPrice < shippingPrice) {
this.showTooltip$.next(true);
}
return catalogPrice <= shippingPrice ? catalogPrice : shippingPrice;
return catalogPrice < shippingPrice;
})
);
constructor(private purchasingOptionsModalStore: PurchasingOptionsModalStore) {}
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('delivery', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this.purchasingOptionsModalStore.setOption('delivery');
this._purchasingOptionsModalStore.setOption('delivery');
}
}

View File

@@ -6,7 +6,7 @@
<h4>DIG Versand</h4>
<p>Möchten Sie den Artikel geliefert bekommen?</p>
<div class="price-wrapper">
<span class="price">{{ price$ | async | currency: availability.price?.value?.currency:'code' }}</span>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<ng-container *ngIf="showTooltip$ | async">
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
i
@@ -18,9 +18,18 @@
</div>
<div class="grow"></div>
<span class="delivery">Versandkostenfrei</span>
<span class="date"
>Versanddatum <strong>{{ availability?.estimatedShippingDate | date: 'shortDate' }}</strong></span
>
<span *ngIf="availability?.estimatedDelivery; else estimatedShippingDate" class="date">
Zustellung zwischen <br />
<strong
>{{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}</strong
>
</span>
<ng-template #estimatedShippingDate>
<span class="date">
Versanddatum <strong>{{ estimatedShippingDate | date }}</strong>
</span>
</ng-template>
<div>
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">

View File

@@ -1,5 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BehaviorSubject, combineLatest } from 'rxjs';
import { DomainAvailabilityService } from '@domain/availability';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@@ -10,26 +11,25 @@ import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DigDeliveryOptionComponent {
readonly item$ = this.purchasingOptionsModalStore.selectItem;
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this.purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['dig-delivery']));
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['dig-delivery']));
showTooltip$ = new BehaviorSubject<boolean>(false);
price$ = combineLatest([this.availability$, this.item$]).pipe(
readonly showTooltip$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => {
const shippingPrice = availability?.price?.value?.value;
const catalogPrice = item?.catalogAvailability?.price?.value?.value;
if (catalogPrice < shippingPrice) {
this.showTooltip$.next(true);
}
return catalogPrice <= shippingPrice ? catalogPrice : shippingPrice;
return catalogPrice < shippingPrice;
})
);
constructor(private purchasingOptionsModalStore: PurchasingOptionsModalStore) {}
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('dig-delivery', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this.purchasingOptionsModalStore.setOption('dig-delivery');
this._purchasingOptionsModalStore.setOption('dig-delivery');
}
}

View File

@@ -25,7 +25,7 @@ p {
}
.date {
@apply text-cta-l;
@apply text-cta-l whitespace-nowrap;
}
.grow {

View File

@@ -1,44 +0,0 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { BranchDTO } from '@swagger/checkout';
import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../../purchasing-options-modal.store';
@Component({
selector: 'page-pick-up-dropdown',
templateUrl: 'pick-up-dropdown.component.html',
styleUrls: ['pick-up-dropdown.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PickUpDropdownComponent implements OnInit {
branches$: Observable<BranchDTO[]>;
selected$: Observable<string>;
searchFilter: string;
preselectedBranch: BranchDTO;
isOpen = false;
constructor(private purchasingOptionsModalStore: PurchasingOptionsModalStore) {}
ngOnInit() {
this.branches$ = this.purchasingOptionsModalStore.selectFilterResult;
this.selected$ = this.branches$.pipe(
switchMap((branches) =>
this.purchasingOptionsModalStore.selectBranch.pipe(map((branch) => branches.find((b) => b.id === branch.id)?.name))
)
);
}
setBranch(branch: BranchDTO) {
this.isOpen = false;
this.purchasingOptionsModalStore.setBranch(branch);
}
preselectStyling(branch: BranchDTO) {
this.preselectedBranch = branch;
}
filter(event: string) {
this.purchasingOptionsModalStore.setFilteredBranches(event);
}
}

View File

@@ -7,8 +7,12 @@
<p>
Möchten Sie den Artikel in einer unserer Filialen abholen?
</p>
<span class="price">{{ availability.price?.value?.value | currency: availability.price?.value?.currency:'code' }}</span>
<page-pick-up-dropdown></page-pick-up-dropdown>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<ui-branch-dropdown
[branches]="branches$ | async"
[selected]="selected$ | async"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>
<span class="date"
>Abholung ab <strong>{{ (availability$ | async)?.estimatedShippingDate | date: 'shortDate' }}</strong></span
>

View File

@@ -1,4 +1,7 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { BranchDTO } from '@swagger/checkout';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@@ -9,13 +12,24 @@ import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PickUpOptionComponent {
readonly item$ = this.purchasingOptionsModalStore.selectItem;
branches$: Observable<BranchDTO[]> = this._purchasingOptionsModalStore.selectAvailableBranches;
selected$: Observable<string> = this._purchasingOptionsModalStore.selectBranch.pipe(map((branch) => branch.name));
readonly availability$ = this.purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['pick-up']));
readonly item$ = this._purchasingOptionsModalStore.selectItem;
constructor(private purchasingOptionsModalStore: PurchasingOptionsModalStore) {}
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['pick-up']));
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('pick-up', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this.purchasingOptionsModalStore.setOption('pick-up');
this._purchasingOptionsModalStore.setOption('pick-up');
}
selectBranch(branch: BranchDTO) {
this._purchasingOptionsModalStore.setBranch(branch);
}
}

View File

@@ -9,6 +9,9 @@
<page-dig-delivery-option *ngSwitchCase="'dig-delivery'"></page-dig-delivery-option>
<page-b2b-delivery-option *ngSwitchCase="'b2b-delivery'"></page-b2b-delivery-option>
</ng-container>
<ng-container *ngIf="(availableOptions$ | async).length === 0">
<p class="hint">Derzeit nicht bestellbar</p>
</ng-container>
</div>
</ng-container>
@@ -41,9 +44,18 @@
{{ price$ | async | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
</div>
<div class="date" *ngIf="option$ | async; let option">
<ng-container *ngIf="option === 'pick-up'">Abholung ab</ng-container>
<ng-container *ngIf="showDeliveryInfo$ | async">Versanddatum</ng-container>
{{ (getAvailability(option) | async)?.estimatedShippingDate | date: 'shortDate' }}
<ng-container *ngIf="option === 'pick-up'">
Abholung ab {{ (getAvailability(option) | async)?.estimatedShippingDate | date: 'shortDate' }}
</ng-container>
<ng-container *ngIf="showDeliveryInfo$ | async">
<ng-container *ngIf="getAvailability(option) | async; let availability">
<ng-container *ngIf="availability?.estimatedDelivery; else estimatedShippingDate">
Zustellung zwischen {{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate> Versanddatum {{ availability?.estimatedShippingDate | date: 'shortDate' }} </ng-template>
</ng-container>
</ng-container>
</div>
</div>
<div class="quantity">
@@ -52,6 +64,7 @@
#quantityControl
[showSpinner]="purchasingOptionsModalStore.selectFetchingAvailability | async"
[ngModel]="quantity$ | async"
[range]="quantityRange$ | async"
(ngModelChange)="changeQuantity($event)"
>
</ui-quantity-dropdown>

View File

@@ -110,3 +110,7 @@ img.thumbnail {
.quantity-error {
@apply text-dark-goldenrod font-bold text-sm mt-2;
}
.hint {
@apply text-dark-goldenrod font-bold text-xl;
}

View File

@@ -4,10 +4,10 @@ import { ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { AddToShoppingCartDTO, AvailabilityDTO, VATType } from '@swagger/checkout';
import { UiModalRef } from '@ui/modal';
import { debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { shareReplay, debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { combineLatest, Observable } from 'rxjs';
import { PurchasingOptionsModalData } from './purchasing-options-modal.data';
import { PurchasingOptions, PurchasingOptionsModalStore } from './purchasing-options-modal.store';
import { PurchasingOptionsModalStore } from './purchasing-options-modal.store';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
@@ -21,7 +21,7 @@ import { BreadcrumbService } from '@core/breadcrumb';
export class PurchasingOptionsModalComponent {
readonly item$ = this.purchasingOptionsModalStore.selectItem;
readonly availableOptions$ = this.purchasingOptionsModalStore.selectAvailableOptions;
readonly availableOptions$ = this.purchasingOptionsModalStore.selectAvailableOptions.pipe(shareReplay());
readonly option$ = this.purchasingOptionsModalStore.selectOption;
@@ -169,6 +169,10 @@ export class PurchasingOptionsModalComponent {
)
);
quantityRange$ = combineLatest([this.option$, this.availability$]).pipe(
map(([option, availability]) => (option === 'take-away' ? availability.inStock : 999))
);
activeSpinner: string;
constructor(
@@ -293,17 +297,11 @@ export class PurchasingOptionsModalComponent {
break;
}
const shoppingCart = await this.checkoutService.getShoppingCart({ processId }).pipe(first()).toPromise();
const existingItem = shoppingCart?.items?.find(
({ data }) => data.product.ean === item.product.ean && data.features['orderType'] === this.getNameForOption(option)
);
if (shoppingCartItem || existingItem) {
if (shoppingCartItem) {
await this.checkoutService
.updateItemInShoppingCart({
processId,
shoppingCartItemId: shoppingCartItem?.id || existingItem?.id,
shoppingCartItemId: shoppingCartItem?.id,
update: {
availability: newItem.availability,
quantity: newItem.quantity,
@@ -367,21 +365,4 @@ export class PurchasingOptionsModalComponent {
}
this.activeSpinner = undefined;
}
getNameForOption(option: PurchasingOptions) {
switch (option) {
case 'take-away':
return 'Rücklage';
case 'pick-up':
return 'Abholung';
case 'delivery':
return 'Versand';
case 'dig-delivery':
return 'DIG-Versand';
case 'b2b-delivery':
return 'B2B-Versand';
}
return option;
}
}

View File

@@ -1,5 +1,5 @@
import { ItemDTO } from '@swagger/cat';
import { AvailabilityDTO, BranchDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { PurchasingOptions } from './purchasing-options-modal.store';
export interface PurchasingOptionsModalData {

View File

@@ -14,7 +14,6 @@ import {
} from './options';
import { PurchasingOptionsModalComponent } from './purchasing-options-modal.component';
import { PickUpDropdownComponent } from './pick-up-option/pick-up-dropdown/pick-up-dropdown.component';
import { PageCheckoutPipeModule } from '../../pipes/page-checkout-pipe.module';
import { ProductImageModule } from 'apps/cdn/product-image/src/public-api';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
@@ -24,6 +23,7 @@ import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { PurchasingOptionsModalPriceInputModule } from './price-input/purchasing-options-modal-price-input.module';
import { UiTooltipModule } from '@ui/tooltip';
import { UiCommonModule } from '@ui/common';
import { UiBranchDropdownModule } from '@ui/branch-dropdown';
@NgModule({
imports: [
@@ -40,6 +40,7 @@ import { UiCommonModule } from '@ui/common';
RouterModule,
PurchasingOptionsModalPriceInputModule,
UiTooltipModule,
UiBranchDropdownModule,
],
exports: [PurchasingOptionsModalComponent],
declarations: [
@@ -47,7 +48,6 @@ import { UiCommonModule } from '@ui/common';
B2BDeliveryOptionComponent,
TakeAwayOptionComponent,
PickUpOptionComponent,
PickUpDropdownComponent,
DeliveryOptionComponent,
DigDeliveryOptionComponent,
],

View File

@@ -9,7 +9,6 @@ import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, ShoppingCartItemDTO, VA
import { isBoolean, isNullOrUndefined, isString } from '@utils/common';
import { NEVER, Observable } from 'rxjs';
import { delay, filter, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { geoDistance } from '@utils/common';
export type PurchasingOptions = 'take-away' | 'pick-up' | 'delivery' | 'dig-delivery' | 'b2b-delivery' | 'download';
@@ -29,8 +28,6 @@ interface PurchasingOptionsModalState {
canAdd: boolean;
canAddError?: string;
canUpgrade: boolean;
// TODO: FilterBranch in der UI Component sortieren und filtern
filterResult?: BranchDTO[];
availabilities: { [key: string]: AvailabilityDTO };
customPrice?: number;
customVat?: VATType;
@@ -97,8 +94,6 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
readonly selectCanAddError = this.select((s) => s.canAddError);
readonly selectFilterResult = this.select((s) => s.filterResult);
readonly selectFetchingAvailability = this.select((s) => s.fetchingAvailability);
readonly selectMaxQuantityError = this.select((s) => s.maxQuantityError);
@@ -218,15 +213,12 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
readonly setBranch = this.updater((state, branch: BranchDTO) => {
this.loadAvailability();
const filterResult = state?.availableBranches;
filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, branch));
return {
...state,
branch,
availability: undefined,
canAdd: false,
canAddError: undefined,
filterResult,
};
});
@@ -317,14 +309,10 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
readonly setAvailableBranches = this.updater((state, availableBranches: BranchDTO[]) => {
const branch = state.branch || state.defaultBranch;
const userBranch = availableBranches.find((b) => b.id === branch.id);
const filterResult = availableBranches;
filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, userBranch));
return {
...state,
availableBranches,
branch,
filterResult,
};
});
@@ -352,38 +340,6 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
};
});
readonly setFilteredBranches = this.updater((state, filterValue?: string) => {
const branch = state.branch || state.defaultBranch;
if (!!filterValue) {
const filterResult = state.availableBranches.filter((b) => {
const name = b.name.toLowerCase();
const zipCode = b.address?.zipCode;
const city = b.address?.city?.toLowerCase();
if (!zipCode || !city) {
return name.indexOf(filterValue.toLowerCase()) >= 0;
} else {
return (
name.indexOf(filterValue.toLowerCase()) >= 0 ||
zipCode.indexOf(filterValue) >= 0 ||
city.indexOf(filterValue.toLowerCase()) >= 0
);
}
});
filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, branch));
return {
...state,
filterResult,
};
} else {
const filterResult = state.availableBranches;
filterResult.sort((a: BranchDTO, b: BranchDTO) => this.branchSorterFn(a, b, branch));
return {
...state,
filterResult,
};
}
});
loadBranches = this.effect((branchId$: Observable<number>) =>
branchId$.pipe(
switchMap((branchId) =>
@@ -476,19 +432,15 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
withLatestFrom(this.selectOlaAvailability, this.selectProcessId, this.selectOrderType),
switchMap(([_, availability, processId, orderType]) => {
this.patchState({ checkingCanAdd: true });
return this.checkoutService
.canAddItem({
processId,
availability,
orderType,
})
.pipe(
tapResponse(
(canAdd) => this.setCanAdd(canAdd),
(error: Error) => this.setCanAdd(error?.message)
),
tap((_) => this.patchState({ checkingCanAdd: false }))
);
return this.checkoutService.canAddItems({ processId, payload: [{ availabilities: [availability] }], orderType }).pipe(
tapResponse(
(response: any) => {
this.setCanAdd(response?.find((_) => true)?.status === 0 ? true : response?.find((_) => true)?.message);
},
(error: Error) => this.setCanAdd(error?.message)
),
tap((_) => this.patchState({ checkingCanAdd: false }))
);
})
)
);
@@ -515,13 +467,6 @@ export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOption
)
);
private branchSorterFn(a: BranchDTO, b: BranchDTO, userBranch: BranchDTO) {
return (
geoDistance(userBranch?.address?.geoLocation, a?.address?.geoLocation) -
geoDistance(userBranch?.address?.geoLocation, b?.address?.geoLocation)
);
}
readonly loadDefaultBranch = this.effect(($) =>
$.pipe(
switchMap((_) =>

View File

@@ -7,14 +7,7 @@
<p>
Möchten Sie den Artikel zurücklegen lassen oder sofort mitnehmen?
</p>
<span class="price" *ngIf="availability.price?.value?.value; else retailPrice">{{
availability.price?.value?.value | currency: availability.price?.value?.currency:'code'
}}</span>
<ng-template #retailPrice>
<span class="price" *ngIf="availability.retailPrice?.value?.value">{{
availability.retailPrice?.value?.value | currency: availability.retailPrice?.value?.currency:'code'
}}</span>
</ng-template>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<div class="grow"></div>
<div>
<button

View File

@@ -1,4 +1,6 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@@ -9,13 +11,17 @@ import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TakeAwayOptionComponent {
readonly item$ = this.purchasingOptionsModalStore.selectItem;
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this.purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['take-away']));
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['take-away']));
constructor(private purchasingOptionsModalStore: PurchasingOptionsModalStore) {}
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('take-away', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this.purchasingOptionsModalStore.setOption('take-away');
this._purchasingOptionsModalStore.setOption('take-away');
}
}

View File

@@ -1,9 +1,10 @@
import { NgModule } from '@angular/core';
import { PurchasingOptionsListModalModule } from './modals/purchasing-options-list-modal';
import { PurchasingOptionsModalModule } from './modals/purchasing-options-modal';
@NgModule({
imports: [PurchasingOptionsModalModule],
exports: [PurchasingOptionsModalModule],
imports: [PurchasingOptionsModalModule, PurchasingOptionsListModalModule],
exports: [PurchasingOptionsModalModule, PurchasingOptionsListModalModule],
})
export class PageCheckoutModalsModule {}

View File

@@ -1,6 +1,5 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { ApplicationService } from '@core/application';
import { map } from 'rxjs/operators';
@Component({
selector: 'page-checkout',

View File

@@ -67,7 +67,7 @@ export class TaskInfoComponent implements OnChanges {
return `${this.datePipe.transform(dateFrom, 'dd.MM.yy')} bis ${this.datePipe.transform(dateTo, 'dd.MM.yy')}`;
}
return `${this.datePipe.transform(dateFrom, 'dd.MM.yy HH:mm')} Uhr bis ${this.datePipe.transform(dateTo, 'dd.MM.yy HH:mm')} Uhr`;
return `${this.datePipe.transform(dateFrom, 'dd.MM.yy')} bis ${this.datePipe.transform(dateTo, 'dd.MM.yy')}`;
}
}

View File

@@ -12,7 +12,7 @@
</ng-container>
</div>
<ng-container *ngIf="itemType$ | async; let itemType">
<div class="indicator" *ngIf="isTask$ | async" [style.backgroundColor]="indicatorColor$ | async"></div>
<div class="indicator" *ngIf="(isTask$ | async) && !(hasUpdate$ | async)" [style.backgroundColor]="indicatorColor$ | async"></div>
<div class="icon" *ngIf="hasIcon$ | async">
<ui-icon icon="info" *ngIf="isInfoOrPreInfo$ | async" size="16px"></ui-icon>
<ui-icon icon="calendar" size="16px" *ngIf="isPreInfo$ | async"></ui-icon>

View File

@@ -51,6 +51,8 @@ export class TaskListItemComponent implements OnChanges {
map(([processingStatus, info]) => (info.successor && processingStatus.includes('Removed')) || info.predecessor)
);
hasUpdate$ = this.item$.pipe(map((item) => !!item.successor));
@HostBinding('class')
get completed() {
return this.domainTaskCalendarService.getProcessingStatusList(this.item);

View File

@@ -6,7 +6,6 @@
[(page)]="page"
(after-load-complete)="callBackFn($event)"
[fit-to-page]="true"
zoom-scale="page-height"
></pdf-viewer>
<button (click)="print()" class="cta-print" type="button">

View File

@@ -44,7 +44,8 @@ export class TaskCalendarStore extends ComponentStore<TaskCalendarState> {
(s) =>
this.dateAdapter.equals({ first: s.date, second: new Date(calendarIndicator.date), precision: 'day' }) &&
s.color === calendarIndicator.color
)
) &&
!item.successor // do not show color indicator for items with successor
) {
return [...agg, calendarIndicator];
}

View File

@@ -4,15 +4,20 @@
<div class="process-name-container pt-3" (click)="selectProcess(process)">
<span class="process-name">{{ process.name }}</span>
</div>
<ng-container>
<div class="cart-container" [ngClass]="{ download: cartBackgroundForDownload }" (click)="openCart(process)">
<ng-container *ngIf="{ length: cartCount$ | async }; let cartCount">
<div
[class.items-in-cart]="cartCount.length > 0"
class="cart-container"
[ngClass]="{ download: cartBackgroundForDownload }"
(click)="openCart(process)"
>
<lib-icon
mt="12px"
ml="15px"
width="17px"
height="16px"
name="Shopping_Cart"
*ngIf="!cartBackgroundForDownload && process.id === (currentProcessId$ | async)"
*ngIf="cartCount.length === 0 && !cartBackgroundForDownload && process.id === (currentProcessId$ | async)"
class="process-cart-icon"
></lib-icon>
<lib-icon
@@ -21,7 +26,7 @@
width="17px"
height="16px"
name="Shopping_Cart_Inactive"
*ngIf="!cartBackgroundForDownload && process.id !== (currentProcessId$ | async)"
*ngIf="cartCount.length === 0 && !cartBackgroundForDownload && process.id !== (currentProcessId$ | async)"
class="process-cart-icon"
></lib-icon>
<lib-icon
@@ -30,11 +35,16 @@
width="17px"
height="16px"
name="shopping_cart_white"
*ngIf="cartBackgroundForDownload"
*ngIf="cartCount.length > 0 || cartBackgroundForDownload"
class="process-cart-icon"
></lib-icon>
<div [@cartnumber]="cartanimation" class="pt-3 process-cart-number-container">
<span class="process-cart-number" [ngClass]="{ 'number-download': cartBackgroundForDownload }">{{ cartCount$ | async }}</span>
<span
[class.items-in-cart]="cartCount.length > 0"
class="process-cart-number"
[ngClass]="{ 'number-download': cartBackgroundForDownload }"
>{{ cartCount.length }}</span
>
</div>
</div>
</ng-container>

View File

@@ -126,3 +126,8 @@
.last {
padding-right: 0px;
}
.items-in-cart {
@apply bg-active-customer !important;
color: #fff !important;
}

View File

@@ -70,6 +70,10 @@
<div class="detail">
<div class="label">ISBN/EAN</div>
<div class="value">{{ orderItem.product?.ean }}</div>
</div>
<div class="detail">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
<button class="cta-more" *ngIf="(more$ | async) === false" (click)="setMore(true)">
Mehr <ui-icon size="15px" icon="arrow"></ui-icon>
</button>
@@ -83,10 +87,6 @@
<div class="label">Lieferant</div>
<div class="value">{{ orderItem.supplier }}</div>
</div>
<div class="detail">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
</div>
<div class="detail">
<div class="label">
<ng-container
@@ -160,40 +160,38 @@
<div class="goods-in-out-order-details-item-comment">
<label for="comment">Anmerkung</label>
<input #specialCommentInput type="text" name="comment" [formControl]="specialCommentControl" (keyup.enter)="saveSpecialComment()" />
<textarea
matInput
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
#specialCommentInput
(keydown.delete)="triggerResize()"
(keydown.backspace)="triggerResize()"
type="text"
name="comment"
[formControl]="specialCommentControl"
[class.inactive]="!specialCommentControl.dirty"
></textarea>
<button
type="reset"
class="clear"
*ngIf="specialCommentControl?.enabled && !!specialCommentControl.value?.length"
(click)="specialCommentControl.setValue(''); saveSpecialComment()"
>
<ui-icon icon="close" size="12px"></ui-icon>
</button>
<button
class="cta-save"
type="submit"
*ngIf="specialCommentControl?.enabled && specialCommentControl.dirty"
(click)="saveSpecialComment()"
>
Speichern
</button>
<button
class="cta-edit"
type="button"
(click)="specialCommentControl?.enable(); specialCommentInput?.focus()"
*ngIf="!specialCommentControl?.value && specialCommentControl?.disabled"
>
Hinzufügen
</button>
<button
class="cta-edit"
type="button"
(click)="specialCommentControl?.enable(); specialCommentInput?.focus()"
*ngIf="!!specialCommentControl?.value && specialCommentControl?.disabled"
>
Ändern
</button>
<div class="comment-actions">
<button
type="reset"
class="clear"
*ngIf="!!specialCommentControl.value?.length"
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
>
<ui-icon icon="close" size="12px"></ui-icon>
</button>
<button
class="cta-save"
type="submit"
*ngIf="specialCommentControl?.enabled && specialCommentControl.dirty"
(click)="saveSpecialComment()"
>
Speichern
</button>
</div>
</div>
</ng-container>

View File

@@ -59,14 +59,28 @@ button {
}
.goods-in-out-order-details-item-comment {
@apply flex flex-row items-center p-4 bg-white text-base font-bold;
@apply flex flex-row items-start p-4 bg-white text-base font-bold;
textarea {
@apply flex-grow bg-transparent border-none outline-none text-base mx-4;
resize: none;
}
textarea.inactive {
@apply text-warning font-bold;
@apply flex-grow bg-transparent border-none outline-none text-base mx-4 text-warning font-bold;
// ipad color fix
-webkit-text-fill-color: rgb(190, 129, 0);
opacity: 1;
}
input {
@apply flex-grow bg-transparent border-none outline-none text-base mx-4;
}
input:disabled {
input.inactive {
@apply text-warning font-bold;
@apply flex-grow bg-transparent border-none outline-none text-base mx-4 text-warning font-bold;
// ipad color fix
-webkit-text-fill-color: rgb(190, 129, 0);
opacity: 1;
@@ -79,6 +93,10 @@ button {
button.clear {
@apply text-inactive-customer;
}
.comment-actions {
@apply flex justify-center items-center;
}
}
.cta-more {

View File

@@ -1,4 +1,15 @@
import { Component, ChangeDetectionStrategy, Input, Output, EventEmitter, ChangeDetectorRef, OnDestroy, OnInit } from '@angular/core';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import {
Component,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
ChangeDetectorRef,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { FormControl } from '@angular/forms';
import { DomainOmsService, DomainReceiptService } from '@domain/oms';
import { HistoryComponent } from '@modal/history';
@@ -27,6 +38,8 @@ export interface SharedGoodsInOutOrderDetailsItemComponentState {
})
export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<SharedGoodsInOutOrderDetailsItemComponentState>
implements OnInit, OnDestroy {
@ViewChild('autosize') autosize: CdkTextareaAutosize;
@Input()
get orderItem() {
return this.get((s) => s.orderItem);
@@ -38,7 +51,6 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<Sh
this.patchState({ orderItem, quantity: orderItem?.quantity, receipts: [], more: false });
this.specialCommentControl.reset(orderItem?.specialComment);
this.specialCommentControl.disable();
// Add New OrderItem to selected list if selected was set to true by its input
if (this.get((s) => s.selected)) {
@@ -173,17 +185,15 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<Sh
);
async saveSpecialComment() {
this.specialCommentControl.disable();
const { orderId, orderItemId, orderItemSubsetId } = this.orderItem;
try {
this.specialCommentControl.reset(this.specialCommentControl.value);
const res = await this._omsService
.patchComment({ orderId, orderItemId, orderItemSubsetId, specialComment: this.specialCommentControl.value ?? '' })
.pipe(first())
.toPromise();
this.specialCommentControl.reset(this.specialCommentControl.value);
this.orderItem = { ...this.orderItem, specialComment: this.specialCommentControl.value ?? '' };
this.orderItemChange.next(this.orderItem);
this._host.updateOrderItems([this.orderItem]);
@@ -218,4 +228,8 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<Sh
const orderItems = this.order?.items;
return orderItems.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features?.orderType;
}
triggerResize() {
this.autosize.reset();
}
}

View File

@@ -18,6 +18,7 @@ import { UiSliderModule } from '@ui/slider';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { UiTooltipModule } from '@ui/tooltip';
import { TextFieldModule } from '@angular/cdk/text-field';
@NgModule({
imports: [
@@ -35,6 +36,7 @@ import { UiTooltipModule } from '@ui/tooltip';
UiSelectBulletModule,
UiSpinnerModule,
UiTooltipModule,
TextFieldModule,
],
exports: [
SharedGoodsInOutOrderDetailsComponent,

View File

@@ -10,3 +10,7 @@ import { Injectable } from '@angular/core';
export class AvConfiguration {
rootUrl: string = 'https://isa-test.paragon-data.net/ava/v4';
}
export interface AvConfigurationInterface {
rootUrl?: string;
}

View File

@@ -1,7 +1,7 @@
/* tslint:disable */
import { NgModule } from '@angular/core';
import { NgModule, ModuleWithProviders } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { AvConfiguration } from './av-configuration';
import { AvConfiguration, AvConfigurationInterface } from './av-configuration';
import { AvailabilityService } from './services/availability.service';
@@ -21,4 +21,16 @@ import { AvailabilityService } from './services/availability.service';
AvailabilityService
],
})
export class AvModule { }
export class AvModule {
static forRoot(customParams: AvConfigurationInterface): ModuleWithProviders<AvModule> {
return {
ngModule: AvModule,
providers: [
{
provide: AvConfiguration,
useValue: {rootUrl: customParams.rootUrl}
}
]
}
}
}

View File

@@ -2,10 +2,16 @@ export { ResponseArgsOfIEnumerableOfAvailabilityDTO } from './models/response-ar
export { AvailabilityDTO } from './models/availability-dto';
export { PriceDTO } from './models/price-dto';
export { PriceValueDTO } from './models/price-value-dto';
export { TouchedBase } from './models/touched-base';
export { VATValueDTO } from './models/vatvalue-dto';
export { VATType } from './models/vattype';
export { AvailabilityType } from './models/availability-type';
export { RangeDTO } from './models/range-dto';
export { ResponseArgs } from './models/response-args';
export { DialogOfString } from './models/dialog-of-string';
export { DialogSettings } from './models/dialog-settings';
export { DialogContentType } from './models/dialog-content-type';
export { KeyValueDTOOfStringAndString } from './models/key-value-dtoof-string-and-string';
export { IPublicUserInfo } from './models/ipublic-user-info';
export { ProblemDetails } from './models/problem-details';
export { AvailabilityRequestDTO } from './models/availability-request-dto';

View File

@@ -1,26 +1,29 @@
/* tslint:disable */
import { RangeDTO } from './range-dto';
import { PriceDTO } from './price-dto';
import { AvailabilityType } from './availability-type';
export interface AvailabilityDTO {
itemId?: number;
supplierProductNumber?: string;
requestReference?: string;
altAt?: string;
at?: string;
ean?: string;
shop?: number;
price?: PriceDTO;
supplier?: string;
supplierId?: number;
estimatedDelivery?: RangeDTO;
isPrebooked?: boolean;
itemId?: number;
logistician?: string;
logisticianId?: number;
orderReference?: string;
preferred?: number;
price?: PriceDTO;
qty?: number;
requestMessage?: string;
requestReference?: string;
requestStatusCode?: string;
requested?: string;
shop?: number;
ssc?: string;
sscText?: string;
qty?: number;
isPrebooked?: boolean;
at?: string;
altAt?: string;
status: AvailabilityType;
preferred?: number;
requested?: string;
requestStatusCode?: string;
requestMessage?: string;
supplier?: string;
supplierId?: number;
supplierProductNumber?: string;
}

View File

@@ -1,18 +1,18 @@
/* tslint:disable */
import { PriceDTO } from './price-dto';
export interface AvailabilityRequestDTO {
itemId?: string;
supplierProductNumber?: string;
availabilityReference?: string;
branchNumber?: string;
ean?: string;
qty: number;
estimatedShipping?: string;
itemId?: string;
name?: string;
orderCode?: string;
supplier?: string;
preBook?: boolean;
price?: PriceDTO;
ssc?: string;
estimatedShipping?: string;
qty: number;
shopId?: number;
branchNumber?: string;
availabilityReference?: string;
name?: string;
ssc?: string;
supplier?: string;
supplierProductNumber?: string;
}

View File

@@ -1,2 +1,2 @@
/* tslint:disable */
export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 1024 | 2048 | 4096 | 8192 | 16384;
export type AvailabilityType = 0 | 1 | 2 | 32 | 256 | 512 | 1024 | 2048 | 4096 | 8192 | 16384;

View File

@@ -0,0 +1,2 @@
/* tslint:disable */
export type DialogContentType = 0 | 1 | 2 | 4 | 8 | 16 | 32 | 64 | 128;

View File

@@ -0,0 +1,16 @@
/* tslint:disable */
import { KeyValueDTOOfStringAndString } from './key-value-dtoof-string-and-string';
import { DialogContentType } from './dialog-content-type';
import { DialogSettings } from './dialog-settings';
export interface DialogOfString {
actions?: Array<KeyValueDTOOfStringAndString>;
actionsRequired?: number;
area?: string;
content?: string;
contentType: DialogContentType;
description?: string;
displayTimeout?: number;
settings: DialogSettings;
subtitle?: string;
title?: string;
}

View File

@@ -0,0 +1,2 @@
/* tslint:disable */
export type DialogSettings = 0 | 1 | 2 | 4;

View File

@@ -2,6 +2,6 @@
export interface IPublicUserInfo {
alias?: string;
displayName?: string;
username?: string;
isAuthenticated: boolean;
username?: string;
}

View File

@@ -0,0 +1,11 @@
/* tslint:disable */
export interface KeyValueDTOOfStringAndString {
command?: string;
description?: string;
enabled?: boolean;
key?: string;
label?: string;
selected?: boolean;
sort?: number;
value?: string;
}

View File

@@ -1,7 +1,8 @@
/* tslint:disable */
import { TouchedBase } from './touched-base';
import { PriceValueDTO } from './price-value-dto';
import { VATValueDTO } from './vatvalue-dto';
export interface PriceDTO {
export interface PriceDTO extends TouchedBase{
value?: PriceValueDTO;
vat?: VATValueDTO;
}

View File

@@ -1,6 +1,7 @@
/* tslint:disable */
export interface PriceValueDTO {
value?: number;
import { TouchedBase } from './touched-base';
export interface PriceValueDTO extends TouchedBase{
currency?: string;
currencySymbol?: string;
value?: number;
}

View File

@@ -1,9 +1,10 @@
/* tslint:disable */
export interface ProblemDetails {
type?: string;
title?: string;
status?: number;
detail?: string;
extensions: {[key: string]: any};
instance?: string;
extensions?: {[key: string]: any};
status?: number;
title?: string;
type?: string;
[prop: string]: any;
}

View File

@@ -0,0 +1,5 @@
/* tslint:disable */
export interface RangeDTO {
start?: string;
stop?: string;
}

View File

@@ -1,6 +1,6 @@
/* tslint:disable */
import { ResponseArgs } from './response-args';
import { AvailabilityDTO } from './availability-dto';
export interface ResponseArgsOfIEnumerableOfAvailabilityDTO extends ResponseArgs {
export interface ResponseArgsOfIEnumerableOfAvailabilityDTO extends ResponseArgs{
result?: Array<AvailabilityDTO>;
}

View File

@@ -1,9 +1,11 @@
/* tslint:disable */
import { DialogOfString } from './dialog-of-string';
import { IPublicUserInfo } from './ipublic-user-info';
export interface ResponseArgs {
dialog?: DialogOfString;
error: boolean;
invalidProperties?: {[key: string]: string};
message?: string;
requestId?: number;
userInfo?: IPublicUserInfo;
invalidProperties?: {[key: string]: string};
}

Some files were not shown because too many files have changed in this diff Show More