mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
Merge branch 'develop' into split-screen-demo
This commit is contained in:
@@ -8,7 +8,7 @@ import {
|
||||
StoreCheckoutSupplierService,
|
||||
SupplierDTO,
|
||||
} from '@swagger/checkout';
|
||||
import { combineLatest, Observable, of } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, Observable, of } from 'rxjs';
|
||||
import {
|
||||
AvailabilityRequestDTO,
|
||||
AvailabilityService,
|
||||
@@ -21,11 +21,16 @@ import { isArray, memorize } from '@utils/common';
|
||||
import { LogisticianDTO, LogisticianService } from '@swagger/oms';
|
||||
import { ResponseArgsOfIEnumerableOfStockInfoDTO, StockDTO, StockInfoDTO, StockService } from '@swagger/remi';
|
||||
import { PriceDTO } from '@swagger/availability';
|
||||
import { AvailabilityByBranchDTO, ItemData } from './defs';
|
||||
import { AvailabilityByBranchDTO, ItemData, Ssc } from './defs';
|
||||
import { Availability } from './defs/availability';
|
||||
import { isEmpty } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class DomainAvailabilityService {
|
||||
// Ticket #3378 Keep Result List Items and Details Page SSC in sync
|
||||
sscs$ = new BehaviorSubject<Array<Ssc>>([]);
|
||||
sscsObs$ = this.sscs$.asObservable();
|
||||
|
||||
constructor(
|
||||
private _availabilityService: AvailabilityService,
|
||||
private _logisticanService: LogisticianService,
|
||||
@@ -284,7 +289,7 @@ export class DomainAvailabilityService {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
|
||||
const availability: AvailabilityDTO = {
|
||||
return {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
@@ -297,8 +302,8 @@ export class DomainAvailabilityService {
|
||||
supplierProductNumber: preferred?.supplierProductNumber,
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
priceMaintained: preferred?.priceMaintained,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
@@ -347,7 +352,7 @@ export class DomainAvailabilityService {
|
||||
const availabilities = r.result;
|
||||
const preferred = availabilities?.find((f) => f.preferred === 1);
|
||||
|
||||
const availability: AvailabilityDTO = {
|
||||
return {
|
||||
availabilityType: preferred?.status,
|
||||
ssc: preferred?.ssc,
|
||||
sscText: preferred?.sscText,
|
||||
@@ -359,8 +364,8 @@ export class DomainAvailabilityService {
|
||||
logistician: { id: preferred?.logisticianId },
|
||||
supplierInfo: preferred?.requestStatusCode,
|
||||
lastRequest: preferred?.requested,
|
||||
priceMaintained: preferred?.priceMaintained,
|
||||
};
|
||||
return availability;
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
@@ -453,30 +458,8 @@ export class DomainAvailabilityService {
|
||||
return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
|
||||
}
|
||||
|
||||
mapToOlaAvailability({
|
||||
availability,
|
||||
item,
|
||||
quantity,
|
||||
}: {
|
||||
availability: AvailabilityDTO;
|
||||
item: ItemDTO;
|
||||
quantity: number;
|
||||
}): OLAAvailabilityDTO {
|
||||
return {
|
||||
status: availability?.availabilityType,
|
||||
at: availability?.estimatedShippingDate,
|
||||
ean: item?.product?.ean,
|
||||
itemId: item?.id,
|
||||
format: item?.product?.format,
|
||||
isPrebooked: availability?.isPrebooked,
|
||||
logisticianId: availability?.logistician?.id,
|
||||
price: availability?.price,
|
||||
qty: quantity,
|
||||
ssc: availability?.ssc,
|
||||
sscText: availability?.sscText,
|
||||
supplierId: availability?.supplier?.id,
|
||||
supplierProductNumber: availability?.supplierProductNumber,
|
||||
};
|
||||
private _priceIsEmpty(price: PriceDTO) {
|
||||
return isEmpty(price?.value) || isEmpty(price?.vat);
|
||||
}
|
||||
|
||||
private _mapToTakeAwayAvailability({
|
||||
@@ -499,7 +482,7 @@ export class DomainAvailabilityService {
|
||||
inStock: inStock,
|
||||
supplierSSC: quantity <= inStock ? '999' : '',
|
||||
supplierSSCText: quantity <= inStock ? 'Filialentnahme' : '',
|
||||
price: price ?? stockInfo?.retailPrice,
|
||||
price: this._priceIsEmpty(price) ? stockInfo?.retailPrice : price,
|
||||
supplier: { id: supplier?.id },
|
||||
// TODO: Change after API Update
|
||||
// LH: 2021-03-09 preis Property hat nun ein Fallback auf retailPrice
|
||||
@@ -554,6 +537,7 @@ export class DomainAvailabilityService {
|
||||
supplierInfo: p?.requestStatusCode,
|
||||
lastRequest: p?.requested,
|
||||
itemId: p.itemId,
|
||||
priceMaintained: p.priceMaintained,
|
||||
},
|
||||
p,
|
||||
];
|
||||
@@ -561,7 +545,7 @@ export class DomainAvailabilityService {
|
||||
}
|
||||
}
|
||||
|
||||
private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]) {
|
||||
private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]): AvailabilityDTO[] {
|
||||
const preferred = availabilities.filter((f) => f.preferred === 1);
|
||||
return preferred.map((p) => {
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './availability-by-branch-dto';
|
||||
export * from './availability';
|
||||
export * from './item-data';
|
||||
export * from './ssc';
|
||||
|
||||
5
apps/domain/availability/src/lib/defs/ssc.ts
Normal file
5
apps/domain/availability/src/lib/defs/ssc.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Ssc {
|
||||
itemId?: number;
|
||||
ssc?: string;
|
||||
sscText?: string;
|
||||
}
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
StoreCheckoutPayerService,
|
||||
StoreCheckoutBranchService,
|
||||
ItemsResult,
|
||||
ShoppingCartItemDTO,
|
||||
} from '@swagger/checkout';
|
||||
import {
|
||||
DisplayOrderDTO,
|
||||
@@ -36,20 +37,45 @@ import {
|
||||
ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString,
|
||||
} 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, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { combineLatest, Observable, of, concat, isObservable, throwError, interval, zip, EMPTY, Subscription } from 'rxjs';
|
||||
import {
|
||||
bufferCount,
|
||||
catchError,
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
mergeMap,
|
||||
share,
|
||||
shareReplay,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import * as DomainCheckoutSelectors from './store/domain-checkout.selectors';
|
||||
import * as DomainCheckoutActions from './store/domain-checkout.actions';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainAvailabilityService, ItemData } from '@domain/availability';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { CustomerDTO, EntityDTOContainerOfAttributeDTO } from '@swagger/crm';
|
||||
import { CustomerDTO } from '@swagger/crm';
|
||||
import { Config } from '@core/config';
|
||||
import parseDuration from 'parse-duration';
|
||||
import { CheckoutEntity } from './store/defs/checkout.entity';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class DomainCheckoutService {
|
||||
get olaExpiration() {
|
||||
const exp = this._config.get('@domain/checkout.olaExpiration') ?? '5m';
|
||||
return parseDuration(exp);
|
||||
}
|
||||
|
||||
constructor(
|
||||
private store: Store<any>,
|
||||
private _config: Config,
|
||||
private applicationService: ApplicationService,
|
||||
private storeCheckoutService: StoreCheckoutService,
|
||||
private orderCheckoutService: OrderCheckoutService,
|
||||
@@ -119,14 +145,14 @@ export class DomainCheckoutService {
|
||||
})
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
tap((shoppingCart) =>
|
||||
tap((shoppingCart) => {
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.setShoppingCart({
|
||||
processId,
|
||||
shoppingCart,
|
||||
})
|
||||
)
|
||||
),
|
||||
);
|
||||
}),
|
||||
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
|
||||
)
|
||||
)
|
||||
@@ -249,11 +275,24 @@ export class DomainCheckoutService {
|
||||
shoppingCartItemId: number;
|
||||
availability: AvailabilityDTO;
|
||||
}) {
|
||||
return this._shoppingCartService.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
|
||||
shoppingCartId,
|
||||
shoppingCartItemId,
|
||||
availability,
|
||||
});
|
||||
return this._shoppingCartService
|
||||
.StoreCheckoutShoppingCartUpdateShoppingCartItemAvailability({
|
||||
shoppingCartId,
|
||||
shoppingCartItemId,
|
||||
availability,
|
||||
})
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
tap((shoppingCart) => {
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId({
|
||||
shoppingCartId,
|
||||
availability,
|
||||
shoppingCartItemId,
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
updateItemInShoppingCart({
|
||||
@@ -265,7 +304,7 @@ export class DomainCheckoutService {
|
||||
shoppingCartItemId: number;
|
||||
update: UpdateShoppingCartItemDTO;
|
||||
}): Observable<ShoppingCartDTO> {
|
||||
return this.getShoppingCart({ processId }).pipe(
|
||||
return this.getShoppingCart({ processId, latest: true }).pipe(
|
||||
first(),
|
||||
mergeMap((shoppingCart) =>
|
||||
this._shoppingCartService
|
||||
@@ -276,8 +315,21 @@ export class DomainCheckoutService {
|
||||
})
|
||||
.pipe(
|
||||
map((response) => response.result),
|
||||
tap((shoppingCart) => this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }))),
|
||||
tap((shoppingCart) => this.updateProcessCount(processId, shoppingCart?.items?.length))
|
||||
tap((shoppingCart) => {
|
||||
this.store.dispatch(DomainCheckoutActions.setShoppingCart({ processId, shoppingCart }));
|
||||
|
||||
if (update.availability) {
|
||||
this.store.dispatch(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory({
|
||||
processId,
|
||||
availability: update.availability,
|
||||
shoppingCartItemId,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.updateProcessCount(processId, shoppingCart?.items?.length);
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -547,6 +599,172 @@ export class DomainCheckoutService {
|
||||
);
|
||||
}
|
||||
|
||||
async refreshAvailability({
|
||||
processId,
|
||||
shoppingCartItemId,
|
||||
}: {
|
||||
processId: number;
|
||||
shoppingCartItemId: number;
|
||||
}): Promise<AvailabilityDTO> {
|
||||
const shoppingCart = await this.getShoppingCart({ processId }).pipe(first()).toPromise();
|
||||
const item = shoppingCart?.items.find((item) => item.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemData: ItemData = {
|
||||
ean: item.product.ean,
|
||||
itemId: Number(item.product.catalogProductNumber),
|
||||
price: item.availability.price,
|
||||
};
|
||||
|
||||
let availability: AvailabilityDTO;
|
||||
|
||||
switch (item.features.orderType) {
|
||||
case 'Abholung':
|
||||
const abholung = await this.availabilityService
|
||||
.getPickUpAvailability({
|
||||
item: itemData,
|
||||
branch: item.destination?.data?.targetBranch?.data,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
.toPromise();
|
||||
availability = abholung[0];
|
||||
break;
|
||||
case 'Rücklage':
|
||||
const ruecklage = await this.availabilityService
|
||||
.getTakeAwayAvailability({
|
||||
item: itemData,
|
||||
quantity: item.quantity,
|
||||
branch: item.destination?.data?.targetBranch?.data,
|
||||
})
|
||||
.toPromise();
|
||||
availability = ruecklage;
|
||||
break;
|
||||
case 'Download':
|
||||
const download = await this.availabilityService
|
||||
.getDownloadAvailability({
|
||||
item: itemData,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
availability = download;
|
||||
break;
|
||||
|
||||
case 'Versand':
|
||||
const versand = await this.availabilityService
|
||||
.getDeliveryAvailability({
|
||||
item: itemData,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
availability = versand;
|
||||
break;
|
||||
|
||||
case 'DIG-Versand':
|
||||
const digVersand = await this.availabilityService
|
||||
.getDigDeliveryAvailability({
|
||||
item: itemData,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
availability = digVersand;
|
||||
break;
|
||||
|
||||
case 'B2B-Versand':
|
||||
const b2bVersand = await this.availabilityService
|
||||
.getB2bDeliveryAvailability({
|
||||
item: itemData,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
availability = b2bVersand;
|
||||
break;
|
||||
}
|
||||
|
||||
await this.updateItemInShoppingCart({
|
||||
processId,
|
||||
update: { availability },
|
||||
shoppingCartItemId: item.id,
|
||||
}).toPromise();
|
||||
|
||||
return availability;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the availability of all items is valid
|
||||
* @param param0 Process Id
|
||||
* @returns true if the availability of all items is valid
|
||||
*/
|
||||
validateOlaStatus({ processId, interval }: { processId: number; interval?: number }): Observable<boolean> {
|
||||
return new Observable<boolean>((observer) => {
|
||||
const enity$ = this.store.select(DomainCheckoutSelectors.selectCheckoutEntityByProcessId, { processId });
|
||||
|
||||
const olaExpiration = this.olaExpiration;
|
||||
|
||||
let timeout: any;
|
||||
|
||||
let subscription: Subscription;
|
||||
|
||||
function check() {
|
||||
const now = Date.now();
|
||||
|
||||
subscription?.unsubscribe();
|
||||
subscription = enity$.pipe(take(1)).subscribe((entity) => {
|
||||
if (!entity || !entity.shoppingCart || !entity.shoppingCart.items) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ?? {};
|
||||
const shoppingCart = entity.shoppingCart;
|
||||
|
||||
const timestamps = shoppingCart.items
|
||||
?.map((i) => i.data)
|
||||
?.filter((item) => !!item?.features?.orderType)
|
||||
?.map((item) => itemAvailabilityTimestamp[`${item.id}_${item.features.orderType}`]);
|
||||
|
||||
if (timestamps?.length > 0) {
|
||||
const oldestTimestamp = Math.min(...timestamps);
|
||||
observer.next(now - oldestTimestamp < olaExpiration);
|
||||
}
|
||||
|
||||
timeout = setTimeout(() => {
|
||||
check.call(this);
|
||||
}, interval ?? olaExpiration / 10);
|
||||
});
|
||||
}
|
||||
|
||||
check.call(this);
|
||||
|
||||
return () => {
|
||||
subscription?.unsubscribe();
|
||||
clearTimeout(timeout);
|
||||
};
|
||||
}).pipe(distinctUntilChanged());
|
||||
}
|
||||
|
||||
validateAvailabilities({ processId }: { processId: number }): Observable<boolean> {
|
||||
return this.getShoppingCart({ processId }).pipe(
|
||||
map((shoppingCart) => {
|
||||
const items = shoppingCart?.items?.map((item) => item.data) || [];
|
||||
|
||||
return items.every((i) => this.availabilityService.isAvailable({ availability: i.availability }));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
checkoutIsValid({ processId }: { processId: number }): Observable<boolean> {
|
||||
const olaStatus$ = this.validateOlaStatus({ processId, interval: 250 });
|
||||
|
||||
const availabilities$ = this.validateAvailabilities({ processId });
|
||||
|
||||
return combineLatest([olaStatus$, availabilities$]).pipe(map(([olaStatus, availabilities]) => olaStatus && availabilities));
|
||||
}
|
||||
|
||||
completeCheckout({ processId }: { processId: number }): Observable<DisplayOrderDTO[]> {
|
||||
const refreshShoppingCart$ = this.getShoppingCart({ processId, latest: true }).pipe(first());
|
||||
const refreshCheckout$ = this.getCheckout({ processId, refresh: true }).pipe(first());
|
||||
@@ -700,21 +918,23 @@ export class DomainCheckoutService {
|
||||
)
|
||||
);
|
||||
|
||||
return updateDestination$
|
||||
.pipe(tap(console.log.bind(window, 'updateDestination$')))
|
||||
return of(undefined)
|
||||
.pipe(
|
||||
mergeMap((_) => updateDestination$.pipe(tap(console.log.bind(window, 'updateDestination$')))),
|
||||
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((_) => updateAvailabilities$.pipe(tap(console.log.bind(window, 'updateAvailabilities$'))))
|
||||
)
|
||||
.pipe(
|
||||
mergeMap((_) => setBuyer$.pipe(tap(console.log.bind(window, 'setBuyer$')))),
|
||||
mergeMap((_) => setNotificationChannels$.pipe(tap(console.log.bind(window, 'setNotificationChannels$')))),
|
||||
mergeMap((_) => setPayer$.pipe(tap(console.log.bind(window, 'setPayer$')))),
|
||||
mergeMap((_) => setPaymentType$.pipe(tap(console.log.bind(window, 'setPaymentType$')))),
|
||||
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$'))))
|
||||
)
|
||||
.pipe(mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$')))));
|
||||
mergeMap((_) => setDestination$.pipe(tap(console.log.bind(window, 'setDestination$')))),
|
||||
mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$'))))
|
||||
);
|
||||
}
|
||||
|
||||
completeKulturpassOrder({
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { BuyerDTO, CheckoutDTO, NotificationChannel, PayerDTO, ShippingAddressDTO, ShoppingCartDTO } from '@swagger/checkout';
|
||||
import {
|
||||
AvailabilityDTO,
|
||||
BuyerDTO,
|
||||
CheckoutDTO,
|
||||
NotificationChannel,
|
||||
PayerDTO,
|
||||
ShippingAddressDTO,
|
||||
ShoppingCartDTO,
|
||||
} from '@swagger/checkout';
|
||||
import { CustomerDTO } from '@swagger/crm';
|
||||
import { DisplayOrderDTO } from '@swagger/oms';
|
||||
|
||||
@@ -14,4 +22,5 @@ export interface CheckoutEntity {
|
||||
specialComment: string;
|
||||
notificationChannels: NotificationChannel;
|
||||
olaErrorIds: number[];
|
||||
itemAvailabilityTimestamp: Record<string, number>;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ShippingAddressDTO,
|
||||
BuyerDTO,
|
||||
PayerDTO,
|
||||
AvailabilityDTO,
|
||||
} from '@swagger/checkout';
|
||||
import { CustomerDTO } from '@swagger/crm';
|
||||
import { DisplayOrderDTO, DisplayOrderItemDTO } from '@swagger/oms';
|
||||
@@ -61,3 +62,13 @@ export const setSpecialComment = createAction(`${prefix} Set Agent Comment`, pro
|
||||
export const setOlaError = createAction(`${prefix} Set Ola Error`, props<{ processId: number; olaErrorIds: number[] }>());
|
||||
|
||||
export const setCustomer = createAction(`${prefix} Set Customer`, props<{ processId: number; customer: CustomerDTO }>());
|
||||
|
||||
export const addShoppingCartItemAvailabilityToHistory = createAction(
|
||||
`${prefix} Add Shopping Cart Item Availability To History`,
|
||||
props<{ processId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
|
||||
);
|
||||
|
||||
export const addShoppingCartItemAvailabilityToHistoryByShoppingCartId = createAction(
|
||||
`${prefix} Add Shopping Cart Item Availability To History By Shopping Cart Id`,
|
||||
props<{ shoppingCartId: number; shoppingCartItemId: number; availability: AvailabilityDTO }>()
|
||||
);
|
||||
|
||||
@@ -10,7 +10,22 @@ const _domainCheckoutReducer = createReducer(
|
||||
initialCheckoutState,
|
||||
on(DomainCheckoutActions.setShoppingCart, (s, { processId, shoppingCart }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
|
||||
const addedShoppingCartItems =
|
||||
shoppingCart?.items?.filter((item) => !entity.shoppingCart?.items?.find((i) => i.id === item.id))?.map((item) => item.data) ?? [];
|
||||
|
||||
entity.shoppingCart = shoppingCart;
|
||||
|
||||
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ? { ...entity.itemAvailabilityTimestamp } : {};
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (let shoppingCartItem of addedShoppingCartItems) {
|
||||
if (shoppingCartItem.features?.orderType) {
|
||||
entity.itemAvailabilityTimestamp[`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`] = now;
|
||||
}
|
||||
}
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
|
||||
@@ -100,7 +115,40 @@ const _domainCheckoutReducer = createReducer(
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.customer = customer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
})
|
||||
}),
|
||||
on(DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory, (s, { processId, shoppingCartItemId, availability }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp ? { ...entity?.itemAvailabilityTimestamp } : {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
|
||||
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
|
||||
const entity = getCheckoutEntityByShoppingCartId({ shoppingCartId, entities: s.entities });
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp ? { ...entity?.itemAvailabilityTimestamp } : {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export function domainCheckoutReducer(state, action) {
|
||||
@@ -123,8 +171,20 @@ function getOrCreateCheckoutEntity({ entities, processId }: { entities: Dictiona
|
||||
notificationChannels: 0,
|
||||
olaErrorIds: [],
|
||||
customer: undefined,
|
||||
// availabilityHistory: [],
|
||||
itemAvailabilityTimestamp: {},
|
||||
};
|
||||
}
|
||||
|
||||
return { ...entity };
|
||||
}
|
||||
|
||||
function getCheckoutEntityByShoppingCartId({
|
||||
entities,
|
||||
shoppingCartId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
shoppingCartId: number;
|
||||
}): CheckoutEntity {
|
||||
return Object.values(entities).find((entity) => entity.shoppingCart?.id === shoppingCartId);
|
||||
}
|
||||
|
||||
@@ -266,6 +266,16 @@
|
||||
"name": "text-decrease",
|
||||
"data": "m40-200 220-560h80l220 560h-75l-57-150H172l-57 150H40Zm156-214h208L302-685h-4L196-414Zm414-36v-60h310v60H610Z",
|
||||
"viewBox":"0 -960 960 960"
|
||||
},
|
||||
{
|
||||
"name": "calendar-today",
|
||||
"data": "M180-80q-24 0-42-18t-18-42v-620q0-24 18-42t42-18h65v-60h65v60h340v-60h65v60h65q24 0 42 18t18 42v620q0 24-18 42t-42 18H180Zm0-60h600v-430H180v430Zm0-490h600v-130H180v130Zm0 0v-130 130Z",
|
||||
"viewBox": "0 -960 960 960"
|
||||
},
|
||||
{
|
||||
"name": "apps",
|
||||
"data": "M226-160q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19Zm254 0q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19Zm254 0q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19ZM226-414q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19Zm254 0q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19Zm254 0q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19ZM226-668q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19Zm254 0q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19Zm254 0q-28 0-47-19t-19-47q0-28 19-47t47-19q28 0 47 19t19 47q0 28-19 47t-47 19Z",
|
||||
"viewBox": "0 -960 960 960"
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
|
||||
},
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-integration.paragon-data.net/isa/v1"
|
||||
},
|
||||
|
||||
@@ -47,6 +47,9 @@
|
||||
"@swagger/wws": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/wws/v1"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "30s"
|
||||
},
|
||||
"hubs": {
|
||||
"notifications": {
|
||||
"url": "https://isa-test.paragon-data.net/isa/v1/rt",
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa.paragon-systems.de/isa/v1"
|
||||
},
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-staging.paragon-systems.de/isa/v1"
|
||||
},
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
"@core/logger": {
|
||||
"logLevel": "debug"
|
||||
},
|
||||
"@domain/checkout": {
|
||||
"olaExpiration": "5m"
|
||||
},
|
||||
"@swagger/isa": {
|
||||
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply text-[#0556B4];
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<a [routerLink]="route?.path" [queryParams]="route?.queryParams">
|
||||
<ng-content></ng-content>
|
||||
</a>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-details-text-link',
|
||||
templateUrl: 'article-details-text-link.component.html',
|
||||
styleUrls: ['article-details-text-link.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-article-details-text-link' },
|
||||
standalone: true,
|
||||
imports: [RouterLink],
|
||||
})
|
||||
export class ArticleDetailsTextLinkComponent {
|
||||
@Input()
|
||||
route: { path: string[]; queryParams?: Record<string, string> };
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply block whitespace-pre-line;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<ng-container *ngFor="let line of lines">
|
||||
<ng-container [ngSwitch]="line | lineType">
|
||||
<ng-container *ngSwitchCase="'reihe'">
|
||||
<page-article-details-text-link *ngFor="let reihe of getReihen(line)" [route]="reihe | reiheRoute">
|
||||
{{ reihe }}
|
||||
</page-article-details-text-link>
|
||||
<br />
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault> {{ line }} <br /> </ng-container>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { TextDTO } from '@swagger/cat';
|
||||
import { ArticleDetailsTextLinkComponent } from './article-details-text-link.component';
|
||||
import { NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault } from '@angular/common';
|
||||
import { LineTypePipe } from './line-type.pipe';
|
||||
import { ReiheRoutePipe } from './reihe-route.pipe';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-details-text',
|
||||
templateUrl: 'article-details-text.component.html',
|
||||
styleUrls: ['article-details-text.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-article-details-text' },
|
||||
standalone: true,
|
||||
imports: [ArticleDetailsTextLinkComponent, NgFor, NgSwitch, NgSwitchCase, NgSwitchDefault, LineTypePipe, ReiheRoutePipe],
|
||||
})
|
||||
export class ArticleDetailsTextComponent {
|
||||
@Input()
|
||||
text: TextDTO;
|
||||
|
||||
get lines() {
|
||||
return this.text?.value?.split('\n');
|
||||
}
|
||||
|
||||
constructor() {}
|
||||
|
||||
getReihen(line: string): string[] {
|
||||
let splittedReihen = line?.split(';');
|
||||
return splittedReihen?.map((reihe, index) => {
|
||||
if (splittedReihen?.length !== index + 1) {
|
||||
return reihe + ';';
|
||||
}
|
||||
return reihe;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({
|
||||
name: 'lineType',
|
||||
standalone: true,
|
||||
pure: true,
|
||||
})
|
||||
export class LineTypePipe implements PipeTransform {
|
||||
transform(value: string, ...args: any[]): 'text' | 'reihe' {
|
||||
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
|
||||
const reihe = REIHE_REGEX.exec(value)?.[1];
|
||||
return reihe ? 'reihe' : 'text';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { isEqual } from 'lodash';
|
||||
import { Subscription, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
import { distinctUntilChanged } from 'rxjs/operators';
|
||||
|
||||
@Pipe({
|
||||
name: 'reiheRoute',
|
||||
standalone: true,
|
||||
pure: false,
|
||||
})
|
||||
export class ReiheRoutePipe implements PipeTransform, OnDestroy {
|
||||
private subscription: Subscription;
|
||||
|
||||
value$ = new BehaviorSubject<string>('');
|
||||
|
||||
result: { path: string[]; queryParams?: Record<string, string> };
|
||||
|
||||
constructor(
|
||||
private navigation: ProductCatalogNavigationService,
|
||||
private application: ApplicationService,
|
||||
private cdr: ChangeDetectorRef
|
||||
) {
|
||||
this.subscription = combineLatest([this.application.activatedProcessId$, this.value$])
|
||||
.pipe(distinctUntilChanged(isEqual))
|
||||
.subscribe(([processId, value]) => {
|
||||
const REIHE_REGEX = /[";]|Reihe:/g; // Entferne jedes Semikolon, Anführungszeichen und den String Reihe:
|
||||
const reihe = value?.replace(REIHE_REGEX, '')?.trim();
|
||||
|
||||
if (!reihe) {
|
||||
this.result = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const main_qs = reihe.split('/')[0];
|
||||
|
||||
const path = this.navigation.getArticleSearchResultsPath(processId).path;
|
||||
|
||||
this.result = {
|
||||
path,
|
||||
queryParams: {
|
||||
main_qs,
|
||||
main_serial: 'serial',
|
||||
},
|
||||
};
|
||||
|
||||
this.cdr.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscription?.unsubscribe();
|
||||
this.value$?.unsubscribe();
|
||||
}
|
||||
|
||||
transform(value: string, ...args: any[]) {
|
||||
this.value$.next(value);
|
||||
|
||||
return this.result;
|
||||
|
||||
// const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/g;
|
||||
// const reihe = REIHE_REGEX.exec(value)?.[1];
|
||||
|
||||
// this.navigation.getArticleSearchResultsPath(this.)
|
||||
|
||||
// return reihe ? `/search?query=${reihe}` : null;
|
||||
}
|
||||
}
|
||||
@@ -103,28 +103,16 @@
|
||||
<div class="page-article-details__product-publication">{{ publicationDate$ | async }}</div>
|
||||
</div>
|
||||
|
||||
<div class="page-article-details__product-price-info flex flex-col mb-4">
|
||||
<div
|
||||
class="page-article-details__product-price font-bold text-xl self-end"
|
||||
*ngIf="item.catalogAvailability?.price?.value?.value; else retailPrice"
|
||||
>
|
||||
{{ item.catalogAvailability?.price?.value?.value | currency: item.catalogAvailability?.price?.value?.currency:'code' }}
|
||||
<div class="page-article-details__product-price-info flex flex-col mb-4 flex-nowrap self-end">
|
||||
<div class="page-article-details__product-price font-bold text-xl self-end" *ngIf="price$ | async; let price">
|
||||
{{ price?.value?.value | currency: price?.value?.currency:'code' }}
|
||||
</div>
|
||||
<div *ngIf="price$ | async; let price" class="page-article-details__product-price-bound self-end">
|
||||
{{ price?.vat?.vatType | vat: (priceMaintained$ | async) }}
|
||||
</div>
|
||||
<ng-template #retailPrice>
|
||||
<div
|
||||
class="page-article-details__product-price font-bold text-xl self-end"
|
||||
*ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability"
|
||||
>
|
||||
{{ takeAwayAvailability?.retailPrice?.value?.value | currency: takeAwayAvailability?.retailPrice?.value?.currency:'code' }}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<div class="page-article-details__product-points self-end" *ngIf="store.promotionPoints$ | async; let promotionPoints">
|
||||
{{ promotionPoints }} Lesepunkte
|
||||
</div>
|
||||
|
||||
<!-- TODO: Ticket PREISGEBUNDEN -->
|
||||
<div class="page-article-details__product-price-bound self-end"></div>
|
||||
</div>
|
||||
|
||||
<div class="page-article-details__product-origin-infos flex flex-col mb-4">
|
||||
@@ -137,21 +125,23 @@
|
||||
|
||||
<div class="page-article-details__product-stock flex justify-end items-center">
|
||||
<div class="h-5 w-16 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="store.fetchingTakeAwayAvailability$ | async"></div>
|
||||
<div
|
||||
<button
|
||||
class="flex flex-row py-4 pl-4"
|
||||
type="button"
|
||||
[uiOverlayTrigger]="tooltip"
|
||||
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
|
||||
(click)="showTooltip()"
|
||||
*ngIf="!(store.fetchingTakeAwayAvailability$ | async)"
|
||||
>
|
||||
<ng-container *ngIf="store.takeAwayAvailability$ | async; let takeAwayAvailability">
|
||||
<ui-icon class="mr-2 mb-1" icon="home" size="15px"></ui-icon>
|
||||
<span class="font-bold text-p3">{{ takeAwayAvailability.inStock || 0 }}x</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
</button>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-12" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
|
||||
<div class="page-article-details__product-ean-specs flex flex-col">
|
||||
<div class="page-article-details__product-ean" data-name="product-ean">{{ item.product?.ean }}</div>
|
||||
@@ -234,7 +224,7 @@
|
||||
<div class="page-article-details__ssc flex justify-end my-2 font-bold text-lg">
|
||||
<div class="w-52 h-px-20 bg-[#e6eff9] animate-[load_0.75s_linear_infinite]" *ngIf="fetchingAvailabilities$ | async"></div>
|
||||
<ng-container *ngIf="!(fetchingAvailabilities$ | async)">
|
||||
<div *ngIf="store.sscText$ | async; let sscText">
|
||||
<div class="text-right" *ngIf="store.sscText$ | async; let sscText">
|
||||
{{ sscText }}
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -325,9 +315,7 @@
|
||||
<hr class="bg-[#E6EFF9] border-t-2 my-3" />
|
||||
|
||||
<div #description class="page-article-details__product-description flex flex-col flex-grow" *ngIf="item.texts?.length > 0">
|
||||
<div class="whitespace-pre-line">
|
||||
{{ item.texts[0].value }}
|
||||
</div>
|
||||
<page-article-details-text [text]="item.texts[0]"> </page-article-details-text>
|
||||
|
||||
<button
|
||||
class="font-bold flex flex-row text-[#0556B4] items-center mt-2"
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
'image contributors contributors contributors'
|
||||
'image title title print'
|
||||
'image title title .'
|
||||
'image misc misc price'
|
||||
'image misc misc price'
|
||||
'image misc price price'
|
||||
'image misc price price'
|
||||
'image origin origin stock'
|
||||
'image origin origin stock'
|
||||
'image specs availabilities availabilities'
|
||||
|
||||
@@ -22,6 +22,7 @@ import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-mod
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
@Component({
|
||||
selector: 'page-article-details',
|
||||
@@ -100,23 +101,6 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
switchMap((processId) => this.applicationService.getSelectedBranch$(processId))
|
||||
);
|
||||
|
||||
stockTooltipText$ = combineLatest([this.store.branch$, this.selectedBranchId$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
if (defaultBranch?.branchType === 4) {
|
||||
if (!selectedBranch) {
|
||||
return 'Wählen Sie eine Filiale aus, um den Bestand zu sehen.';
|
||||
}
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
} else {
|
||||
if (selectedBranch && defaultBranch.id !== selectedBranch?.id) {
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}),
|
||||
shareReplay(1)
|
||||
);
|
||||
|
||||
get isTablet$() {
|
||||
return this._environment.matchTablet$;
|
||||
}
|
||||
@@ -134,6 +118,58 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
return this.detailsContainer?.nativeElement;
|
||||
}
|
||||
|
||||
stockTooltipText$ = combineLatest([this.store.defaultBranch$, this.selectedBranchId$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
if (defaultBranch?.branchType !== 4 && selectedBranch && defaultBranch.id !== selectedBranch?.id) {
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
);
|
||||
|
||||
priceMaintained$ = combineLatest([
|
||||
this.store.takeAwayAvailability$,
|
||||
this.store.deliveryAvailability$,
|
||||
this.store.deliveryDigAvailability$,
|
||||
this.store.deliveryB2BAvailability$,
|
||||
]).pipe(
|
||||
map((availabilities) => {
|
||||
return availabilities?.some((availability) => availability?.priceMaintained) ?? false;
|
||||
})
|
||||
);
|
||||
|
||||
price$ = combineLatest([
|
||||
this.store.item$,
|
||||
this.store.takeAwayAvailability$,
|
||||
this.store.deliveryAvailability$,
|
||||
this.store.deliveryDigAvailability$,
|
||||
this.store.deliveryB2BAvailability$,
|
||||
]).pipe(
|
||||
map(([item, takeAway, delivery, deliveryDig, deliveryB2B]) => {
|
||||
if (item?.catalogAvailability?.price?.value?.value) {
|
||||
return item?.catalogAvailability?.price;
|
||||
}
|
||||
|
||||
if (takeAway?.price?.value?.value) {
|
||||
return takeAway.price;
|
||||
}
|
||||
|
||||
if (delivery?.price?.value?.value) {
|
||||
return delivery.price;
|
||||
}
|
||||
|
||||
if (deliveryDig?.price?.value?.value) {
|
||||
return deliveryDig.price;
|
||||
}
|
||||
|
||||
if (deliveryB2B?.price?.value?.value) {
|
||||
return deliveryB2B.price;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
constructor(
|
||||
public readonly applicationService: ApplicationService,
|
||||
private activatedRoute: ActivatedRoute,
|
||||
@@ -149,7 +185,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
private _checkoutNavigationService: CheckoutNavigationService,
|
||||
private _environment: EnvironmentService,
|
||||
private _router: Router,
|
||||
private _domainCheckoutService: DomainCheckoutService
|
||||
private _domainCheckoutService: DomainCheckoutService,
|
||||
private _store: Store
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -231,6 +268,14 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
async showTooltip() {
|
||||
const text = await this.stockTooltipText$.pipe(first()).toPromise();
|
||||
if (!text) {
|
||||
// Show Tooltip attached to branch selector dropdown
|
||||
this._store.dispatch({ type: 'OPEN_TOOLTIP_NO_BRANCH_SELECTED' });
|
||||
}
|
||||
}
|
||||
|
||||
async updateBreadcrumbDesktop(item: ItemDTO) {
|
||||
const crumbs = await this.breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog', 'details'])
|
||||
|
||||
@@ -12,6 +12,7 @@ import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { ArticleDetailsTextComponent } from './article-details-text/article-details-text.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -26,6 +27,7 @@ import { IconModule } from '@shared/components/icon';
|
||||
IconModule,
|
||||
PipesModule,
|
||||
OrderDeadlinePipeModule,
|
||||
ArticleDetailsTextComponent,
|
||||
],
|
||||
exports: [ArticleDetailsComponent, ArticleRecommendationsComponent],
|
||||
declarations: [ArticleDetailsComponent, ArticleRecommendationsComponent],
|
||||
|
||||
@@ -61,8 +61,12 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
|
||||
return this.get((s) => s.branch);
|
||||
}
|
||||
|
||||
get defaultBranch$() {
|
||||
return this.domainAvailabilityService.getDefaultBranch();
|
||||
}
|
||||
|
||||
readonly branch$ = this.select((s) => s.branch).pipe(
|
||||
withLatestFrom(this.domainAvailabilityService.getDefaultBranch()),
|
||||
withLatestFrom(this.defaultBranch$),
|
||||
map(([selectedBranch, defaultBranch]) => selectedBranch ?? defaultBranch)
|
||||
);
|
||||
|
||||
@@ -267,7 +271,8 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
|
||||
this.deliveryB2BAvailability$,
|
||||
this.downloadAvailability$,
|
||||
]).pipe(
|
||||
map(([item, isDownload, pickupAvailability, deliveryDigAvailability, deliveryB2BAvailability, downloadAvailability]) => {
|
||||
withLatestFrom(this.domainAvailabilityService.sscs$),
|
||||
map(([[item, isDownload, pickupAvailability, deliveryDigAvailability, deliveryB2BAvailability, downloadAvailability], sscs]) => {
|
||||
let availability: AvailabilityDTO;
|
||||
|
||||
if (isDownload) {
|
||||
@@ -282,15 +287,30 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
|
||||
}
|
||||
}
|
||||
|
||||
let ssc = '';
|
||||
let sscText = 'Keine Lieferanten vorhanden';
|
||||
|
||||
if (item?.catalogAvailability?.supplier === 'S' && !isDownload) {
|
||||
return [item?.catalogAvailability?.ssc, item?.catalogAvailability?.sscText].filter((f) => !!f).join(' - ');
|
||||
ssc = item?.catalogAvailability?.ssc;
|
||||
sscText = item?.catalogAvailability?.sscText;
|
||||
|
||||
return [ssc, sscText].filter((f) => !!f).join(' - ');
|
||||
}
|
||||
|
||||
if (availability?.ssc || availability?.sscText) {
|
||||
return [availability?.ssc, availability?.sscText].filter((f) => !!f).join(' - ');
|
||||
ssc = availability?.ssc;
|
||||
sscText = availability?.sscText;
|
||||
|
||||
const sscExists = !!sscs?.find((ssc) => !!item?.id && ssc?.itemId === item.id);
|
||||
const sscEqualsCatalogSsc = ssc === item.catalogAvailability.ssc && sscText === item.catalogAvailability.sscText;
|
||||
|
||||
// To keep result list in sync with details page
|
||||
if (!sscExists && !sscEqualsCatalogSsc) {
|
||||
this.domainAvailabilityService.sscs$.next([...sscs, { itemId: item?.id, ssc, sscText }]);
|
||||
}
|
||||
}
|
||||
|
||||
return 'Keine Lieferanten vorhanden';
|
||||
return [ssc, sscText].filter((f) => !!f).join(' - ');
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
|
||||
.cta-wrapper {
|
||||
@apply text-center whitespace-nowrap absolute bottom-8 left-0 w-full;
|
||||
@apply text-center whitespace-nowrap absolute bottom-8 left-1/2 -translate-x-1/2;
|
||||
}
|
||||
|
||||
.cta-reset-filter,
|
||||
|
||||
@@ -81,33 +81,29 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start"
|
||||
<button
|
||||
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start flex flex-row items-center justify-center"
|
||||
[class.justify-self-end]="!mainOutletActive"
|
||||
[uiOverlayTrigger]="tooltip"
|
||||
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
|
||||
type="button"
|
||||
(click)="$event.stopPropagation(); $event.preventDefault(); showTooltip()"
|
||||
>
|
||||
<ui-icon class="mr-[0.125rem] -mt-[0.275rem]" icon="home" size="1rem"></ui-icon>
|
||||
<ng-container *ngIf="isOrderBranch$ | async">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<ui-icon class="-mt-[0.1875rem]" icon="home" size="1em"></ui-icon>
|
||||
<span
|
||||
*ngIf="inStock$ | async; let stock"
|
||||
[class.skeleton]="stock.inStock === undefined"
|
||||
class="min-w-[1rem] text-right inline-block"
|
||||
>{{ stock?.inStock }}</span
|
||||
>
|
||||
<span>x</span>
|
||||
</div>
|
||||
<span
|
||||
*ngIf="inStock$ | async; let stock"
|
||||
[class.skeleton]="stock.inStock === undefined"
|
||||
class="min-w-[0.75rem] text-right inline-block"
|
||||
>{{ stock?.inStock }}</span
|
||||
>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!(isOrderBranch$ | async)">
|
||||
<div class="flex flex-row items-center justify-between z-dropdown">
|
||||
<ui-icon class="block" icon="home" size="1em"></ui-icon>
|
||||
<span class="min-w-[1rem] text-center inline-block">-</span>
|
||||
<span>x</span>
|
||||
</div>
|
||||
<span class="min-w-[1rem] text-center inline-block">-</span>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
|
||||
<span>x</span>
|
||||
</button>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-12" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
|
||||
@@ -115,10 +111,10 @@
|
||||
class="page-search-result-item__item-ssc desktop-small:text-p3 w-full text-right overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
[class.page-search-result-item__item-ssc-main]="mainOutletActive"
|
||||
>
|
||||
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive">
|
||||
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
|
||||
</div>
|
||||
<strong>{{ item?.catalogAvailability?.ssc }}</strong> - {{ item?.catalogAvailability?.sscText }}
|
||||
<ng-container *ngIf="ssc$ | async; let ssc">
|
||||
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive">{{ ssc?.ssc }} - {{ ssc?.sscText }}</div>
|
||||
<strong>{{ ssc?.ssc }}</strong> - {{ ssc?.sscText }}
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -8,9 +8,10 @@ import { ItemDTO } from '@swagger/cat';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { isEqual } from 'lodash';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { debounceTime, switchMap, map, shareReplay, filter } from 'rxjs/operators';
|
||||
import { debounceTime, switchMap, map, filter, first } from 'rxjs/operators';
|
||||
import { ArticleSearchService } from '../article-search.store';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
export interface SearchResultItemComponentState {
|
||||
item?: ItemDTO;
|
||||
@@ -105,15 +106,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
|
||||
stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
if (defaultBranch?.branchType === 4) {
|
||||
if (!selectedBranch) {
|
||||
return 'Wählen Sie eine Filiale aus, um den Bestand zu sehen.';
|
||||
}
|
||||
if (defaultBranch?.branchType !== 4 && selectedBranch && defaultBranch.id !== selectedBranch?.id) {
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
} else {
|
||||
if (selectedBranch && defaultBranch.id !== selectedBranch?.id) {
|
||||
return 'Sie sehen den Bestand einer anderen Filiale.';
|
||||
}
|
||||
}
|
||||
return '';
|
||||
})
|
||||
@@ -127,6 +121,17 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
)
|
||||
);
|
||||
|
||||
ssc$ = this._availability.sscsObs$.pipe(
|
||||
debounceTime(100),
|
||||
map((sscs) => {
|
||||
const updatedSsc = sscs?.find((ssc) => this.item?.id === ssc?.itemId);
|
||||
return {
|
||||
ssc: updatedSsc?.ssc ?? this.item?.catalogAvailability?.ssc,
|
||||
sscText: updatedSsc?.sscText ?? this.item?.catalogAvailability?.sscText,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
constructor(
|
||||
private _dateAdapter: DateAdapter,
|
||||
private _datePipe: DatePipe,
|
||||
@@ -136,7 +141,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
private _availability: DomainAvailabilityService,
|
||||
private _environment: EnvironmentService,
|
||||
private _navigationService: ProductCatalogNavigationService,
|
||||
private _elRef: ElementRef<HTMLElement>
|
||||
private _elRef: ElementRef<HTMLElement>,
|
||||
private _store: Store
|
||||
) {
|
||||
super({
|
||||
selected: false,
|
||||
@@ -158,6 +164,14 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
|
||||
// }
|
||||
}
|
||||
|
||||
async showTooltip() {
|
||||
const text = await this.stockTooltipText$.pipe(first()).toPromise();
|
||||
if (!text) {
|
||||
// Show Tooltip attached to branch selector dropdown
|
||||
this._store.dispatch({ type: 'OPEN_TOOLTIP_NO_BRANCH_SELECTED' });
|
||||
}
|
||||
}
|
||||
|
||||
@HostBinding('style') get class() {
|
||||
return this.mainOutletActive ? { height: '6.125rem' } : '';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<shared-breadcrumb class="mb-5 desktop-small:mb-9" [key]="activatedProcessId$ | async" [tags]="['catalog']">
|
||||
<shared-branch-selector
|
||||
[uiOverlayTrigger]="tooltip"
|
||||
[overlayTriggerDisabled]="stockTooltipDisabled"
|
||||
[filterCurrentBranch]="!!auth.hasRole('Store')"
|
||||
[orderBy]="auth.hasRole('Store') ? 'distance' : 'name'"
|
||||
[branchType]="1"
|
||||
@@ -7,6 +9,9 @@
|
||||
(valueChange)="patchProcessData($event)"
|
||||
>
|
||||
</shared-branch-selector>
|
||||
<ui-tooltip #tooltip yPosition="below" xPosition="after" [xOffset]="-263" [yOffset]="4" [closeable]="true">
|
||||
{{ stockTooltipText$ | async }}
|
||||
</ui-tooltip>
|
||||
</shared-breadcrumb>
|
||||
|
||||
<shared-splitscreen></shared-splitscreen>
|
||||
|
||||
@@ -5,9 +5,12 @@ import { EnvironmentService } from '@core/environment';
|
||||
import { BranchSelectorComponent } from '@shared/components/branch-selector';
|
||||
import { BreadcrumbComponent } from '@shared/components/breadcrumb';
|
||||
import { BranchDTO } from '@swagger/checkout';
|
||||
import { UiOverlayTriggerDirective } from '@ui/common';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { fromEvent, Observable, Subject } from 'rxjs';
|
||||
import { combineLatest, fromEvent, Observable, Subject } from 'rxjs';
|
||||
import { first, map, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import { ActionsSubject } from '@ngrx/store';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
|
||||
@Component({
|
||||
selector: 'page-catalog',
|
||||
@@ -22,6 +25,8 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
activatedProcessId$: Observable<string>;
|
||||
selectedBranch$: Observable<BranchDTO>;
|
||||
|
||||
@ViewChild(UiOverlayTriggerDirective) branchInputNoBranchSelectedTrigger: UiOverlayTriggerDirective;
|
||||
|
||||
get branchSelectorWidth() {
|
||||
return `${this.breadcrumbRef?.nativeElement?.clientWidth}px`;
|
||||
}
|
||||
@@ -36,21 +41,50 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
|
||||
return this._environmentService.matchDesktopLarge$;
|
||||
}
|
||||
|
||||
defaultBranch$ = this._availability.getDefaultBranch();
|
||||
|
||||
stockTooltipText$: Observable<string>;
|
||||
|
||||
stockTooltipDisabled$: Observable<boolean>;
|
||||
|
||||
get stockTooltipDisabled() {
|
||||
return this.branchInputNoBranchSelectedTrigger?.opened ? false : true;
|
||||
}
|
||||
|
||||
constructor(
|
||||
public application: ApplicationService,
|
||||
private _availability: DomainAvailabilityService,
|
||||
private _uiModal: UiModalService,
|
||||
public auth: AuthService,
|
||||
private _environmentService: EnvironmentService,
|
||||
private _renderer: Renderer2
|
||||
private _renderer: Renderer2,
|
||||
private _actions: ActionsSubject
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.activatedProcessId$ = this.application.activatedProcessId$.pipe(map((processId) => String(processId)));
|
||||
|
||||
this.selectedBranch$ = this.activatedProcessId$.pipe(switchMap((processId) => this.application.getSelectedBranch$(Number(processId))));
|
||||
|
||||
this.stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranch$]).pipe(
|
||||
map(([defaultBranch, selectedBranch]) => {
|
||||
if (defaultBranch?.branchType === 4 && !selectedBranch) {
|
||||
return 'Bitte wählen Sie eine Filiale aus, um den Bestand zu sehen.';
|
||||
} else if (defaultBranch?.branchType !== 4 && !selectedBranch) {
|
||||
return 'Bitte wählen Sie eine Filiale aus, um den Bestand einer anderen Filiale zu sehen';
|
||||
}
|
||||
return '';
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this._actions.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.stockTooltipText$)).subscribe(([action, text]) => {
|
||||
if (action.type === 'OPEN_TOOLTIP_NO_BRANCH_SELECTED' && !!text) {
|
||||
this.branchInputNoBranchSelectedTrigger.open();
|
||||
}
|
||||
});
|
||||
|
||||
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
|
||||
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.isTablet$))
|
||||
.subscribe(([_, isTablet]) => {
|
||||
|
||||
@@ -7,6 +7,8 @@ import { ArticleSearchModule } from './article-search/article-search.module';
|
||||
import { PageCatalogRoutingModule } from './page-catalog-routing.module';
|
||||
import { PageCatalogComponent } from './page-catalog.component';
|
||||
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -17,6 +19,8 @@ import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
|
||||
BreadcrumbModule,
|
||||
BranchSelectorComponent,
|
||||
SharedSplitscreenComponent,
|
||||
UiCommonModule,
|
||||
UiTooltipModule,
|
||||
],
|
||||
exports: [],
|
||||
declarations: [PageCatalogComponent],
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { TrimPipe } from './trim.pipe';
|
||||
import { VatPipe } from './vat.pipe';
|
||||
|
||||
@NgModule({
|
||||
imports: [],
|
||||
exports: [TrimPipe],
|
||||
declarations: [TrimPipe],
|
||||
exports: [TrimPipe, VatPipe],
|
||||
declarations: [TrimPipe, VatPipe],
|
||||
providers: [],
|
||||
})
|
||||
export class PipesModule {}
|
||||
|
||||
18
apps/page/catalog/src/lib/shared/pipes/vat.pipe.ts
Normal file
18
apps/page/catalog/src/lib/shared/pipes/vat.pipe.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import { VATType } from '@swagger/cat';
|
||||
|
||||
@Pipe({
|
||||
name: 'vat',
|
||||
})
|
||||
export class VatPipe implements PipeTransform {
|
||||
transform(vatType: VATType, priceMaintained?: boolean, ...args: any[]): any {
|
||||
const vatString = vatType === 1 ? '0%' : vatType === 2 ? '19%' : vatType === 8 ? '7%' : undefined;
|
||||
if (!vatString) {
|
||||
return;
|
||||
}
|
||||
if (priceMaintained) {
|
||||
return `inkl. ${vatString} MwSt; Preisgebunden`;
|
||||
}
|
||||
return `inkl. ${vatString} MwSt`;
|
||||
}
|
||||
}
|
||||
@@ -76,7 +76,7 @@
|
||||
"
|
||||
/>
|
||||
</ng-container>
|
||||
<ng-container *ngFor="let item of group.items; let lastItem = last; let i = index">
|
||||
<ng-container *ngFor="let item of group.items; let lastItem = last; let i = index; trackBy: trackByItemId">
|
||||
<ng-container
|
||||
*ngIf="group?.orderType !== undefined && (item.features?.orderType === 'Abholung' || item.features?.orderType === 'Rücklage')"
|
||||
>
|
||||
@@ -126,7 +126,8 @@
|
||||
[disabled]="
|
||||
showOrderButtonSpinner ||
|
||||
((primaryCtaLabel$ | async) === 'Bestellen' && !(checkNotificationChannelControl$ | async)) ||
|
||||
notificationsControl?.invalid
|
||||
notificationsControl?.invalid ||
|
||||
((primaryCtaLabel$ | async) === 'Bestellen' && ((checkingOla$ | async) || (checkoutIsInValid$ | async)))
|
||||
"
|
||||
>
|
||||
<ui-spinner [show]="showOrderButtonSpinner">
|
||||
|
||||
@@ -1,4 +1,14 @@
|
||||
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
ViewChildren,
|
||||
QueryList,
|
||||
AfterViewInit,
|
||||
TrackByFunction,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
@@ -6,8 +16,8 @@ import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { AvailabilityDTO, DestinationDTO, 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 { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
|
||||
import { catchError, debounceTime, delay, first, map, shareReplay, switchMap, take, takeUntil, tap } from 'rxjs/operators';
|
||||
import { Subject, NEVER, combineLatest, BehaviorSubject, interval, of, merge, Subscription } from 'rxjs';
|
||||
import { DomainCatalogService } from '@domain/catalog';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { DomainPrinterService } from '@domain/printer';
|
||||
@@ -17,6 +27,8 @@ import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-mod
|
||||
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { CheckoutReviewStore } from './checkout-review.store';
|
||||
import { ToasterService } from '@shared/shell';
|
||||
import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-item.component';
|
||||
|
||||
@Component({
|
||||
selector: 'page-checkout-review',
|
||||
@@ -24,9 +36,13 @@ import { CheckoutReviewStore } from './checkout-review.store';
|
||||
styleUrls: ['checkout-review.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CheckoutReviewComponent implements OnInit, OnDestroy {
|
||||
export class CheckoutReviewComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
checkingOla$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
payer$ = this._store.payer$;
|
||||
|
||||
buyer$ = this._store.buyer$;
|
||||
|
||||
shoppingCart$ = this._store.shoppingCart$;
|
||||
|
||||
fetching$ = this._store.fetching$;
|
||||
@@ -120,12 +136,12 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
|
||||
showQuantityControlSpinnerItemId: number;
|
||||
quantityError$ = new BehaviorSubject<{ [key: string]: string }>({});
|
||||
|
||||
primaryCtaLabel$ = combineLatest([this.payer$, this.shoppingCartItemsWithoutOrderType$]).pipe(
|
||||
map(([payer, shoppingCartItemsWithoutOrderType]) => {
|
||||
primaryCtaLabel$ = combineLatest([this.payer$, this.buyer$, this.shoppingCartItemsWithoutOrderType$]).pipe(
|
||||
map(([payer, buyer, shoppingCartItemsWithoutOrderType]) => {
|
||||
if (shoppingCartItemsWithoutOrderType?.length > 0) {
|
||||
return 'Kaufoptionen';
|
||||
}
|
||||
if (!payer) {
|
||||
if (!(payer || buyer)) {
|
||||
return 'Weiter';
|
||||
}
|
||||
return 'Bestellen';
|
||||
@@ -147,6 +163,11 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
|
||||
loadingOnQuantityChangeById$ = new Subject<number>();
|
||||
showOrderButtonSpinner: boolean;
|
||||
|
||||
checkoutIsInValid$ = this.applicationService.activatedProcessId$.pipe(
|
||||
switchMap((processId) => this.domainCheckoutService.checkoutIsValid({ processId })),
|
||||
map((valid) => !valid)
|
||||
);
|
||||
|
||||
get productSearchBasePath() {
|
||||
return this._productNavigationService.getArticleSearchBasePath(this.applicationService.activatedProcessId).path;
|
||||
}
|
||||
@@ -157,6 +178,13 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
@ViewChildren(ShoppingCartItemComponent)
|
||||
private _shoppingCartItems: QueryList<ShoppingCartItemComponent>;
|
||||
|
||||
olaCheckSubscription: Subscription;
|
||||
|
||||
trackByItemId: TrackByFunction<ShoppingCartItemDTO> = (_, item) => item?.id;
|
||||
|
||||
constructor(
|
||||
private domainCheckoutService: DomainCheckoutService,
|
||||
public applicationService: ApplicationService,
|
||||
@@ -171,7 +199,8 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
|
||||
private _productNavigationService: ProductCatalogNavigationService,
|
||||
private _navigationService: CheckoutNavigationService,
|
||||
private _environmentService: EnvironmentService,
|
||||
private _store: CheckoutReviewStore
|
||||
private _store: CheckoutReviewStore,
|
||||
private _toaster: ToasterService
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -181,6 +210,14 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
|
||||
|
||||
await this.removeBreadcrumbs();
|
||||
await this.updateBreadcrumb();
|
||||
|
||||
window['Checkout'] = {
|
||||
refreshAvailabilities: this.refreshAvailabilities.bind(this),
|
||||
};
|
||||
}
|
||||
|
||||
ngAfterViewInit() {
|
||||
this.registerOlaCechk();
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -189,6 +226,35 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
registerOlaCechk() {
|
||||
this.olaCheckSubscription?.unsubscribe();
|
||||
this.olaCheckSubscription = this.applicationService.activatedProcessId$
|
||||
.pipe(
|
||||
takeUntil(this._onDestroy$),
|
||||
delay(250),
|
||||
switchMap((processId) =>
|
||||
this.domainCheckoutService.validateOlaStatus({
|
||||
processId,
|
||||
})
|
||||
)
|
||||
)
|
||||
.subscribe((result) => {
|
||||
if (!result) {
|
||||
this.refreshAvailabilities();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async refreshAvailabilities() {
|
||||
this.checkingOla$.next(true);
|
||||
|
||||
for (let itemComp of this._shoppingCartItems.toArray()) {
|
||||
await itemComp.refreshAvailability();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
this.checkingOla$.next(false);
|
||||
}
|
||||
|
||||
async updateBreadcrumb() {
|
||||
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
|
||||
key: this.applicationService.activatedProcessId,
|
||||
@@ -474,6 +540,8 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
|
||||
title: 'Hinweis',
|
||||
data: { message: message.trim() },
|
||||
});
|
||||
} else if (error) {
|
||||
this.uiModal.error('Fehler beim abschließen der Bestellung', error);
|
||||
}
|
||||
|
||||
if (error.status === 409) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import { CheckoutReviewDetailsComponent } from './details/checkout-review-detail
|
||||
import { CheckoutReviewStore } from './checkout-review.store';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { LoaderComponent } from '@shared/components/loader';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -39,6 +40,7 @@ import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
UiCheckboxModule,
|
||||
SharedNotificationChannelControlModule,
|
||||
TextFieldModule,
|
||||
LoaderComponent,
|
||||
],
|
||||
exports: [CheckoutReviewComponent, CheckoutReviewDetailsComponent],
|
||||
declarations: [CheckoutReviewComponent, SpecialCommentComponent, ShoppingCartItemComponent, CheckoutReviewDetailsComponent],
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
<page-special-comment
|
||||
class="mb-6 mt-4"
|
||||
[hasPayer]="!!(payer$ | async)"
|
||||
[hasBuyer]="!!(buyer$ | async)"
|
||||
[ngModel]="specialComment$ | async"
|
||||
(ngModelChange)="setAgentComment($event)"
|
||||
>
|
||||
|
||||
@@ -40,16 +40,26 @@
|
||||
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
|
||||
{{ item?.product?.publicationDate | date }}
|
||||
</div>
|
||||
|
||||
<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 *ngIf="notAvailable$ | async">
|
||||
<span class="text-brand item-date">Nicht verfügbar</span>
|
||||
</div>
|
||||
<ui-spinner class="ava-loader" [show]="refreshingAvailabilit$ | async">
|
||||
<div class="item-date" [class.ssc-changed]="sscChanged$ | async" *ngIf="orderType === 'Abholung'">
|
||||
Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
|
||||
</div>
|
||||
<div
|
||||
class="item-date"
|
||||
[class.ssc-changed]="sscChanged$ | async"
|
||||
*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>
|
||||
</ui-spinner>
|
||||
|
||||
<div class="item-availability-message" *ngIf="olaError$ | async">
|
||||
Artikel nicht verfügbar
|
||||
|
||||
@@ -101,8 +101,16 @@ button {
|
||||
}
|
||||
}
|
||||
|
||||
.ssc-changed {
|
||||
@apply text-dark-goldenrod;
|
||||
}
|
||||
|
||||
::ng-deep page-shopping-cart-item ui-quantity-dropdown {
|
||||
.current-quantity {
|
||||
font-weight: normal !important;
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep page-shopping-cart-item .ava-loader ui-icon {
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, NgZone, OnInit, Output, inject } from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
@@ -6,6 +6,7 @@ import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { ProductCatalogNavigationService } from '@shared/services';
|
||||
import { ItemType, ShoppingCartItemDTO } from '@swagger/checkout';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
@@ -14,6 +15,9 @@ export interface ShoppingCartItemComponentState {
|
||||
orderType: string;
|
||||
loadingOnItemChangeById?: number;
|
||||
loadingOnQuantityChangeById?: number;
|
||||
refreshingAvailability: boolean;
|
||||
sscChanged: boolean;
|
||||
sscTextChanged: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -23,6 +27,8 @@ export interface ShoppingCartItemComponentState {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemComponentState> implements OnInit {
|
||||
private _zone = inject(NgZone);
|
||||
|
||||
@Output() changeItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
|
||||
@Output() changeDummyItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
|
||||
@Output() changeQuantity = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO; quantity: number }>();
|
||||
@@ -127,14 +133,35 @@ export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemCo
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
refreshingAvailabilit$ = this.select((s) => s.refreshingAvailability);
|
||||
|
||||
sscChanged$ = this.select((s) => s.sscChanged || s.sscTextChanged);
|
||||
|
||||
notAvailable$ = this.item$.pipe(
|
||||
map((item) => {
|
||||
const availability = item?.availability;
|
||||
|
||||
if (availability.availabilityType === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availability.inStock && item.quantity > availability.inStock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.availabilityService.isAvailable({ availability });
|
||||
})
|
||||
);
|
||||
|
||||
constructor(
|
||||
private availabilityService: DomainAvailabilityService,
|
||||
private checkoutService: DomainCheckoutService,
|
||||
public application: ApplicationService,
|
||||
private _productNavigationService: ProductCatalogNavigationService,
|
||||
private _environment: EnvironmentService
|
||||
private _environment: EnvironmentService,
|
||||
private _cdr: ChangeDetectorRef
|
||||
) {
|
||||
super({ item: undefined, orderType: '' });
|
||||
super({ item: undefined, orderType: '', refreshingAvailability: false, sscChanged: false, sscTextChanged: false });
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
@@ -147,4 +174,48 @@ export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemCo
|
||||
onChangeQuantity(quantity: number) {
|
||||
this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
|
||||
}
|
||||
|
||||
async refreshAvailability() {
|
||||
const currentAvailability = cloneDeep(this.item.availability);
|
||||
|
||||
try {
|
||||
this.patchRefreshingAvailability(true);
|
||||
this._cdr.markForCheck();
|
||||
const availability = await this.checkoutService.refreshAvailability({
|
||||
processId: this.application.activatedProcessId,
|
||||
shoppingCartItemId: this.item.id,
|
||||
});
|
||||
|
||||
if (currentAvailability.ssc !== availability.ssc) {
|
||||
this.sscChanged();
|
||||
}
|
||||
if (currentAvailability.sscText !== availability.sscText) {
|
||||
this.ssctextChanged();
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
this.patchRefreshingAvailability(false);
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
patchRefreshingAvailability(value: boolean) {
|
||||
this._zone.run(() => {
|
||||
this.patchState({ refreshingAvailability: value });
|
||||
this._cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ssctextChanged() {
|
||||
this._zone.run(() => {
|
||||
this.patchState({ sscTextChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
sscChanged() {
|
||||
this._zone.run(() => {
|
||||
this.patchState({ sscChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!hasPayer" class="text-p3">Zur Info: Sie haben dem Warenkorb noch keinen Kunden hinzugefügt.</div>
|
||||
<div *ngIf="!(hasPayer || hasBuyer)" class="text-p3">Zur Info: Sie haben dem Warenkorb noch keinen Kunden hinzugefügt.</div>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,9 @@ export class SpecialCommentComponent implements ControlValueAccessor {
|
||||
@Input()
|
||||
hasPayer: boolean;
|
||||
|
||||
@Input()
|
||||
hasBuyer: boolean;
|
||||
|
||||
@Output()
|
||||
isDirtyChange = new EventEmitter<boolean>();
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BreadcrumbService } from '@core/breadcrumb';
|
||||
import { debounce, isEqual } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { debounceTime, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { isEqual } from 'lodash';
|
||||
import { combineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { debounceTime, filter, first, map, switchMap, take } from 'rxjs/operators';
|
||||
import { CustomerOrderSearchStore } from '../customer-order-search.store';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { Filter, FilterInputGroupMainComponent } from '@shared/components/filter';
|
||||
@@ -17,7 +17,10 @@ import { ApplicationService } from '@core/application';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CustomerOrderSearchMainComponent implements OnInit, OnDestroy {
|
||||
filter$ = this._customerOrderSearchStore.filter$;
|
||||
filter$ = this._customerOrderSearchStore.filter$.pipe(
|
||||
filter((f) => !!f),
|
||||
take(1)
|
||||
);
|
||||
|
||||
loading$ = this._customerOrderSearchStore.fetching$;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
QueryList,
|
||||
AfterViewInit,
|
||||
} from '@angular/core';
|
||||
import { debounceTime, first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import { debounceTime, filter, first, map, shareReplay, switchMap, take, takeUntil, withLatestFrom } from 'rxjs/operators';
|
||||
import { KeyValueDTOOfStringAndString, OrderItemListItemDTO } from '@swagger/oms';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
import { CustomerOrderSearchStore } from '../customer-order-search.store';
|
||||
@@ -99,7 +99,10 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
|
||||
|
||||
private _searchResultSubscription = new Subscription();
|
||||
|
||||
filter$ = this._customerOrderSearchStore.filter$;
|
||||
filter$ = this._customerOrderSearchStore.filter$.pipe(
|
||||
filter((f) => !!f),
|
||||
take(1)
|
||||
);
|
||||
|
||||
hasFilter$ = combineLatest([this.filter$, this._customerOrderSearchStore.defaultSettings$]).pipe(
|
||||
map(([filter, defaultFilter]) => !isEqual(filter?.getQueryParams(), Filter.create(defaultFilter).getQueryParams()))
|
||||
@@ -206,7 +209,7 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
|
||||
this._customerOrderSearchStore.processId,
|
||||
this._customerOrderSearchStore.filter?.getQueryParams()
|
||||
);
|
||||
} else {
|
||||
} else if (this._customerOrderSearchStore?.results?.length === 0) {
|
||||
this._customerOrderSearchStore.search({ siletReload: true });
|
||||
}
|
||||
} else if (branchChanged) {
|
||||
@@ -247,7 +250,8 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
|
||||
if (result.results.hits === 1) {
|
||||
await this.navigateToDetails(
|
||||
processId,
|
||||
result?.results?.result?.find((_) => true)
|
||||
result?.results?.result?.find((_) => true),
|
||||
queryParams
|
||||
);
|
||||
} else if (!!result?.clear || this.isTablet || this._activatedRoute.outlet === 'primary') {
|
||||
await this._navigationService.navigateToResults({
|
||||
@@ -401,8 +405,7 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
|
||||
this._customerOrderSearchStore.search({ clear });
|
||||
}
|
||||
|
||||
async navigateToDetails(processId: number, orderItem: OrderItemListItemDTO) {
|
||||
const archive = !!this._customerOrderSearchStore.filter?.getQueryParams()?.main_archive || false;
|
||||
async navigateToDetails(processId: number, orderItem: OrderItemListItemDTO, queryParams: Record<string, string>) {
|
||||
await this._navigationService.navigateToDetails({
|
||||
processId,
|
||||
processingStatus: orderItem?.processingStatus,
|
||||
@@ -411,9 +414,9 @@ export class CustomerOrderSearchResultsComponent extends ComponentStore<Customer
|
||||
queryParams: orderItem?.compartmentCode
|
||||
? {
|
||||
buyerNumber: orderItem.buyerNumber,
|
||||
archive: String(archive),
|
||||
...queryParams,
|
||||
}
|
||||
: { archive: String(archive) },
|
||||
: { ...queryParams },
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
:host {
|
||||
@apply inline-block;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<button
|
||||
type="button"
|
||||
class="px-2 py-3 bg-[#C6CBD0] rounded flex flex-row items-center open:bg-[#596470] open:text-white z-dropdown"
|
||||
[cdkMenuTriggerFor]="navMenu"
|
||||
#menuTrigger="cdkMenuTriggerFor"
|
||||
[class.open]="menuTrigger.isOpen()"
|
||||
>
|
||||
<shared-icon icon="apps" [size]="24"></shared-icon>
|
||||
<shared-icon [icon]="menuTrigger.isOpen() ? 'arrow-drop-up' : 'arrow-drop-down'" [size]="24"></shared-icon>
|
||||
</button>
|
||||
|
||||
<ng-template #navMenu>
|
||||
<div class="pt-1">
|
||||
<shared-menu>
|
||||
<a
|
||||
sharedMenuItem
|
||||
*ngIf="customerDetailsRoute$ | async; let customerDetailsRoute"
|
||||
[routerLink]="customerDetailsRoute.path"
|
||||
[queryParams]="customerDetailsRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>Kundendetails</a
|
||||
>
|
||||
<a
|
||||
sharedMenuItem
|
||||
*ngIf="ordersRoute$ | async; let ordersRoute"
|
||||
[routerLink]="ordersRoute.path"
|
||||
[queryParams]="ordersRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>Bestellungen</a
|
||||
>
|
||||
<a
|
||||
sharedMenuItem
|
||||
*ngIf="kundenkarteRoute$ | async; let kundenkarteRoute"
|
||||
[routerLink]="kundenkarteRoute.path"
|
||||
[queryParams]="kundenkarteRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>Kundenkarte</a
|
||||
>
|
||||
<a
|
||||
sharedMenuItem
|
||||
*ngIf="historyRoute$ | async; let historyRoute"
|
||||
[routerLink]="historyRoute.path"
|
||||
[queryParams]="historyRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>Historie</a
|
||||
>
|
||||
</shared-menu>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,111 @@
|
||||
import { BooleanInput, NumberInput, coerceBooleanProperty, coerceNumberProperty } from '@angular/cdk/coercion';
|
||||
import { CdkMenuModule } from '@angular/cdk/menu';
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { SharedMenuModule } from '@shared/components/menu';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { CustomerSearchNavigation } from '../../navigations';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { AsyncPipe, NgIf } from '@angular/common';
|
||||
|
||||
export interface CustomerMenuComponentState {
|
||||
customerId?: number;
|
||||
processId?: number;
|
||||
hasCustomerCard?: boolean;
|
||||
showCustomerDetails: boolean;
|
||||
showCustomerOrders: boolean;
|
||||
showCustomerHistory: boolean;
|
||||
showCustomerCard: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-menu',
|
||||
templateUrl: 'customer-menu.component.html',
|
||||
styleUrls: ['customer-menu.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-customer-menu' },
|
||||
standalone: true,
|
||||
imports: [CdkMenuModule, SharedMenuModule, IconComponent, RouterLink, NgIf, AsyncPipe],
|
||||
})
|
||||
export class CustomerMenuComponent extends ComponentStore<CustomerMenuComponentState> {
|
||||
@Input() set customerId(value: NumberInput) {
|
||||
this.patchState({ customerId: coerceNumberProperty(value) });
|
||||
}
|
||||
|
||||
@Input() set hasCustomerCard(value: BooleanInput) {
|
||||
this.patchState({ hasCustomerCard: coerceBooleanProperty(value) });
|
||||
}
|
||||
|
||||
@Input() set processId(value: NumberInput) {
|
||||
this.patchState({ processId: coerceNumberProperty(value) });
|
||||
}
|
||||
|
||||
@Input() set showCustomerDetails(value: BooleanInput) {
|
||||
this.patchState({ showCustomerDetails: coerceBooleanProperty(value) });
|
||||
}
|
||||
|
||||
@Input() set showCustomerOrders(value: BooleanInput) {
|
||||
this.patchState({ showCustomerOrders: coerceBooleanProperty(value) });
|
||||
}
|
||||
|
||||
@Input() set showCustomerHistory(value: BooleanInput) {
|
||||
this.patchState({ showCustomerHistory: coerceBooleanProperty(value) });
|
||||
}
|
||||
|
||||
@Input() set showCustomerCard(value: BooleanInput) {
|
||||
this.patchState({ showCustomerCard: coerceBooleanProperty(value) });
|
||||
}
|
||||
|
||||
readonly customerId$ = this.select((state) => state.customerId);
|
||||
|
||||
readonly processId$ = this.select((state) => state.processId);
|
||||
|
||||
readonly hasCustomerCard$ = this.select((state) => state.hasCustomerCard);
|
||||
|
||||
readonly showCustomerDetails$ = this.select((state) => state.showCustomerDetails);
|
||||
|
||||
readonly showCustomerOrders$ = this.select((state) => state.showCustomerOrders);
|
||||
|
||||
readonly showCustomerHistory$ = this.select((state) => state.showCustomerHistory);
|
||||
|
||||
readonly showCustomerCard$ = this.select((state) => state.showCustomerCard);
|
||||
|
||||
historyRoute$ = combineLatest([this.showCustomerHistory$, this.processId$, this.customerId$]).pipe(
|
||||
map(
|
||||
([showCustomerHistory, processId, customerId]) =>
|
||||
showCustomerHistory && processId && customerId && this._navigation.historyRoute({ processId, customerId })
|
||||
)
|
||||
);
|
||||
|
||||
ordersRoute$ = combineLatest([this.showCustomerOrders$, this.processId$, this.customerId$]).pipe(
|
||||
map(
|
||||
([showCustomerOrders, processId, customerId]) =>
|
||||
showCustomerOrders && processId && customerId && this._navigation.ordersRoute({ processId, customerId })
|
||||
)
|
||||
);
|
||||
|
||||
kundenkarteRoute$ = combineLatest([this.showCustomerCard$, this.hasCustomerCard$, this.processId$, this.customerId$]).pipe(
|
||||
map(
|
||||
([showCustomerCard, hasCustomerCard, processId, customerId]) =>
|
||||
showCustomerCard && hasCustomerCard && processId && customerId && this._navigation.kundenkarteRoute({ processId, customerId })
|
||||
)
|
||||
);
|
||||
|
||||
customerDetailsRoute$ = combineLatest([this.showCustomerDetails$, this.processId$, this.customerId$]).pipe(
|
||||
map(
|
||||
([showCustomerDetails, processId, customerId]) =>
|
||||
showCustomerDetails && processId && customerId && this._navigation.detailsRoute({ processId, customerId })
|
||||
)
|
||||
);
|
||||
|
||||
constructor(private _navigation: CustomerSearchNavigation) {
|
||||
super({
|
||||
showCustomerCard: true,
|
||||
showCustomerDetails: true,
|
||||
showCustomerHistory: true,
|
||||
showCustomerOrders: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './customer-menu.component';
|
||||
@@ -1,31 +1,13 @@
|
||||
<shared-loader [loading]="fetching$ | async" background="true" spinnerSize="32">
|
||||
<div class="overflow-scroll max-h-[calc(100vh-15rem)]">
|
||||
<div class="customer-details-header grid grid-flow-row pt-1 px-1 pb-6">
|
||||
<div class="customer-details-header-actions flex flex-row justify-end pt-1 px-1">
|
||||
<a
|
||||
*ngIf="ordersRoute$ | async; let ordersRoute"
|
||||
class="btn btn-label font-bold text-brand"
|
||||
[routerLink]="ordersRoute.path"
|
||||
[queryParams]="ordersRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>Bestellungen</a
|
||||
>
|
||||
<a
|
||||
*ngIf="kundenkarteRoute$ | async; let kundenkarteRoute"
|
||||
class="btn btn-label font-bold text-brand"
|
||||
[routerLink]="kundenkarteRoute.path"
|
||||
[queryParams]="kundenkarteRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>Kundenkarte</a
|
||||
>
|
||||
<a
|
||||
*ngIf="historyRoute$ | async; let historyRoute"
|
||||
class="btn btn-label font-bold text-brand"
|
||||
[routerLink]="historyRoute.path"
|
||||
[queryParams]="historyRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>Historie</a
|
||||
>
|
||||
<div class="customer-details-header grid grid-flow-row pb-6">
|
||||
<div class="customer-details-header-actions flex flex-row justify-end pt-4 px-4">
|
||||
<page-customer-menu
|
||||
[customerId]="customerId$ | async"
|
||||
[processId]="processId$ | async"
|
||||
[hasCustomerCard]="hasKundenkarte$ | async"
|
||||
[showCustomerDetails]="false"
|
||||
></page-customer-menu>
|
||||
</div>
|
||||
<div class="customer-details-header-body text-center -mt-3">
|
||||
<h1 class="text-[1.625rem] font-bold">
|
||||
@@ -165,7 +147,7 @@
|
||||
[disabled]="showLoader$ | async"
|
||||
>
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">
|
||||
Weiter zur Artielsuche
|
||||
Weiter zur Artikelsuche
|
||||
</shared-loader>
|
||||
</button>
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Subject, combineLatest } from 'rxjs';
|
||||
import { debounceTime, filter, first, map, share, switchMap, takeUntil, tap, throttleTime } from 'rxjs/operators';
|
||||
import { first, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { CustomerSearchNavigation } from '../../navigations';
|
||||
import { CustomerSearchStore } from '../store';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { ShippingAddressDTO, NotificationChannel, ShoppingCartDTO, PayerDTO, BuyerDTO } from '@swagger/checkout';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { CantAddCustomerToCartData, CantAddCustomerToCartModalComponent, CantSelectGuestModalComponent } from '../../modals';
|
||||
@@ -13,7 +12,6 @@ import { ApplicationService } from '@core/application';
|
||||
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
|
||||
import { Router } from '@angular/router';
|
||||
import { log, logAsync } from '@utils/common';
|
||||
import { isBoolean } from 'lodash';
|
||||
|
||||
const GENDER_MAP = {
|
||||
2: 'Herr',
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DetailsMainViewBillingAddressesComponent } from './details-main-view-bi
|
||||
import { DetailsMainViewDeliveryAddressesComponent } from './details-main-view-delivery-addresses/details-main-view-delivery-addresses.component';
|
||||
import { LoaderComponent } from '@shared/components/loader';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { CustomerMenuComponent } from '../../components/customer-menu';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -19,6 +20,7 @@ import { IconComponent } from '@shared/components/icon';
|
||||
DetailsMainViewDeliveryAddressesComponent,
|
||||
LoaderComponent,
|
||||
IconComponent,
|
||||
CustomerMenuComponent,
|
||||
],
|
||||
exports: [CustomerDetailsViewMainComponent],
|
||||
declarations: [CustomerDetailsViewMainComponent],
|
||||
|
||||
@@ -2,16 +2,13 @@
|
||||
<shared-loader [loading]="fetching$ | async" [background]="true" [spinnerSize]="48">
|
||||
<div>
|
||||
<div class="customer-history-header">
|
||||
<div class="customer-history-header-actions flex flex-row justify-end pt-1 px-1">
|
||||
<a
|
||||
*ngIf="detailsRoute$ | async; let route"
|
||||
[routerLink]="route.path"
|
||||
[queryParams]="route.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
class="btn btn-label"
|
||||
>
|
||||
<ui-icon [icon]="'close'"></ui-icon>
|
||||
</a>
|
||||
<div class="customer-history-header-actions flex flex-row justify-end pt-4 px-4">
|
||||
<page-customer-menu
|
||||
[customerId]="customerId$ | async"
|
||||
[processId]="processId$ | async"
|
||||
[hasCustomerCard]="hasKundenkarte$ | async"
|
||||
[showCustomerHistory]="false"
|
||||
></page-customer-menu>
|
||||
</div>
|
||||
<div class="customer-history-header-body text-center -mt-3">
|
||||
<h1 class="text-[1.625rem] font-bold">Historie</h1>
|
||||
|
||||
@@ -27,6 +27,10 @@ export class CustomerHistoryMainViewComponent extends ComponentStore<CustomerHis
|
||||
|
||||
customerId$ = this._store.customerId$;
|
||||
|
||||
hasKundenkarte$ = combineLatest([this._store.isKundenkarte$, this._store.isOnlineKontoMitKundenkarte$]).pipe(
|
||||
map(([isKundenkarte, isOnlineKontoMitKundenkarte]) => isKundenkarte || isOnlineKontoMitKundenkarte)
|
||||
);
|
||||
|
||||
customer$ = this._store.customer$;
|
||||
|
||||
detailsRoute$ = combineLatest([this.processId$, this.customerId$]).pipe(
|
||||
@@ -65,6 +69,7 @@ export class CustomerHistoryMainViewComponent extends ComponentStore<CustomerHis
|
||||
};
|
||||
|
||||
handleFetchHistoryError = (err: any) => {
|
||||
this.patchState({ fetching: false });
|
||||
console.error(err);
|
||||
};
|
||||
|
||||
|
||||
@@ -6,9 +6,10 @@ import { CustomerHistoryMainViewComponent } from './history-main-view.component'
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { LoaderModule } from '@shared/components/loader';
|
||||
import { CustomerMenuComponent } from '../../components/customer-menu';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, RouterModule, SharedHistoryListModule, UiIconModule, LoaderModule],
|
||||
imports: [CommonModule, RouterModule, SharedHistoryListModule, UiIconModule, LoaderModule, CustomerMenuComponent],
|
||||
exports: [CustomerHistoryMainViewComponent],
|
||||
declarations: [CustomerHistoryMainViewComponent],
|
||||
})
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
<div class="flex flex-row justify-end -mt-4 -mr-2">
|
||||
<a
|
||||
*ngIf="detailsRoute$ | async; let detailsRoute"
|
||||
[routerLink]="detailsRoute.path"
|
||||
[queryParams]="detailsRoute.urlTree.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>
|
||||
<shared-icon icon="close" [size]="32"></shared-icon>
|
||||
</a>
|
||||
<div class="flex flex-row justify-end -mt-2">
|
||||
<page-customer-menu [customerId]="customerId$ | async" [processId]="processId$ | async" [showCustomerCard]="false"></page-customer-menu>
|
||||
</div>
|
||||
<h1 class="text-center text-2xl font-bold">Kundenkarte</h1>
|
||||
<p class="text-center text-xl" *ngIf="!(noDataFound$ | async)">
|
||||
|
||||
@@ -9,6 +9,7 @@ import { AsyncPipe, NgFor, NgIf } from '@angular/common';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { CustomerSearchNavigation } from '../../navigations';
|
||||
import { BonusCardInfoDTO } from '@swagger/crm';
|
||||
import { CustomerMenuComponent } from '../../components/customer-menu';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-kundenkarte-main-view',
|
||||
@@ -17,13 +18,15 @@ import { BonusCardInfoDTO } from '@swagger/crm';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-customer-kundenkarte-main-view' },
|
||||
standalone: true,
|
||||
imports: [KundenkarteComponent, NgFor, AsyncPipe, NgIf, IconComponent, RouterLink],
|
||||
imports: [CustomerMenuComponent, KundenkarteComponent, NgFor, AsyncPipe, NgIf, IconComponent, RouterLink],
|
||||
})
|
||||
export class KundenkarteMainViewComponent implements OnInit, OnDestroy {
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
customerId$ = this._activatedRoute.params.pipe(map((params) => params.customerId));
|
||||
|
||||
processId$ = this._store.processId$;
|
||||
|
||||
kundenkarte$ = this.customerId$.pipe(
|
||||
switchMap((customerId) =>
|
||||
this._customerService.getCustomerCard(customerId).pipe(
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<div class="wrapper">
|
||||
<div class="flex flex-row justify-end -mt-4 -mr-2">
|
||||
<a
|
||||
*ngIf="detailsRoute$ | async; let detailsRoute"
|
||||
[routerLink]="detailsRoute.path"
|
||||
[queryParams]="detailsRoute.urlTree.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
>
|
||||
<shared-icon icon="close" [size]="32"></shared-icon>
|
||||
</a>
|
||||
<page-customer-menu
|
||||
[customerId]="customerId$ | async"
|
||||
[processId]="processId$ | async"
|
||||
[hasCustomerCard]="hasKundenkarte$ | async"
|
||||
[showCustomerOrders]="false"
|
||||
></page-customer-menu>
|
||||
</div>
|
||||
<h1 class="text-2xl text-center font-bold mb-4">Bestellungen</h1>
|
||||
<p class="text-xl text-center">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { RouterLink } from '@angular/router';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { LoaderComponent } from '@shared/components/loader';
|
||||
import { CustomerOrderListItemComponent } from './order-list-item/order-list-item.component';
|
||||
import { CustomerMenuComponent } from '../../components/customer-menu';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-orders-main-view',
|
||||
@@ -16,11 +17,19 @@ import { CustomerOrderListItemComponent } from './order-list-item/order-list-ite
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-customer-orders-main-view' },
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, NgFor, NgIf, RouterLink, IconComponent, LoaderComponent, CustomerOrderListItemComponent],
|
||||
imports: [CustomerMenuComponent, AsyncPipe, NgFor, NgIf, RouterLink, IconComponent, LoaderComponent, CustomerOrderListItemComponent],
|
||||
})
|
||||
export class CustomerOrdersMainViewComponent implements OnInit, OnDestroy {
|
||||
private _onDestroy = new Subject<void>();
|
||||
|
||||
processId$ = this._store.processId$;
|
||||
|
||||
customerId$ = this._store.customerId$;
|
||||
|
||||
hasKundenkarte$ = combineLatest([this._store.isKundenkarte$, this._store.isOnlineKontoMitKundenkarte$]).pipe(
|
||||
map(([isKundenkarte, isOnlineKontoMitKundenkarte]) => isKundenkarte || isOnlineKontoMitKundenkarte)
|
||||
);
|
||||
|
||||
orders$ = this._store.customerOrders$;
|
||||
|
||||
fetching$ = this._store.fetchingCustomerOrders$;
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
[hint]="hint$ | async"
|
||||
(scan)="search($event)"
|
||||
[scanner]="true"
|
||||
(hintCleared)="clearHint()"
|
||||
></ui-searchbox>
|
||||
</div>
|
||||
|
||||
@@ -59,8 +59,12 @@ export class FinishShippingDocumentComponent implements OnInit, OnDestroy {
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
clearHint() {
|
||||
this.hint$.next('');
|
||||
}
|
||||
|
||||
search(query: string) {
|
||||
query = query?.trim();
|
||||
if (!query) {
|
||||
this.hint$.next('Ungültige Eingabe');
|
||||
return;
|
||||
|
||||
@@ -1,70 +1,80 @@
|
||||
<div class="options-wrapper">
|
||||
<div
|
||||
*ngIf="uiStartOption"
|
||||
class="option"
|
||||
<div class="grid grid-flow-col items-center justify-start gap-4" [formGroup]="formGroup">
|
||||
<shared-form-control
|
||||
[attr.data-label]="uiStartOption?.label"
|
||||
[attr.data-value]="uiStartOption?.value"
|
||||
[attr.data-key]="uiStartOption?.key"
|
||||
[attr.data-selected]="uiStartOption?.selected"
|
||||
>
|
||||
<div class="option-wrapper">
|
||||
<span> {{ uiStartOption?.label }}: </span>
|
||||
<button
|
||||
class="cta-picker"
|
||||
[class.open]="dpStartTrigger?.opened"
|
||||
[uiOverlayTrigger]="dpStart"
|
||||
#dpStartTrigger="uiOverlayTrigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
{{ uiStartOption?.value | date: 'dd.MM.yy' }}
|
||||
</span>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ui-datepicker
|
||||
class="dp-left"
|
||||
#dpStart
|
||||
yPosition="below"
|
||||
xPosition="after"
|
||||
[ngModel]="uiStartOption?.value"
|
||||
saveLabel="Übernehmen"
|
||||
(save)="uiStartOption?.setValue($event)"
|
||||
>
|
||||
</ui-datepicker>
|
||||
</div>
|
||||
<div
|
||||
*ngIf="uiStopOption"
|
||||
class="option"
|
||||
<shared-input-control>
|
||||
<input
|
||||
placeholder="TT.MM.JJJJ"
|
||||
sharedInputControlInput
|
||||
sharedDateInput
|
||||
type="text"
|
||||
formControlName="start"
|
||||
(blur)="setStratValue($event.target['value'])"
|
||||
/>
|
||||
<shared-input-control-suffix>
|
||||
<button
|
||||
type="button"
|
||||
class="grid items-center justify-center h-10 w-14 my-2 border-l solid border-[#AEB7C1] text-[#596470]"
|
||||
[uiOverlayTrigger]="dpStart"
|
||||
#dpStartTrigger="uiOverlayTrigger"
|
||||
>
|
||||
<shared-icon icon="calendar-today"></shared-icon>
|
||||
</button>
|
||||
</shared-input-control-suffix>
|
||||
</shared-input-control>
|
||||
</shared-form-control>
|
||||
<div class="font-bold -mt-4">bis</div>
|
||||
<shared-form-control
|
||||
[attr.data-label]="uiStopOption?.label"
|
||||
[attr.data-value]="uiStopOption?.value"
|
||||
[attr.data-key]="uiStopOption?.key"
|
||||
[attr.data-selected]="uiStopOption?.selected"
|
||||
>
|
||||
<div class="option-wrapper">
|
||||
<span> {{ uiStopOption?.label }}: </span>
|
||||
<button
|
||||
class="cta-picker"
|
||||
[class.open]="dpStopTrigger?.opened"
|
||||
[uiOverlayTrigger]="dpStop"
|
||||
#dpStopTrigger="uiOverlayTrigger"
|
||||
type="button"
|
||||
>
|
||||
<span>
|
||||
{{ uiStopOptionValue | date: 'dd.MM.yy' }}
|
||||
</span>
|
||||
<ui-icon icon="arrow_head" size="1em"></ui-icon>
|
||||
</button>
|
||||
</div>
|
||||
<ui-datepicker
|
||||
class="dp-right"
|
||||
yPosition="below"
|
||||
xPosition="after"
|
||||
#dpStop
|
||||
[ngModel]="uiStopOptionValue"
|
||||
(save)="setStopValue($event)"
|
||||
saveLabel="Übernehmen"
|
||||
>
|
||||
</ui-datepicker>
|
||||
</div>
|
||||
<shared-input-control>
|
||||
<input
|
||||
placeholder="TT.MM.JJJJ"
|
||||
sharedInputControlInput
|
||||
sharedDateInput
|
||||
type="text"
|
||||
formControlName="stop"
|
||||
(blur)="setStopValue($event.target['value'])"
|
||||
/>
|
||||
<shared-input-control-suffix>
|
||||
<button
|
||||
type="button"
|
||||
class="grid items-center justify-center h-10 w-14 my-2 border-l solid border-[#AEB7C1] text-[#596470]"
|
||||
[uiOverlayTrigger]="dpStop"
|
||||
#dpStartTrigger="uiOverlayTrigger"
|
||||
>
|
||||
<shared-icon icon="calendar-today"></shared-icon>
|
||||
</button>
|
||||
</shared-input-control-suffix>
|
||||
</shared-input-control>
|
||||
</shared-form-control>
|
||||
|
||||
<ui-datepicker
|
||||
class="dp-left"
|
||||
#dpStart
|
||||
yPosition="below"
|
||||
xPosition="after"
|
||||
formControlName="start"
|
||||
[max]="maxDate"
|
||||
saveLabel="Übernehmen"
|
||||
(save)="setStratValue($event)"
|
||||
>
|
||||
</ui-datepicker>
|
||||
<ui-datepicker
|
||||
class="dp-right"
|
||||
yPosition="below"
|
||||
xPosition="after"
|
||||
#dpStop
|
||||
[min]="minDate"
|
||||
formControlName="stop"
|
||||
(save)="setStopValue($event)"
|
||||
saveLabel="Übernehmen"
|
||||
>
|
||||
</ui-datepicker>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, Input, ChangeDetectorRef, ViewChild } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { IOption, Option } from '../../../tree';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { DateValidator } from '@shared/forms';
|
||||
import { UiDatepickerComponent } from '@ui/datepicker';
|
||||
import moment from 'moment';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-input-option-date-range',
|
||||
@@ -9,11 +13,29 @@ import { IOption, Option } from '../../../tree';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FilterInputOptionDateRangeComponent {
|
||||
@ViewChild('dpStart', { static: true }) startDatepicker: UiDatepickerComponent;
|
||||
|
||||
@ViewChild('dpStop', { static: true }) stopDatepicker: UiDatepickerComponent;
|
||||
|
||||
private _options: Option[];
|
||||
|
||||
formGroup = new FormGroup({
|
||||
start: new FormControl<Date>(undefined, DateValidator),
|
||||
stop: new FormControl<Date>(undefined, DateValidator),
|
||||
});
|
||||
|
||||
get startControl(): FormControl<Date> {
|
||||
return this.formGroup.get('start') as FormControl<Date>;
|
||||
}
|
||||
|
||||
get stopControl(): FormControl<Date> {
|
||||
return this.formGroup.get('stop') as FormControl<Date>;
|
||||
}
|
||||
|
||||
@Input()
|
||||
set options(value: IOption[]) {
|
||||
this._options = value?.map((option) => (option instanceof Option ? option : Option.create(option)));
|
||||
|
||||
this.subscribeChanges();
|
||||
}
|
||||
|
||||
@@ -29,28 +51,56 @@ export class FilterInputOptionDateRangeComponent {
|
||||
return this.uiOptions?.find((o) => o.key === 'stop');
|
||||
}
|
||||
|
||||
get uiStopOptionValue() {
|
||||
const stopDate = new Date(this.uiStopOption?.value);
|
||||
stopDate?.setDate(stopDate?.getDate() - 1); // to update the view correctly after setStopValue() gets called !
|
||||
return stopDate?.toJSON();
|
||||
optionChangeSubscription: Subscription;
|
||||
|
||||
get startDate() {
|
||||
return this.uiStartOption?.value ? new Date(this.uiStartOption?.value) : undefined;
|
||||
}
|
||||
|
||||
optionChangeSubscription: Subscription;
|
||||
get stopDate() {
|
||||
return this.uiStopOption?.value ? new Date(this.uiStopOption?.value) : undefined;
|
||||
}
|
||||
|
||||
get minDate() {
|
||||
return this.startDate ?? new Date(0);
|
||||
}
|
||||
|
||||
get maxDate() {
|
||||
return this.stopDate ?? new Date('9999-12-31');
|
||||
}
|
||||
|
||||
constructor(private cdr: ChangeDetectorRef) {}
|
||||
|
||||
subscribeChanges() {
|
||||
this.unsubscribeChanges();
|
||||
if (this.uiStartOption) {
|
||||
this.formGroup.patchValue({ start: (this.uiStartOption.value as any) as Date });
|
||||
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStartOption.changes.subscribe(() => {
|
||||
this.uiStartOption.changes.subscribe(({ target, keys }) => {
|
||||
if (keys.includes('value')) {
|
||||
if (new Date(target.value) !== this.formGroup.get('start').value) {
|
||||
this.formGroup.patchValue({ start: target.value as any });
|
||||
this.startDatepicker?.setDisplayed(new Date(target.value));
|
||||
}
|
||||
}
|
||||
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
}
|
||||
if (this.uiStopOption) {
|
||||
this.formGroup.patchValue({ stop: (this.uiStopOption.value as any) as Date });
|
||||
|
||||
this.optionChangeSubscription.add(
|
||||
this.uiStopOption.changes.subscribe(() => {
|
||||
this.uiStopOption.changes.subscribe(({ target, keys }) => {
|
||||
if (keys.includes('value')) {
|
||||
if (new Date(target.value) !== this.formGroup.get('start').value) {
|
||||
this.formGroup.patchValue({ stop: target.value as any });
|
||||
this.stopDatepicker?.setDisplayed(new Date(target.value));
|
||||
}
|
||||
}
|
||||
|
||||
this.cdr.markForCheck();
|
||||
})
|
||||
);
|
||||
@@ -62,9 +112,57 @@ export class FilterInputOptionDateRangeComponent {
|
||||
this.optionChangeSubscription = new Subscription();
|
||||
}
|
||||
|
||||
setStratValue(date: Date) {
|
||||
// 06.09.2023 Nino Righi --- Code abgeändert, da nicht richtig funktioniert -> Bugticket #4255 HSC // Neue Filteroption - Erscheinungsdatum
|
||||
if (new Date(date)?.toString() === 'Invalid Date') {
|
||||
this.uiStartOption?.setValue(undefined);
|
||||
this.formGroup.patchValue({ start: undefined });
|
||||
this.startDatepicker?.setDisplayed(undefined);
|
||||
this.startDatepicker?.setSelected(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = moment(date, 'L').toDate();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
|
||||
if (this.startDate === date) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiStartOption?.setValue(startDate);
|
||||
this.formGroup.patchValue({ start: startDate });
|
||||
this.startDatepicker?.setDisplayed(startDate ?? new Date());
|
||||
this.startDatepicker?.setSelected(startDate);
|
||||
|
||||
if (this.stopDate && startDate > this.stopDate) {
|
||||
this.setStopValue(startDate);
|
||||
}
|
||||
}
|
||||
|
||||
setStopValue(date: Date) {
|
||||
const stopDate = date;
|
||||
stopDate?.setDate(stopDate?.getDate() + 1); // to include the selected stop date !
|
||||
// 06.09.2023 Nino Righi --- Code abgeändert, da nicht richtig funktioniert -> Bugticket #4255 HSC // Neue Filteroption - Erscheinungsdatum
|
||||
if (new Date(date)?.toString() === 'Invalid Date') {
|
||||
this.uiStopOption?.setValue(undefined);
|
||||
this.formGroup.patchValue({ stop: undefined });
|
||||
this.stopDatepicker?.setDisplayed(undefined);
|
||||
this.stopDatepicker?.setSelected(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const stopDate = moment(date, 'L').toDate();
|
||||
stopDate.setHours(23, 59, 59, 999);
|
||||
|
||||
if (this.stopDate === date) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.uiStopOption?.setValue(stopDate);
|
||||
this.formGroup.patchValue({ stop: stopDate });
|
||||
this.stopDatepicker?.setDisplayed(stopDate ?? new Date());
|
||||
this.stopDatepicker?.setSelected(stopDate);
|
||||
|
||||
if (this.startDate && stopDate < this.startDate) {
|
||||
this.setStratValue(stopDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,28 @@ import { CommonModule } from '@angular/common';
|
||||
|
||||
import { FilterInputOptionDateRangeComponent } from './filter-input-option-date-range.component';
|
||||
import { UiDatepickerModule } from '@ui/datepicker';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { FormControlComponent } from '@shared/components/form-control';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { InputControlModule } from '@shared/components/input-control';
|
||||
import { UiDateInputDirective } from '@ui/input';
|
||||
import { DateInputDirective } from '@shared/forms';
|
||||
|
||||
@NgModule({
|
||||
imports: [CommonModule, UiCommonModule, UiDatepickerModule, FormsModule, UiIconModule],
|
||||
imports: [
|
||||
CommonModule,
|
||||
InputControlModule,
|
||||
FormControlComponent,
|
||||
UiCommonModule,
|
||||
UiDatepickerModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
IconComponent,
|
||||
UiDateInputDirective,
|
||||
DateInputDirective,
|
||||
],
|
||||
exports: [FilterInputOptionDateRangeComponent],
|
||||
declarations: [FilterInputOptionDateRangeComponent],
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<label class="shared-fomr-control-label">{{ displayLabel }}</label>
|
||||
<ng-content select="shared-select, input"></ng-content>
|
||||
<ng-content select="shared-select, input, shared-input-control"></ng-content>
|
||||
<div class="shared-fomr-control-error">
|
||||
{{ control?.errors | firstError: label }}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.shared-input-control {
|
||||
@apply relative leading-[21px] text-p2 font-bold;
|
||||
@apply relative leading-[1.3125rem] text-p2;
|
||||
}
|
||||
|
||||
.shared-input-control:has(input.ng-invalid.ng-dirty) {
|
||||
@@ -7,7 +7,11 @@
|
||||
}
|
||||
|
||||
.shared-input-control-wrapper {
|
||||
@apply flex flex-row items-center grow border border-solid border-[#AEB7C1] rounded-[5px] p-4;
|
||||
@apply grid grid-flow-col items-stretch grow border border-solid border-[#AEB7C1] rounded h-14;
|
||||
}
|
||||
|
||||
.shared-input-control-wrapper input {
|
||||
@apply bg-transparent px-4;
|
||||
}
|
||||
|
||||
.shared-input-control-wrapper:has(input.ng-invalid.ng-dirty) {
|
||||
@@ -31,14 +35,6 @@
|
||||
@apply inline-block grow-0;
|
||||
}
|
||||
|
||||
.shared-input-control-prefix {
|
||||
@apply -ml-2 mr-2;
|
||||
}
|
||||
|
||||
.shared-input-control-suffix {
|
||||
@apply -mr-2 ml-2;
|
||||
}
|
||||
|
||||
.shared-input-control-error {
|
||||
@apply text-left mt-[2px];
|
||||
@apply text-left mt-[.125rem];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { OnDestroy, TemplateRef } from '@angular/core';
|
||||
import { OnDestroy } from '@angular/core';
|
||||
import { QueryList } from '@angular/core';
|
||||
import { ContentChildren } from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, ViewEncapsulation, AfterContentInit, ContentChild, ViewChild } from '@angular/core';
|
||||
@@ -98,7 +98,7 @@ export class InputControlComponent implements AfterContentInit, OnDestroy {
|
||||
console.error(new Error(`No input[sharedInput] found in \`<shared-input-control>\` component`));
|
||||
}
|
||||
|
||||
const statusChangesSub = this.inputDirective.control.statusChanges.subscribe(() => {
|
||||
const statusChangesSub = this.inputDirective.control.statusChanges.subscribe((s) => {
|
||||
this.renderError();
|
||||
this.renderIndicator();
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Highlightable } from '@angular/cdk/a11y';
|
||||
import { Directive, ElementRef, HostListener, Input, Renderer2 } from '@angular/core';
|
||||
|
||||
@Directive({ selector: '[menuItem]', host: { class: 'menu-item', role: 'menuitem', tabindex: '-1' } })
|
||||
@Directive({ selector: '[menuItem], [sharedMenuItem]', host: { class: 'menu-item', role: 'menuitem', tabindex: '-1' } })
|
||||
export class MenuItemDirective implements Highlightable {
|
||||
private _onClick = (_: MenuItemDirective) => {};
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, ContentChildren, QueryList } from '@angular/core';
|
||||
import { MenuItemDirective } from './menu-item.directive';
|
||||
|
||||
@Component({
|
||||
selector: 'menu',
|
||||
selector: 'menu, shared-menu',
|
||||
template: `<ng-content [selector]="[menuItem]"></ng-content>`,
|
||||
host: { class: 'menu', role: 'menu' },
|
||||
exportAs: 'menu',
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { AfterContentInit, ChangeDetectionStrategy, Component, ElementRef, Renderer2 } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-scale-content, [sharedScaleContent]',
|
||||
template: '<ng-content></ng-content>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class ScaleContentComponent implements AfterContentInit {
|
||||
// TODO: Bessere Lösung finden? Falls keine bessere Lösung gefunden wird, dann muss die Komponente auslagen
|
||||
|
||||
fontSizeInEm = 1;
|
||||
|
||||
adjustmentSteps = 0.05;
|
||||
|
||||
constructor(private _elementRef: ElementRef<HTMLElement>, private _renderer: Renderer2) {}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.adjustFontSize();
|
||||
}
|
||||
|
||||
adjustFontSize() {
|
||||
const element = this._elementRef.nativeElement;
|
||||
|
||||
const clientRect = element?.getClientRects();
|
||||
const scrollHeight = element?.scrollHeight;
|
||||
|
||||
const domRect = clientRect && clientRect[0];
|
||||
|
||||
if (domRect && Math.ceil(domRect?.height) < scrollHeight) {
|
||||
this.fontSizeInEm -= this.adjustmentSteps;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this._renderer.setStyle(element, 'font-size', `${this.fontSizeInEm}em`);
|
||||
|
||||
setTimeout(() => this.adjustFontSize(), 1);
|
||||
}
|
||||
}
|
||||
1
apps/shared/components/scale-content/src/public-api.ts
Normal file
1
apps/shared/components/scale-content/src/public-api.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './lib/scale-content.component';
|
||||
6
apps/shared/forms/ng-package.json
Normal file
6
apps/shared/forms/ng-package.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
|
||||
"lib": {
|
||||
"entryFile": "src/public-api.ts"
|
||||
}
|
||||
}
|
||||
92
apps/shared/forms/src/lib/directives/date-input.directive.ts
Normal file
92
apps/shared/forms/src/lib/directives/date-input.directive.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { ChangeDetectorRef, Directive, HostBinding, HostListener, Input, forwardRef } from '@angular/core';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import moment from 'moment';
|
||||
import { DE_DATE_REGEX, ISO_DATE_REGEX } from '../regex';
|
||||
|
||||
@Directive({
|
||||
selector: 'input[type="text"][sharedDateInput]',
|
||||
standalone: true,
|
||||
providers: [
|
||||
{
|
||||
provide: NG_VALUE_ACCESSOR,
|
||||
useExisting: forwardRef(() => DateInputDirective),
|
||||
multi: true,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class DateInputDirective implements ControlValueAccessor {
|
||||
private _onChange = (_: any) => {};
|
||||
|
||||
private _onTouched = () => {};
|
||||
|
||||
@Input()
|
||||
value: any;
|
||||
|
||||
@HostBinding('value')
|
||||
displayValue: string = '';
|
||||
|
||||
@HostBinding('disabled')
|
||||
disabled: boolean;
|
||||
|
||||
constructor(private _cdr: ChangeDetectorRef) {}
|
||||
|
||||
writeValue(obj: any): void {
|
||||
this.value = obj;
|
||||
this.setDisplayValue(obj);
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
this._onChange = fn;
|
||||
}
|
||||
|
||||
registerOnTouched(fn: any): void {
|
||||
this._onTouched = fn;
|
||||
}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {
|
||||
this.disabled = isDisabled;
|
||||
}
|
||||
|
||||
setDisplayValue(value: any) {
|
||||
let date: Date;
|
||||
|
||||
if (value instanceof Date) {
|
||||
date = value;
|
||||
} else {
|
||||
date = new Date(value);
|
||||
}
|
||||
|
||||
// 06.09.2023 Nino Righi --- Code Auskommentiert und abgeändert, da nicht richtig funktioniert -> Bugticket #4255 HSC // Neue Filteroption - Erscheinungsdatum
|
||||
|
||||
// else if (DE_DATE_REGEX.test(value)) {
|
||||
// const mom = moment(value, 'L');
|
||||
// date = mom.toDate();
|
||||
// } else if (ISO_DATE_REGEX.test(value)) {
|
||||
// date = new Date(value);
|
||||
// }
|
||||
|
||||
if (date && date.toString() !== 'Invalid Date') {
|
||||
this.displayValue = moment(date).format('L');
|
||||
} else {
|
||||
this.displayValue = value ?? '';
|
||||
}
|
||||
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
@HostListener('input', ['$event.target.value'])
|
||||
onInput(value: string) {
|
||||
let date: Date;
|
||||
if (DE_DATE_REGEX.test(value)) {
|
||||
const mom = moment(value, 'L');
|
||||
date = mom.toDate();
|
||||
}
|
||||
|
||||
this.value = date ?? value;
|
||||
|
||||
this.displayValue = value;
|
||||
|
||||
this._onTouched();
|
||||
this._onChange(this.value);
|
||||
}
|
||||
}
|
||||
1
apps/shared/forms/src/lib/directives/index.ts
Normal file
1
apps/shared/forms/src/lib/directives/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './date-input.directive';
|
||||
3
apps/shared/forms/src/lib/regex.ts
Normal file
3
apps/shared/forms/src/lib/regex.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const DE_DATE_REGEX = /^(0?[1-9]|[12][0-9]|3[01])\.(0?[1-9]|1[0-2])\.\d{4}$/g;
|
||||
|
||||
export const ISO_DATE_REGEX = /^[0-9]{4}-((0[13578]|1[02])-(0[1-9]|[12][0-9]|3[01])|(0[469]|11)-(0[1-9]|[12][0-9]|30)|(02)-(0[1-9]|[12][0-9]))T(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|[1-5][0-9]):(0[0-9]|[1-5][0-9])\.[0-9]{3}Z$/g;
|
||||
29
apps/shared/forms/src/lib/validators/date.validator.ts
Normal file
29
apps/shared/forms/src/lib/validators/date.validator.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AbstractControl } from '@angular/forms';
|
||||
import { DE_DATE_REGEX, ISO_DATE_REGEX } from '../regex';
|
||||
import moment from 'moment';
|
||||
|
||||
export function DateValidator(control: AbstractControl) {
|
||||
if (control.value) {
|
||||
let date: Date = null;
|
||||
|
||||
if (control.value instanceof Date) {
|
||||
date = control.value;
|
||||
} else if (typeof control.value === 'string') {
|
||||
date = new Date(control.value);
|
||||
|
||||
// 06.09.2023 Nino Righi --- Code Auskommentiert und abgeändert, da nicht richtig funktioniert -> Bugticket #4255 HSC // Neue Filteroption - Erscheinungsdatum
|
||||
|
||||
// if (DE_DATE_REGEX.test(control.value)) {
|
||||
// date = moment(control.value, 'L').toDate();
|
||||
// } else if (ISO_DATE_REGEX.test(control.value)) {
|
||||
// date = new Date(control.value);
|
||||
// }
|
||||
}
|
||||
|
||||
if (date?.toString() === 'Invalid Date' || date === null) {
|
||||
return { date: 'Datum ist ungültig' };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
1
apps/shared/forms/src/lib/validators/index.ts
Normal file
1
apps/shared/forms/src/lib/validators/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './date.validator';
|
||||
2
apps/shared/forms/src/public-api.ts
Normal file
2
apps/shared/forms/src/public-api.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './lib/directives';
|
||||
export * from './lib/validators';
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
|
||||
import { ComponentStore, OnStoreInit, tapResponse } from '@ngrx/component-store';
|
||||
import { AddToShoppingCartDTO, AvailabilityDTO, BranchDTO, CheckoutDTO, ShoppingCartDTO, ShoppingCartItemDTO } from '@swagger/checkout';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import { catchError, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
BranchService,
|
||||
DisplayOrderDTO,
|
||||
@@ -12,12 +12,13 @@ import {
|
||||
ResponseArgsOfIEnumerableOfBranchDTO,
|
||||
ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString,
|
||||
} from '@swagger/oms';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Observable, of, zip } from 'rxjs';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { getCatalogProductNumber } from './catalog-product-number';
|
||||
import { ItemDTO } from '@swagger/cat';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
|
||||
export interface KulturpassOrderModalState {
|
||||
orderItemListItem?: OrderItemListItemDTO;
|
||||
@@ -200,9 +201,18 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
|
||||
addItemToShoppingCart = this.effect((item$: Observable<ItemDTO>) =>
|
||||
item$.pipe(
|
||||
mergeMap((item) =>
|
||||
this._availabilityService
|
||||
.getTakeAwayAvailability({
|
||||
mergeMap((item) => {
|
||||
const takeAwayAvailability$ = this._availabilityService.getTakeAwayAvailability({
|
||||
item: {
|
||||
ean: item.product.ean,
|
||||
itemId: item.id,
|
||||
price: item.catalogAvailability.price,
|
||||
},
|
||||
quantity: this.itemQuantityByCatalogProductNumber(getCatalogProductNumber(item)) + 1,
|
||||
});
|
||||
|
||||
const deliveryAvailability$ = this._availabilityService
|
||||
.getDeliveryAvailability({
|
||||
item: {
|
||||
ean: item.product.ean,
|
||||
itemId: item.id,
|
||||
@@ -210,12 +220,45 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
},
|
||||
quantity: this.itemQuantityByCatalogProductNumber(getCatalogProductNumber(item)) + 1,
|
||||
})
|
||||
.pipe(tapResponse(this.handleAddItemToShoppingCartResponse(item), this.handleAddItemToShoppingCartError))
|
||||
)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
return of(undefined);
|
||||
})
|
||||
);
|
||||
|
||||
return zip(takeAwayAvailability$, deliveryAvailability$).pipe(
|
||||
tapResponse(this.handleAddItemToShoppingCartResponse2(item), this.handleAddItemToShoppingCartError)
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
handleAddItemToShoppingCartResponse = (item: ItemDTO) => (availability: AvailabilityDTO) => {
|
||||
handleAddItemToShoppingCartResponse2 = (item: ItemDTO) => ([takeAwayAvailability, deliveryAvailability]: [
|
||||
AvailabilityDTO,
|
||||
AvailabilityDTO
|
||||
]) => {
|
||||
const isPriceMaintained = deliveryAvailability['priceMaintained'] ?? false;
|
||||
const offlinePrice = takeAwayAvailability.price?.value?.value ?? -1;
|
||||
const onlinePrice = deliveryAvailability?.price?.value?.value ?? -1;
|
||||
|
||||
const availability = takeAwayAvailability;
|
||||
|
||||
/**
|
||||
* Onlinepreis ist niedliger als der Offlinepreis
|
||||
* wenn der Artikel nicht Preisgebunden ist, wird der Onlinepreis genommen
|
||||
* wenn der Artikel Preisgebunden ist, wird der Ladenpreis verwendet
|
||||
*/
|
||||
|
||||
/**
|
||||
* Offlinepreis ist niedliger als der Onlinepreis
|
||||
* wenn der Artikel nicht Preisgebunden ist, wird der Ladenpreis genommen
|
||||
* wenn der Artikel Preisgebunden ist, wird der Ladenpreis verwendet
|
||||
*/
|
||||
|
||||
if (onlinePrice < offlinePrice && !isPriceMaintained) {
|
||||
availability.price = deliveryAvailability.price;
|
||||
}
|
||||
|
||||
this.setAvailability({
|
||||
catalogProductNumber: getCatalogProductNumber(item),
|
||||
availability: availability,
|
||||
@@ -240,6 +283,31 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
this.addItem(addToShoppingCartDTO);
|
||||
};
|
||||
|
||||
// handleAddItemToShoppingCartResponse = (item: ItemDTO) => (availability: AvailabilityDTO) => {
|
||||
// this.setAvailability({
|
||||
// catalogProductNumber: getCatalogProductNumber(item),
|
||||
// availability: availability,
|
||||
// });
|
||||
|
||||
// const addToShoppingCartDTO: AddToShoppingCartDTO = {
|
||||
// quantity: 1,
|
||||
// availability: availability,
|
||||
// destination: {
|
||||
// data: {
|
||||
// target: 1,
|
||||
// targetBranch: { id: this.branch.id },
|
||||
// },
|
||||
// },
|
||||
// promotion: {
|
||||
// points: 0,
|
||||
// },
|
||||
// itemType: item.type,
|
||||
// product: { catalogProductNumber: getCatalogProductNumber(item), ...item.product },
|
||||
// };
|
||||
|
||||
// this.addItem(addToShoppingCartDTO);
|
||||
// };
|
||||
|
||||
handleAddItemToShoppingCartError = (err: any) => {
|
||||
this._modal.error('Fehler beim Hinzufügen des Artikels', err);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="shared-purchase-options-list-item__contributors font-bold">
|
||||
{{ product?.contributors }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__name font-bold h-12" scaleContent>
|
||||
<div class="shared-purchase-options-list-item__name font-bold h-12" sharedScaleContent>
|
||||
{{ product?.name }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__format flex flex-row items-center">
|
||||
@@ -82,67 +82,40 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__price text-right ml-4 flex flex-col items-end">
|
||||
<div
|
||||
class="shared-purchase-options-list-item__price-value font-bold text-xl flex flex-row items-center"
|
||||
*ngIf="!(canEditPrice$ | async)"
|
||||
>
|
||||
<div class="shared-purchase-options-list-item__price-value font-bold text-xl flex flex-row items-center">
|
||||
<ui-svg-icon class="mr-3" [uiOverlayTrigger]="tooltip" icon="mat-info" *ngIf="priceMaintained$ | async"></ui-svg-icon>
|
||||
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [xOffset]="5" [closeable]="true">
|
||||
Günstigerer Preis aus Hugendubel Katalog wird übernommen
|
||||
</ui-tooltip>
|
||||
<ng-container *ngIf="!(setManualPrice$ | async); else setManualPrice">
|
||||
{{ priceValue$ | async | currency: 'EUR':'code' }}
|
||||
</ng-container>
|
||||
<ng-template #setManualPrice>
|
||||
<div class="relative flex flex-row items-start">
|
||||
<ui-select
|
||||
class="w-[6.5rem] min-h-[3.4375rem] p-4 rounded border border-solid border-[#AEB7C1] mr-4"
|
||||
tabindex="-1"
|
||||
[formControl]="manualVatFormControl"
|
||||
[defaultLabel]="'MwSt'"
|
||||
>
|
||||
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
|
||||
</ui-select>
|
||||
|
||||
<shared-input-control [class.ml-8]="manualPriceFormControl?.invalid && manualPriceFormControl?.dirty">
|
||||
<shared-input-control-indicator>
|
||||
<shared-icon *ngIf="manualPriceFormControl?.invalid && manualPriceFormControl?.dirty" icon="mat-info"></shared-icon>
|
||||
</shared-input-control-indicator>
|
||||
<input
|
||||
triggerOn="init"
|
||||
#quantityInput
|
||||
sharedInputControlInput
|
||||
type="string"
|
||||
class="w-24"
|
||||
[formControl]="manualPriceFormControl"
|
||||
placeholder="00,00"
|
||||
(sharedOnInit)="quantityInput.focus()"
|
||||
sharedNumberValue
|
||||
/>
|
||||
<shared-input-control-suffix>EUR</shared-input-control-suffix>
|
||||
<shared-input-control-error error="required">Preis ist ungültig</shared-input-control-error>
|
||||
<shared-input-control-error error="pattern">Preis ist ungültig</shared-input-control-error>
|
||||
<shared-input-control-error error="max">Preis ist ungültig</shared-input-control-error>
|
||||
</shared-input-control>
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__price-value font-bold text-xl" *ngIf="canEditPrice$ | async">
|
||||
<div class="relative flex flex-col">
|
||||
<shared-input-control>
|
||||
<div class="relative flex flex-row justify-end items-start">
|
||||
<ui-select
|
||||
*ngIf="canEditVat$ | async"
|
||||
class="w-[6.5rem] min-h-[3.4375rem] p-4 rounded-card border border-solid border-[#AEB7C1] mr-4"
|
||||
tabindex="-1"
|
||||
[formControl]="manualVatFormControl"
|
||||
[defaultLabel]="'MwSt'"
|
||||
>
|
||||
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
|
||||
</ui-select>
|
||||
<shared-input-control
|
||||
[class.ml-6]="priceFormControl?.invalid && priceFormControl?.dirty"
|
||||
*ngIf="canEditPrice$ | async; else priceTmpl"
|
||||
>
|
||||
<shared-input-control-indicator>
|
||||
<shared-icon *ngIf="priceFormControl?.invalid && priceFormControl?.dirty" icon="mat-info"></shared-icon>
|
||||
</shared-input-control-indicator>
|
||||
<input
|
||||
[uiOverlayTrigger]="tooltip"
|
||||
triggerOn="init"
|
||||
[uiOverlayTrigger]="giftCardTooltip"
|
||||
triggerOn="none"
|
||||
#quantityInput
|
||||
#priceOverlayTrigger="uiOverlayTrigger"
|
||||
sharedInputControlInput
|
||||
type="string"
|
||||
class="w-24"
|
||||
[formControl]="priceFormControl"
|
||||
placeholder="00,00"
|
||||
(sharedOnInit)="quantityInput.focus()"
|
||||
(sharedOnInit)="onPriceInputInit(quantityInput, priceOverlayTrigger)"
|
||||
sharedNumberValue
|
||||
/>
|
||||
<shared-input-control-suffix>EUR</shared-input-control-suffix>
|
||||
@@ -152,11 +125,14 @@
|
||||
<shared-input-control-error error="max">Preis ist ungültig</shared-input-control-error>
|
||||
</shared-input-control>
|
||||
|
||||
<ui-tooltip [warning]="true" xPosition="after" yPosition="below" [xOffset]="-55" [yOffset]="18" [closeable]="true" #tooltip>
|
||||
<ui-tooltip [warning]="true" xPosition="after" yPosition="below" [xOffset]="-55" [yOffset]="18" [closeable]="true" #giftCardTooltip>
|
||||
Tragen Sie hier den <br />
|
||||
Gutscheinbetrag ein.
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<ng-template #priceTmpl>
|
||||
{{ priceValue$ | async | currency: 'EUR':'code' }}
|
||||
</ng-template>
|
||||
</div>
|
||||
<ui-quantity-dropdown class="mt-2" [formControl]="quantityFormControl" [range]="maxSelectableQuantity$ | async"> </ui-quantity-dropdown>
|
||||
<div class="pt-7">
|
||||
|
||||
@@ -1,78 +1,22 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
AfterContentInit,
|
||||
ElementRef,
|
||||
Renderer2,
|
||||
} from '@angular/core';
|
||||
import { Component, ChangeDetectionStrategy, Input, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { InputControlModule } from '@shared/components/input-control';
|
||||
import { ElementLifecycleModule } from '@shared/directives/element-lifecycle';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiCommonModule, UiOverlayTriggerDirective } from '@ui/common';
|
||||
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { combineLatest, ReplaySubject, Subscription } from 'rxjs';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { map, take, shareReplay, startWith, switchMap, withLatestFrom, last } from 'rxjs/operators';
|
||||
import { map, take, shareReplay, startWith, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
|
||||
import { Item, PurchaseOptionsStore } from '../store';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { UiSelectModule } from '@ui/select';
|
||||
import { KeyValueDTOOfStringAndString } from '@swagger/cat';
|
||||
|
||||
@Component({
|
||||
selector: 'scale-content, [scaleContent]',
|
||||
template: '<ng-content></ng-content>',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
styles: [
|
||||
`
|
||||
:host {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class ScaleContentComponent implements AfterContentInit {
|
||||
// TODO: Bessere Lösung finden? Falls keine bessere Lösung gefunden wird, dann muss die Komponente auslagen
|
||||
|
||||
fontSizeInEm = 1;
|
||||
|
||||
adjustmentSteps = 0.05;
|
||||
|
||||
constructor(private _elementRef: ElementRef<HTMLElement>, private _renderer: Renderer2) {}
|
||||
|
||||
ngAfterContentInit(): void {
|
||||
this.adjustFontSize();
|
||||
}
|
||||
|
||||
adjustFontSize() {
|
||||
const element = this._elementRef.nativeElement;
|
||||
|
||||
const clientRect = element?.getClientRects();
|
||||
const scrollHeight = element?.scrollHeight;
|
||||
|
||||
const domRect = clientRect && clientRect[0];
|
||||
|
||||
if (domRect && Math.ceil(domRect?.height) < scrollHeight) {
|
||||
this.fontSizeInEm -= this.adjustmentSteps;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this._renderer.setStyle(element, 'font-size', `${this.fontSizeInEm}em`);
|
||||
|
||||
setTimeout(() => this.adjustFontSize(), 1);
|
||||
}
|
||||
}
|
||||
import { ScaleContentComponent } from '@shared/components/scale-content';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-list-item',
|
||||
@@ -115,14 +59,22 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
|
||||
|
||||
quantityFormControl = new FormControl<number>(null);
|
||||
|
||||
priceFormControl = new FormControl<string>(null, [
|
||||
private readonly _giftCardValidators = [
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.max(GIFT_CARD_MAX_PRICE),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
]);
|
||||
];
|
||||
|
||||
private readonly _defaultValidators = [
|
||||
Validators.required,
|
||||
Validators.min(0.01),
|
||||
Validators.max(999.99),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
priceFormControl = new FormControl<string>(null);
|
||||
|
||||
manualPriceFormControl = new FormControl<string>(null, [Validators.required, Validators.max(999.99), Validators.pattern(PRICE_PATTERN)]);
|
||||
manualVatFormControl = new FormControl<string>('', [Validators.required]);
|
||||
|
||||
selectedFormControl = new FormControl<boolean>(false);
|
||||
@@ -160,10 +112,20 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
|
||||
|
||||
priceVat$ = this.price$.pipe(map((price) => price?.vat?.vatType));
|
||||
|
||||
canEditPrice$ = this.item$.pipe(switchMap((item) => this._store.getCanEditPrice$(item.id)));
|
||||
|
||||
canAddResult$ = this.item$.pipe(switchMap((item) => this._store.getCanAddResultForItemAndCurrentPurchaseOption$(item.id)));
|
||||
|
||||
canEditPrice$ = this.item$.pipe(
|
||||
switchMap((item) => combineLatest([this.canAddResult$, this._store.getCanEditPrice$(item.id)])),
|
||||
map(([canAddResult, canEditPrice]) => canAddResult?.canAdd && canEditPrice)
|
||||
);
|
||||
|
||||
canEditVat$ = this.item$.pipe(
|
||||
switchMap((item) => combineLatest([this.canAddResult$, this._store.getCanEditVat$(item.id)])),
|
||||
map(([canAddResult, canEditVat]) => canAddResult?.canAdd && canEditVat)
|
||||
);
|
||||
|
||||
isGiftCard$ = this.item$.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id)));
|
||||
|
||||
maxSelectableQuantity$ = combineLatest([this._store.purchaseOption$, this.availability$]).pipe(
|
||||
map(([purchaseOption, availability]) => {
|
||||
if (purchaseOption === 'in-store') {
|
||||
@@ -205,6 +167,14 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
|
||||
|
||||
constructor(private _store: PurchaseOptionsStore) {}
|
||||
|
||||
onPriceInputInit(target: HTMLElement, overlayTrigger: UiOverlayTriggerDirective) {
|
||||
if (this._store.getIsGiftCard(this.item.id)) {
|
||||
overlayTrigger.open();
|
||||
}
|
||||
|
||||
target?.focus();
|
||||
}
|
||||
|
||||
// Wichtig für das korrekte Setzen des Preises an das Item für den Endpoint request
|
||||
parsePrice(value: string) {
|
||||
if (PRICE_PATTERN.test(value)) {
|
||||
@@ -225,10 +195,11 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initPriceValidatorSubscription();
|
||||
this.initQuantitySubscription();
|
||||
this.initPriceSubscription();
|
||||
this.initVatSubscription();
|
||||
this.initSelectedSubscription();
|
||||
this.initManualPriceSubscriptions();
|
||||
}
|
||||
|
||||
ngOnChanges({ item }: SimpleChanges) {
|
||||
@@ -242,14 +213,16 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
// Ticket #4074 analog zu Ticket #2244
|
||||
// Logik gilt ausschließlich für Archivartikel und über die Kaufoptionen. Nicht über den Warenkorb
|
||||
async initManualPriceSubscriptions() {
|
||||
const isManualPrice = await this.setManualPrice$.pipe(last()).toPromise();
|
||||
if (!!isManualPrice) {
|
||||
this.initManualPriceSubscription();
|
||||
this.initManualVatSubscription();
|
||||
}
|
||||
initPriceValidatorSubscription() {
|
||||
const sub = this.item$.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id))).subscribe((isGiftCard) => {
|
||||
if (isGiftCard) {
|
||||
this.priceFormControl.setValidators(this._giftCardValidators);
|
||||
} else {
|
||||
this.priceFormControl.setValidators(this._defaultValidators);
|
||||
}
|
||||
});
|
||||
|
||||
this._subscriptions.add(sub);
|
||||
}
|
||||
|
||||
initQuantitySubscription() {
|
||||
@@ -279,7 +252,6 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
|
||||
if (priceStr === '') return;
|
||||
|
||||
if (this.parsePrice(this.priceFormControl.value) !== price?.value?.value) {
|
||||
debugger;
|
||||
this.priceFormControl.setValue(priceStr);
|
||||
}
|
||||
});
|
||||
@@ -305,34 +277,7 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initManualPriceSubscription() {
|
||||
const sub = this.price$.subscribe((price) => {
|
||||
const priceStr = this.stringifyPrice(price?.value?.value);
|
||||
if (priceStr === '') return;
|
||||
|
||||
if (this.parsePrice(this.manualPriceFormControl.value) !== price?.value?.value) {
|
||||
this.manualPriceFormControl.setValue(priceStr);
|
||||
}
|
||||
});
|
||||
|
||||
const valueChangesSub = this.manualPriceFormControl.valueChanges.subscribe((value) => {
|
||||
const price = this._store.getPrice(this.item.id);
|
||||
const parsedPrice = this.parsePrice(value);
|
||||
|
||||
if (!parsedPrice) {
|
||||
this._store.setPrice(this.item.id, null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item.id] !== parsedPrice) {
|
||||
this._store.setPrice(this.item.id, this.parsePrice(value), true);
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initManualVatSubscription() {
|
||||
initVatSubscription() {
|
||||
const valueChangesSub = this.manualVatFormControl.valueChanges.pipe(withLatestFrom(this.vats$)).subscribe(([formVatType, vats]) => {
|
||||
const price = this._store.getPrice(this.item.id);
|
||||
|
||||
|
||||
@@ -25,18 +25,13 @@
|
||||
</div>
|
||||
<div class="text-center -mx-4 border-t border-gray-200 p-4 border-solid">
|
||||
<ng-container *ngIf="type === 'add'">
|
||||
<button
|
||||
type="button"
|
||||
class="isa-cta-button"
|
||||
[disabled]="!(canContinue$ | async) || saving || !(hasPrice$ | async)"
|
||||
(click)="save('continue-shopping')"
|
||||
>
|
||||
<button type="button" class="isa-cta-button" [disabled]="!(canContinue$ | async) || saving" (click)="save('continue-shopping')">
|
||||
Weiter einkaufen
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-4 isa-cta-button isa-button-primary"
|
||||
[disabled]="!(canContinue$ | async) || saving || !(hasPrice$ | async)"
|
||||
[disabled]="!(canContinue$ | async) || saving"
|
||||
(click)="save('continue')"
|
||||
>
|
||||
Fortfahren
|
||||
@@ -46,7 +41,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="ml-4 isa-cta-button isa-button-primary"
|
||||
[disabled]="!(canContinue$ | async) || saving || !(hasPrice$ | async)"
|
||||
[disabled]="!(canContinue$ | async) || saving"
|
||||
(click)="save('continue')"
|
||||
>
|
||||
Fortfahren
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
PickupPurchaseOptionTileComponent,
|
||||
} from './purchase-options-tile';
|
||||
import { isGiftCard, Item, PurchaseOption, PurchaseOptionsStore } from './store';
|
||||
import { delay, map, shareReplay, skip, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { delay, map, shareReplay, skip, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { KeyValueDTOOfStringAndString } from '@swagger/cat';
|
||||
|
||||
@Component({
|
||||
@@ -83,7 +83,7 @@ export class PurchaseOptionsModalComponent implements OnInit, OnDestroy {
|
||||
|
||||
hasDownload$ = this.purchasingOptions$.pipe(map((purchasingOptions) => purchasingOptions.includes('download')));
|
||||
|
||||
canContinue$ = this.store.canContinue$.pipe(shareReplay(1));
|
||||
canContinue$ = this.store.canContinue$;
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
|
||||
@@ -60,6 +60,14 @@ export function isGiftCard(item: Item, type: ActionType): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
export function isArchive(item: Item, type: ActionType): boolean {
|
||||
if (isItemDTO(item, type)) {
|
||||
return item?.features?.some((f) => f.key === 'ARC');
|
||||
} else {
|
||||
return !!item?.features?.['ARC'];
|
||||
}
|
||||
}
|
||||
|
||||
export function mapToItemPayload({
|
||||
item,
|
||||
quantity,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { PriceDTO, PriceValueDTO } from '@swagger/checkout';
|
||||
import { DEFAULT_PRICE_DTO, DEFAULT_PRICE_VALUE, GIFT_CARD_MAX_PRICE, GIFT_CARD_TYPE, PURCHASE_OPTIONS } from '../constants';
|
||||
import { isGiftCard, isItemDTO } from './purchase-options.helpers';
|
||||
import { isArchive, isGiftCard, isItemDTO } from './purchase-options.helpers';
|
||||
import { PurchaseOptionsState } from './purchase-options.state';
|
||||
import { ActionType, Availability, Branch, CanAdd, FetchingAvailability, Item, PurchaseOption } from './purchase-options.types';
|
||||
|
||||
@@ -202,27 +202,47 @@ export function getCanEditPrice(itemId: number): (state: PurchaseOptionsState) =
|
||||
return (state) => {
|
||||
const item = getItems(state).find((item) => item.id === itemId);
|
||||
|
||||
if (isGiftCard(item, getType(state))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const purchaseOption = getPurchaseOption(state);
|
||||
|
||||
if (isArchive(item, getType(state)) && !getAvailabilityPriceForPurchaseOption(itemId, purchaseOption)(state)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export function getCanEditVat(itemId: number): (state: PurchaseOptionsState) => boolean {
|
||||
return (state) => {
|
||||
const item = getItems(state).find((item) => item.id === itemId);
|
||||
const purchaseOption = getPurchaseOption(state);
|
||||
|
||||
if (isArchive(item, getType(state)) && !getAvailabilityPriceForPurchaseOption(itemId, purchaseOption)(state)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export function getIsGiftCard(itemId: number): (state: PurchaseOptionsState) => boolean {
|
||||
return (state) => {
|
||||
const item = getItems(state).find((item) => item.id === itemId);
|
||||
return isGiftCard(item, getType(state));
|
||||
};
|
||||
}
|
||||
|
||||
export function getPriceForPurchaseOption(
|
||||
export function getAvailabilityPriceForPurchaseOption(
|
||||
itemId: number,
|
||||
purchaseOption: PurchaseOption
|
||||
): (state: PurchaseOptionsState) => PriceDTO & { fromCatalogue?: boolean } {
|
||||
): (state: PurchaseOptionsState) => (PriceDTO & { fromCatalogue?: boolean }) | undefined {
|
||||
return (state) => {
|
||||
if (getCanEditPrice(itemId)(state)) {
|
||||
const price = getPrices(state)[itemId];
|
||||
|
||||
if (price) {
|
||||
return price;
|
||||
}
|
||||
}
|
||||
|
||||
const item = getItems(state).find((item) => item.id === itemId);
|
||||
|
||||
const type = getType(state);
|
||||
|
||||
let availabilities = getAvailabilitiesForItem(itemId)(state);
|
||||
|
||||
let availability = availabilities.find((availability) => availability.purchaseOption === purchaseOption);
|
||||
@@ -259,13 +279,29 @@ export function getPriceForPurchaseOption(
|
||||
}
|
||||
|
||||
if (isItemDTO(item, type)) {
|
||||
return item?.catalogAvailability?.price ?? DEFAULT_PRICE_DTO;
|
||||
return item?.catalogAvailability?.price;
|
||||
} else {
|
||||
return item?.unitPrice ?? DEFAULT_PRICE_DTO;
|
||||
return item?.unitPrice;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getPriceForPurchaseOption(
|
||||
itemId: number,
|
||||
purchaseOption: PurchaseOption
|
||||
): (state: PurchaseOptionsState) => PriceDTO & { fromCatalogue?: boolean } {
|
||||
return (state) => {
|
||||
if (getCanEditPrice(itemId)(state)) {
|
||||
const price = getPrices(state)[itemId];
|
||||
if (price) {
|
||||
return price;
|
||||
}
|
||||
}
|
||||
|
||||
return getAvailabilityPriceForPurchaseOption(itemId, purchaseOption)(state) ?? DEFAULT_PRICE_DTO;
|
||||
};
|
||||
}
|
||||
|
||||
export function getQuantityForItem(itemId: number): (state: PurchaseOptionsState) => number {
|
||||
return (state) => {
|
||||
const item = getItems(state).find((item) => item.id === itemId);
|
||||
@@ -352,12 +388,21 @@ export function canContinue(state: PurchaseOptionsState): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const actionType = getType(state);
|
||||
|
||||
for (let item of items) {
|
||||
if (isGiftCard(item, getType(state))) {
|
||||
if (isGiftCard(item, actionType)) {
|
||||
const price = getPriceForPurchaseOption(item.id, purchaseOption)(state);
|
||||
if (!(price?.value?.value > 0 && price?.value?.value <= GIFT_CARD_MAX_PRICE)) {
|
||||
return false;
|
||||
}
|
||||
} else if (isArchive(item, actionType) && !getAvailabilityPriceForPurchaseOption(item.id, purchaseOption)(state)) {
|
||||
const price = getPriceForPurchaseOption(item.id, purchaseOption)(state);
|
||||
const hasPrice = price?.value?.value > 0;
|
||||
const hasVat = price?.vat?.vatType > 0;
|
||||
if (!(hasPrice && hasVat)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -612,8 +612,8 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
});
|
||||
}
|
||||
|
||||
isGiftcard(itemId: number) {
|
||||
return this._service;
|
||||
getIsGiftCard(itemId: number) {
|
||||
return this.get(Selectors.getIsGiftCard(itemId));
|
||||
}
|
||||
|
||||
getPrice(itemId: number) {
|
||||
@@ -640,9 +640,22 @@ export class PurchaseOptionsStore extends ComponentStore<PurchaseOptionsState> {
|
||||
return this.select(Selectors.getCanEditPrice(itemId));
|
||||
}
|
||||
|
||||
getCanEditVat(itemId: number) {
|
||||
return this.get(Selectors.getCanEditVat(itemId));
|
||||
}
|
||||
|
||||
getCanEditVat$(itemId: number) {
|
||||
return this.select(Selectors.getCanEditVat(itemId));
|
||||
}
|
||||
|
||||
getIsGiftCard$(itemId: number) {
|
||||
return this.select(Selectors.getIsGiftCard(itemId));
|
||||
}
|
||||
|
||||
setPrice(itemId: number, value: number, manually: boolean = false) {
|
||||
const prices = this.prices;
|
||||
let price = prices[itemId];
|
||||
|
||||
if (price?.value?.value !== value) {
|
||||
if (!price) {
|
||||
price = {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="desktop-small:hidden px-3 create-process-btn grid start-process-btn grid-flow-col items-center justify-center gap-[0.625rem] grow-0 shrink-0"
|
||||
class="desktop-small:hidden px-3 shell-process-bar__create-process-btn-tablet grid start-process-btn grid-flow-col items-center justify-center gap-[0.625rem] grow-0 shrink-0"
|
||||
[cdkMenuTriggerFor]="menuTpl"
|
||||
type="menu"
|
||||
>
|
||||
@@ -41,14 +41,19 @@
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="hidden desktop-small:grid px-3 create-process-btn start-process-btn grid-flow-col items-center justify-center gap-[0.625rem] grow-0 shrink-0"
|
||||
class="hidden desktop-small:grid px-3 shell-process-bar__create-process-btn-desktop start-process-btn grid-flow-col items-center justify-center gap-[0.625rem] grow-0 shrink-0"
|
||||
(click)="createProcess('product')"
|
||||
type="button"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="createProcessButtonContent"></ng-container>
|
||||
</button>
|
||||
<div class="grow"></div>
|
||||
<button type="button" [disabled]="!(processes$ | async)?.length" class="grow-0 shrink-0 px-3 mr-[.125rem]" (click)="closeAllProcesses()">
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="!(processes$ | async)?.length"
|
||||
class="grow-0 shrink-0 px-3 mr-[.125rem] shell-process-bar__close-processes"
|
||||
(click)="closeAllProcesses()"
|
||||
>
|
||||
<div
|
||||
class="rounded border border-solid flex flex-row pl-3 pr-[0.625rem] py-[0.375rem]"
|
||||
[class.text-brand]="(processes$ | async)?.length"
|
||||
@@ -72,15 +77,19 @@
|
||||
<ng-template #menuTpl>
|
||||
<div class="menu" cdkMenu>
|
||||
<button cdkMenuItem class="menu-item" type="button" (click)="createProcess('product')">
|
||||
<shared-icon class="mr-2" icon="import-contacts"></shared-icon>
|
||||
Artikelsuche
|
||||
</button>
|
||||
<button cdkMenuItem class="menu-item" type="button" (click)="createProcess('customer')">
|
||||
<shared-icon class="mr-2" icon="person"></shared-icon>
|
||||
Kundensuche
|
||||
</button>
|
||||
<button *ifRole="'Store'" cdkMenuItem class="menu-item" type="button" (click)="createProcess('goods-out')">
|
||||
<shared-icon class="mr-2" icon="unarchive"></shared-icon>
|
||||
Warenausgabe
|
||||
</button>
|
||||
<button *ifRole="'CallCenter'" cdkMenuItem class="menu-item" type="button" (click)="createProcess('order')">
|
||||
<shared-icon class="mr-2" icon="deployed-code"></shared-icon>
|
||||
Kundenbestellungen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -68,15 +68,19 @@ export class ShellProcessBarComponent implements OnInit {
|
||||
setTimeout(() => this.scrollToEnd(), 25);
|
||||
}
|
||||
|
||||
static REGEX_PROCESS_NAME = /^Vorgang \d+$/;
|
||||
|
||||
async createCartProcess() {
|
||||
const processes = await this._app.getProcesses$('customer').pipe(first()).toPromise();
|
||||
|
||||
const count = processes.filter((x) => x.type === 'cart' && x.name.startsWith('Vorgang ')).length;
|
||||
const processIds = processes.filter((x) => ShellProcessBarComponent.REGEX_PROCESS_NAME.test(x.name)).map((x) => +x.name.split(' ')[1]);
|
||||
|
||||
const maxId = processIds.length > 0 ? Math.max(...processIds) : 0;
|
||||
|
||||
const process: ApplicationProcess = {
|
||||
id: Date.now(),
|
||||
type: 'cart',
|
||||
name: `Vorgang ${count + 1}`,
|
||||
name: `Vorgang ${maxId + 1}`,
|
||||
section: 'customer',
|
||||
closeable: true,
|
||||
};
|
||||
|
||||
@@ -1,33 +1,52 @@
|
||||
<header class="bg-surface text-surface-content h-[4.625rem] flex flex-row items-center justify-start px-[0.938rem]">
|
||||
<button (click)="toggleSideMenu()" class="btn btn-icon mr-4 desktop-small:hidden">
|
||||
<button (click)="toggleSideMenu()" class="btn btn-icon mr-4 desktop-small:hidden shell-top-bar__side-menu">
|
||||
<shared-icon [icon]="menuIcon$ | async"></shared-icon>
|
||||
</button>
|
||||
<span class="text-[1.625rem] leading-[2.25rem] font-bold" *ngIf="title$ | async; let title">
|
||||
<span class="text-[1.625rem] leading-[2.25rem] font-bold shell-top-bar__title" *ngIf="title$ | async; let title">
|
||||
{{ title }}
|
||||
</span>
|
||||
<div class="grow"></div>
|
||||
|
||||
<div class="mr-8 flex flex-row items-end">
|
||||
<button type="button" class="btn btn-icon btn-white" [disabled]="canNotDecreaseFontSize$ | async" (click)="decreaseFontSize()">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon btn-white shell-top-bar__decrease-font-size"
|
||||
[disabled]="canNotDecreaseFontSize$ | async"
|
||||
(click)="decreaseFontSize()"
|
||||
>
|
||||
<shared-icon icon="text-decrease" [size]="18"></shared-icon>
|
||||
</button>
|
||||
<button type="button" class="btn btn-icon btn-white" [disabled]="canNotIncreaseFontSize$ | async" (click)="increaseFontSize()">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-icon btn-white shell-top-bar__increase-font-size"
|
||||
[disabled]="canNotIncreaseFontSize$ | async"
|
||||
(click)="increaseFontSize()"
|
||||
>
|
||||
<shared-icon icon="text-increase" [size]="26"></shared-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<a class="btn btn-white" [routerLink]="['/kunde/dashboard']"> <shared-icon icon="dashboard"></shared-icon> Dashboard </a>
|
||||
<a class="btn btn-white shell-top-bar__dashboard" [routerLink]="['/kunde/dashboard']">
|
||||
<shared-icon icon="dashboard"></shared-icon> Dashboard
|
||||
</a>
|
||||
|
||||
<button type="button" class="btn btn-white" (click)="logout()">
|
||||
<button type="button" class="btn btn-white shell-top-bar__logout" (click)="logout()">
|
||||
<shared-icon icon="logout"></shared-icon>
|
||||
<strong>{{ branchKey$ | async }}</strong>
|
||||
</button>
|
||||
|
||||
<div class="w-4"></div>
|
||||
|
||||
<button class="btn btn-icon btn-light relative" [disabled]="(notificationCount$ | async) === 0" (click)="openNotifications()">
|
||||
<button
|
||||
class="btn btn-icon btn-light relative shell-top-bar__notifications"
|
||||
[disabled]="(notificationCount$ | async) === 0"
|
||||
(click)="openNotifications()"
|
||||
>
|
||||
<shared-icon icon="notifications"></shared-icon>
|
||||
<div class="grid absolute -top-2 -right-2 bg-brand text-white rounded-full w-6 h-6" *ngIf="notificationCount$ | async; let count">
|
||||
<div
|
||||
class="grid absolute -top-2 -right-2 bg-brand text-white rounded-full w-6 h-6 shell-top-bar__notifications-count"
|
||||
*ngIf="notificationCount$ | async; let count"
|
||||
>
|
||||
<span class="place-self-center text-xs font-bold">{{ count }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@@ -48,7 +48,6 @@ export class UiAutocompleteComponent implements AfterContentInit, OnDestroy {
|
||||
this.subscriptions.add(
|
||||
this.items.changes.subscribe(() => {
|
||||
this.registerItemOnClick();
|
||||
this.activateFirstItem();
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -80,12 +79,6 @@ export class UiAutocompleteComponent implements AfterContentInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
activateFirstItem() {
|
||||
if (this.items.length === 1) {
|
||||
this.listKeyManager.setFirstItemActive();
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
this.opend = true;
|
||||
this.cdr.markForCheck();
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
Directive,
|
||||
ElementRef,
|
||||
EmbeddedViewRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
Input,
|
||||
OnChanges,
|
||||
@@ -13,10 +12,12 @@ import {
|
||||
OnInit,
|
||||
SimpleChanges,
|
||||
ViewContainerRef,
|
||||
Inject,
|
||||
} from '@angular/core';
|
||||
import { asapScheduler, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
import { asapScheduler, Subject, fromEvent, Subscription } from 'rxjs';
|
||||
import { take, takeUntil } from 'rxjs/operators';
|
||||
import { UiOverlayTrigger } from './overlay-trigger';
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
|
||||
@Directive({ selector: '[uiOverlayTrigger]', exportAs: 'uiOverlayTrigger' })
|
||||
export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
|
||||
@@ -24,7 +25,7 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
|
||||
component: UiOverlayTrigger;
|
||||
|
||||
@Input()
|
||||
triggerOn: 'click' | 'hover' | 'init' = 'click';
|
||||
triggerOn: 'click' | 'hover' | 'init' | 'none' = 'click';
|
||||
|
||||
@Input()
|
||||
overlayTriggerDisabled: boolean;
|
||||
@@ -32,6 +33,7 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
|
||||
private overlayRef: OverlayRef;
|
||||
private viewRef: EmbeddedViewRef<any>;
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
private _clickListenerSub: Subscription;
|
||||
|
||||
get opened() {
|
||||
return !!this.viewRef;
|
||||
@@ -42,7 +44,8 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
|
||||
private viewContainerRef: ViewContainerRef,
|
||||
private elementRef: ElementRef,
|
||||
private overlay: Overlay,
|
||||
private cdr: ChangeDetectorRef
|
||||
private cdr: ChangeDetectorRef,
|
||||
@Inject(DOCUMENT) private _document: Document
|
||||
) {}
|
||||
|
||||
ngOnChanges({ position }: SimpleChanges): void {
|
||||
@@ -107,6 +110,9 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
|
||||
this.updatePositionStrategy();
|
||||
|
||||
this.viewRef = this.overlayRef.attach(dropdownPortal);
|
||||
|
||||
this.registerCloseOnClickListener();
|
||||
|
||||
this.component.close = () => this.close();
|
||||
|
||||
this.cdr.markForCheck();
|
||||
@@ -116,6 +122,7 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
|
||||
this.viewRef?.destroy();
|
||||
this.overlayRef.detach();
|
||||
delete this.viewRef;
|
||||
this._clickListenerSub.unsubscribe();
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
@@ -130,6 +137,18 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
|
||||
.subscribe(() => this.close());
|
||||
}
|
||||
|
||||
registerCloseOnClickListener() {
|
||||
asapScheduler.schedule(() => {
|
||||
this._clickListenerSub = fromEvent(this._document.body, 'click')
|
||||
.pipe(take(1))
|
||||
.subscribe((event) => {
|
||||
if (this.viewRef && !this.overlayRef?.hostElement?.contains(event.target as HTMLElement)) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}, 1);
|
||||
}
|
||||
|
||||
updatePositionStrategy() {
|
||||
this.overlayRef.updatePositionStrategy(this.getPositionStrategy());
|
||||
}
|
||||
@@ -169,11 +188,4 @@ export class UiOverlayTriggerDirective implements OnInit, OnDestroy, OnChanges {
|
||||
updatePosition() {
|
||||
this.overlayRef?.updatePositionStrategy(this.getPositionStrategy());
|
||||
}
|
||||
|
||||
@HostListener('document:click', ['$event'])
|
||||
documentClick(event: MouseEvent) {
|
||||
if (this.viewRef && !this.overlayRef?.hostElement?.contains(event.target as HTMLElement)) {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,7 @@
|
||||
<div class="hr"></div>
|
||||
<ui-datepicker-body></ui-datepicker-body>
|
||||
<div class="text-center mb-px-10">
|
||||
<button
|
||||
*ngIf="!content"
|
||||
class="rounded-full font-bold text-white bg-brand py-px-15 px-px-25"
|
||||
(click)="save.emit(selectedDate); close()"
|
||||
>
|
||||
<button *ngIf="!content" class="rounded-full font-bold text-white bg-brand py-px-15 px-px-25" (click)="onSave()">
|
||||
<ng-container>{{ saveLabel }}</ng-container>
|
||||
</button>
|
||||
<ng-content></ng-content>
|
||||
|
||||
@@ -9,12 +9,14 @@ import {
|
||||
ViewChild,
|
||||
TemplateRef,
|
||||
ContentChild,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import { Datepicker } from './datepicker';
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { DateAdapter, UiOverlayTrigger } from '@ui/common';
|
||||
import { DatepickerPositionX, DatepickerPositionY } from './datepicker-positions';
|
||||
import { isDate } from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'ui-datepicker',
|
||||
@@ -59,7 +61,7 @@ export class UiDatepickerComponent extends Datepicker implements UiOverlayTrigge
|
||||
|
||||
onTouched = () => {};
|
||||
|
||||
constructor(dateAdapter: DateAdapter) {
|
||||
constructor(dateAdapter: DateAdapter, private _cdr: ChangeDetectorRef) {
|
||||
super(dateAdapter);
|
||||
const sub = this.selectedChange.subscribe((date) => {
|
||||
this.onChange(date);
|
||||
@@ -75,8 +77,18 @@ export class UiDatepickerComponent extends Datepicker implements UiOverlayTrigge
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
writeValue(obj: Date): void {
|
||||
this.setSelected(obj, { emit: false });
|
||||
writeValue(obj: Date | string): void {
|
||||
let date = undefined;
|
||||
|
||||
if (obj) {
|
||||
date = new Date(obj);
|
||||
}
|
||||
|
||||
this.setSelected(date, { emit: false });
|
||||
|
||||
this.setDisplayed(date ?? new Date(), { emit: false });
|
||||
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
registerOnChange(fn: any): void {
|
||||
@@ -88,4 +100,11 @@ export class UiDatepickerComponent extends Datepicker implements UiOverlayTrigge
|
||||
}
|
||||
|
||||
setDisabledState?(isDisabled: boolean): void {}
|
||||
|
||||
onSave() {
|
||||
this.save.emit(this.selectedDate);
|
||||
this.onChange(this.selectedDate);
|
||||
this.onTouched();
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventEmitter, Input, Output, Directive } from '@angular/core';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
interface SetPropertyOptions {
|
||||
emit: boolean;
|
||||
@@ -26,6 +27,7 @@ export abstract class Datepicker {
|
||||
return this.displayedSubject.value;
|
||||
}
|
||||
set displayed(val: Date) {
|
||||
console.log('setDisplayed', val);
|
||||
this.setDisplayed(val, { emit: false });
|
||||
}
|
||||
@Output()
|
||||
@@ -65,7 +67,7 @@ export abstract class Datepicker {
|
||||
return this.selectedSubject.asObservable();
|
||||
}
|
||||
get displayed$() {
|
||||
return this.displayedSubject.asObservable();
|
||||
return this.displayedSubject.asObservable()?.pipe(map((date) => date ?? this.dateAdapter.today()));
|
||||
}
|
||||
get min$() {
|
||||
return this.minSubject.asObservable();
|
||||
|
||||
@@ -82,6 +82,9 @@ export class UiSearchboxNextComponent extends UiFormControlDirective<any>
|
||||
@Input()
|
||||
hint: string = '';
|
||||
|
||||
@Output()
|
||||
hintCleared = new EventEmitter<void>();
|
||||
|
||||
@Input()
|
||||
autocompleteValueSelector: (item: any) => string = (item: any) => item;
|
||||
|
||||
@@ -196,6 +199,7 @@ export class UiSearchboxNextComponent extends UiFormControlDirective<any>
|
||||
clearHint() {
|
||||
this.hint = '';
|
||||
this.focused.emit(true);
|
||||
this.hintCleared.emit();
|
||||
this.cdr.markForCheck();
|
||||
}
|
||||
|
||||
|
||||
21172
package-lock.json
generated
21172
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -86,6 +86,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"ng2-pdf-viewer": "^9.1.5",
|
||||
"parse-duration": "^1.1.0",
|
||||
"rxjs": "^6.6.7",
|
||||
"scandit-sdk": "^5.13.2",
|
||||
"socket.io": "^4.5.4",
|
||||
|
||||
@@ -7,7 +7,7 @@ module.exports = plugin(function ({ addComponents, theme, addBase, matchUtilitie
|
||||
'--menu-content': theme('colors.components.menu.content'),
|
||||
'--menu-item-height': theme('spacing.12'),
|
||||
'--menu-item-padding': `${theme('spacing.1')} ${theme('spacing.3')}`,
|
||||
'--menu-border-radius': theme('borderRadius.menu'),
|
||||
'--menu-border-radius': theme('borderRadius.DEFAULT'),
|
||||
'--menu-item-hover-background': theme('colors.components.menu.hover.DEFAULT'),
|
||||
'--menu-item-hover-content': theme('colors.components.menu.hover.content'),
|
||||
'--menu-item-hover-border': theme('colors.components.menu.hover.border'),
|
||||
@@ -29,7 +29,7 @@ module.exports = plugin(function ({ addComponents, theme, addBase, matchUtilitie
|
||||
display: 'inline-flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'left',
|
||||
height: 'var(--menu-item-height)',
|
||||
padding: 'var(--menu-item-padding)',
|
||||
backgroundColor: 'var(--menu-background)',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
const plugin = require('tailwindcss/plugin');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./apps/**/*.{html,ts}'],
|
||||
@@ -202,5 +204,8 @@ module.exports = {
|
||||
require('./tailwind-plugins/select-bullet.plugin.js'),
|
||||
require('./tailwind-plugins/section.plugin.js'),
|
||||
require('./tailwind-plugins/typography.plugin.js'),
|
||||
plugin(({ addVariant }) => {
|
||||
addVariant('open', '&.open');
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user