Merge branch 'develop' into split-screen-demo

This commit is contained in:
Nino
2023-09-11 11:51:00 +02:00
100 changed files with 6299 additions and 17334 deletions

View File

@@ -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 {

View File

@@ -1,3 +1,4 @@
export * from './availability-by-branch-dto';
export * from './availability';
export * from './item-data';
export * from './ssc';

View File

@@ -0,0 +1,5 @@
export interface Ssc {
itemId?: number;
ssc?: string;
sscText?: string;
}

View File

@@ -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({

View File

@@ -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>;
}

View File

@@ -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 }>()
);

View File

@@ -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);
}

View File

@@ -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"
}
],

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},

View File

@@ -15,6 +15,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-integration.paragon-data.net/isa/v1"
},

View File

@@ -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",

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa.paragon-systems.de/isa/v1"
},

View File

@@ -16,6 +16,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-staging.paragon-systems.de/isa/v1"
},

View File

@@ -17,6 +17,9 @@
"@core/logger": {
"logLevel": "debug"
},
"@domain/checkout": {
"olaExpiration": "5m"
},
"@swagger/isa": {
"rootUrl": "https://isa-test.paragon-data.net/isa/v1"
},

View File

@@ -0,0 +1,3 @@
:host {
@apply text-[#0556B4];
}

View File

@@ -0,0 +1,3 @@
<a [routerLink]="route?.path" [queryParams]="route?.queryParams">
<ng-content></ng-content>
</a>

View File

@@ -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() {}
}

View File

@@ -0,0 +1,3 @@
:host {
@apply block whitespace-pre-line;
}

View File

@@ -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>

View File

@@ -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;
});
}
}

View File

@@ -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';
}
}

View File

@@ -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;
}
}

View File

@@ -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"

View File

@@ -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'

View File

@@ -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'])

View File

@@ -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],

View File

@@ -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(' - ');
})
);

View File

@@ -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,

View File

@@ -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>

View File

@@ -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' } : '';
}

View File

@@ -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>

View File

@@ -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]) => {

View File

@@ -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],

View File

@@ -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 {}

View 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`;
}
}

View File

@@ -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">

View File

@@ -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) {

View File

@@ -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],

View File

@@ -67,6 +67,7 @@
<page-special-comment
class="mb-6 mt-4"
[hasPayer]="!!(payer$ | async)"
[hasBuyer]="!!(buyer$ | async)"
[ngModel]="specialComment$ | async"
(ngModelChange)="setAgentComment($event)"
>

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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();
});
}
}

View File

@@ -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>

View File

@@ -30,6 +30,9 @@ export class SpecialCommentComponent implements ControlValueAccessor {
@Input()
hasPayer: boolean;
@Input()
hasBuyer: boolean;
@Output()
isDirtyChange = new EventEmitter<boolean>();

View File

@@ -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$;

View File

@@ -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 },
});
}

View File

@@ -0,0 +1,3 @@
:host {
@apply inline-block;
}

View File

@@ -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>

View File

@@ -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,
});
}
}

View File

@@ -0,0 +1 @@
export * from './customer-menu.component';

View File

@@ -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>

View File

@@ -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',

View File

@@ -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],

View File

@@ -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>

View File

@@ -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);
};

View File

@@ -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],
})

View File

@@ -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)">

View File

@@ -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(

View File

@@ -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">

View File

@@ -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$;

View File

@@ -11,5 +11,6 @@
[hint]="hint$ | async"
(scan)="search($event)"
[scanner]="true"
(hintCleared)="clearHint()"
></ui-searchbox>
</div>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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],
})

View File

@@ -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>

View File

@@ -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];
}

View File

@@ -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();
});

View File

@@ -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) => {};

View File

@@ -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',

View File

@@ -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);
}
}

View File

@@ -0,0 +1 @@
export * from './lib/scale-content.component';

View File

@@ -0,0 +1,6 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "src/public-api.ts"
}
}

View 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);
}
}

View File

@@ -0,0 +1 @@
export * from './date-input.directive';

View 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;

View 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;
}

View File

@@ -0,0 +1 @@
export * from './date.validator';

View File

@@ -0,0 +1,2 @@
export * from './lib/directives';
export * from './lib/validators';

View File

@@ -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);
};

View File

@@ -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">

View File

@@ -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);

View File

@@ -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

View File

@@ -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>();

View File

@@ -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,

View File

@@ -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;
}
}
}

View File

@@ -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 = {

View File

@@ -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>

View File

@@ -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,
};

View File

@@ -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>

View File

@@ -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();

View File

@@ -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();
}
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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
View File

File diff suppressed because it is too large Load Diff

View File

@@ -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",

View File

@@ -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)',

View File

@@ -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');
}),
],
};