Compare commits

...

42 Commits

Author SHA1 Message Date
Lorenz Hilpert
967df6316b Merge branch 'release/3.0' into feature/3968-Artikeldetails-Mehrwertsteuer 2023-08-31 14:11:06 +02:00
Lorenz Hilpert
b440ddbe82 Merge branch 'hotfix/4270-4269-Archiv-Artikel'
(cherry picked from commit f4df6e8799)
2023-08-30 14:38:54 +02:00
Nino
878bf44d0b #3968 Article Search Details display vat and if priceMaintained true 2023-08-28 16:51:52 +02:00
Lorenz Hilpert
d6e0d92132 (cherry picked from commit d09b5b1ce7) 2023-08-24 20:18:19 +02:00
Lorenz Hilpert
da6489eb7a Merge tag '4266-Archivartikel' into develop
Hotfix 4266 Archivartikel Preisauswahl
2023-08-24 20:06:36 +02:00
Lorenz Hilpert
819827cc4c Merge branch 'hotfix/4266-Archivartikel' 2023-08-24 20:04:40 +02:00
Lorenz Hilpert
d09b5b1ce7 #4266 Archivartikel nach Preis-Eingabe Button ausgegraut 2023-08-24 20:03:59 +02:00
Lorenz Hilpert
cc03ef4f9c #4264 Fix Ola Refresh Calls 2023-08-24 12:58:53 +02:00
Lorenz Hilpert
b4dbd8889d Merge branch 'develop' of https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend into develop 2023-08-24 12:12:50 +02:00
Lorenz Hilpert
483faad86a #4264 - Bestellen-Button ausgegraut 2023-08-24 11:57:47 +02:00
Michael Auer
0dbc745ed0 Merge tag '2.3' into develop
# Conflicts:
#	apps/isa-app/src/app/shell/shell.component.html
2023-08-24 11:56:11 +02:00
Michael Auer
180e93a7da Merge branch 'release/2.3' 2023-08-24 11:50:39 +02:00
Lorenz Hilpert
5c6f416391 #4263 Versand - Fehler vor Kundensuche 2023-08-23 14:56:57 +02:00
Lorenz Hilpert
d97b6afac8 #4205 Reihensuche 2023-08-23 14:31:55 +02:00
Lorenz Hilpert
771816f3af #4185 OLA Warenkorb - 500 Fix 2023-08-22 13:47:22 +02:00
Lorenz Hilpert
0626538aea #4261 Fehler bei Artikel aus Liste in Warenkorb - Weiter Button War Nicht Aktiv 2023-08-21 15:33:35 +02:00
Lorenz Hilpert
a1ad4e4a05 #4261 Fehler bei Artikel aus Liste in Warenkorb 2023-08-21 15:12:22 +02:00
Lorenz Hilpert
6df48eb555 #4255 Neue Filteroption - Erscheinungsdatum 2023-08-21 14:18:40 +02:00
Lorenz Hilpert
27ab4526e2 Logs entfernt und kleinere Änderungen rückgängig gemacht 2023-08-18 12:48:09 +02:00
Lorenz Hilpert
9a24b34fbc #4255 Verbesserung des Datumsinputs 2023-08-18 12:45:27 +02:00
Lorenz Hilpert
d01e01534b #4185 Bugfix - Bestellabschluss 2023-08-17 17:01:33 +02:00
Lorenz Hilpert
5bca1f2a81 Bugfix - zu viele aurufe bei ola 2023-08-16 15:26:08 +02:00
Lorenz Hilpert
807b300885 #4185 OLA im Warenkorb 2023-08-16 14:54:14 +02:00
Lorenz Hilpert
b16ffa4352 Merge branch 'develop' into release/3.0 2023-08-11 15:34:45 +02:00
Lorenz Hilpert
da79d04ef4 Bugfix Erscheinungsdatum 2023-08-11 15:34:09 +02:00
Lorenz Hilpert
cf619df576 Merge branch 'release/3.0' into develop 2023-08-11 10:29:47 +02:00
Lorenz Hilpert
8054c96315 #3376 Erscheinungstermin Filter Option Mit Input Box 2023-08-11 10:28:14 +02:00
Nino Righi
2363f424f5 Merged PR 1617: #3360 Show Branch Tooltip
#3360 Show Branch Tooltip
2023-08-11 08:11:16 +00:00
Lorenz Hilpert
6ab1ea2e70 #4254 Bestellungen ohne Konto werden nicht als Kunde erkannt 2023-08-10 14:03:18 +02:00
Lorenz Hilpert
c9ce7d6762 #4253 Kundensuche - Typo 2023-08-09 17:53:58 +02:00
Lorenz Hilpert
6ee1b0a7f8 #4250 Vorgänge zählen nicht hoch 2023-08-09 16:50:54 +02:00
Lorenz Hilpert
d881920312 Merge branch 'develop' into release/3.0 2023-08-07 07:07:36 +02:00
Lorenz Hilpert
516465db37 #4246 UI Searchbox Hint Erneut anzeigen
(cherry picked from commit 9671683a93)
2023-08-06 05:11:40 +02:00
Lorenz Hilpert
08e95cec55 #4245 Wannernummer-Prüfung - Leerzeichen entfernen
(cherry picked from commit 15c50779b4)
2023-08-06 05:10:46 +02:00
Lorenz Hilpert
9671683a93 #4246 UI Searchbox Hint Erneut anzeigen 2023-08-04 15:56:51 +02:00
Lorenz Hilpert
d909d6e804 #4236 Kulturpass - Artikel ohne Preisbindung erhalten günstigeren Preis
(cherry picked from commit 1d865c47d7)
2023-08-03 17:06:46 +02:00
Lorenz Hilpert
15c50779b4 #4245 Wannernummer-Prüfung - Leerzeichen entfernen 2023-08-03 17:05:45 +02:00
Lorenz Hilpert
1d865c47d7 #4236 Kulturpass - Artikel ohne Preisbindung erhalten günstigeren Preis 2023-08-03 13:57:09 +02:00
Lorenz Hilpert
5bdfec7c3f #4222 Packstückprüfung aktiviert 2023-08-02 10:55:54 +02:00
Nino Righi
6b0beba1d9 Merged PR 1616: #3378 SSC Sync PDP and Search Result List
#3378 SSC Sync PDP and Search Result List
2023-08-01 16:16:57 +00:00
Lorenz Hilpert
9d886cd33f Merge branch 'develop' into release/3.0 2023-07-31 18:31:30 +02:00
Michael Auer
6bc265a358 Merge branch 'release/2.3' 2023-07-11 12:20:14 +02:00
79 changed files with 1736 additions and 548 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,
@@ -479,6 +484,10 @@ export class DomainAvailabilityService {
};
}
private _priceIsEmpty(price: PriceDTO) {
return isEmpty(price?.value) || isEmpty(price?.vat);
}
private _mapToTakeAwayAvailability({
response,
supplier,
@@ -499,7 +508,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

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((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);
};
});
}
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,11 @@
"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"
}
],

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,6 @@
<ng-container *ngFor="let line of lines">
<ng-container [ngSwitch]="line | lineType">
<page-article-details-text-link *ngSwitchCase="'reihe'" [route]="line | reiheRoute"> {{ line }} <br /> </page-article-details-text-link>
<ng-container *ngSwitchDefault> {{ line }} <br /> </ng-container>
</ng-container>
</ng-container>

View File

@@ -0,0 +1,63 @@
import { Component, ChangeDetectionStrategy, Input, Renderer2, ElementRef, OnChanges, SimpleChanges, HostBinding } 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';
const REIHE_REGEX = /^Reihe:\s*"(.+)\"$/m;
@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(private elementRef: ElementRef, private renderer: Renderer2) {}
// ngOnChanges(changes: SimpleChanges): void {
// if (changes.text) {
// this.renderText();
// }
// }
// renderText() {
// console.log(this.getReihe());
// }
// getReihe() {
// REIHE_REGEX.exec(this.text?.value)?.forEach((match, groupIndex) => {
// if (groupIndex === 0) return;
// return match;
// });
// }
// @HostBinding('innerHTML')
// get htmlContent() {
// let content = this.text?.value;
// REIHE_REGEX.exec(content)?.forEach((match, groupIndex) => {
// if (groupIndex === 0) return;
// const aElement: HTMLAnchorElement = this.renderer.createElement('a');
// const text = this.renderer.createText(match);
// this.renderer.appendChild(aElement, text);
// this.renderer.setAttribute(aElement, 'href', `/search?query=${match}`);
// content = content.replace(match, aElement.outerHTML);
// });
// return content;
// }
}

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:\s*"(.+)\"$/g;
const reihe = REIHE_REGEX.exec(value)?.[1];
if (!reihe) {
this.result = null;
return;
}
const main_qs = reihe.split('/')[0];
const path = this.navigation.getArticleSearchResultsPath(processId);
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

@@ -104,27 +104,15 @@
</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 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,59 @@ 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.item$,
this.store.takeAwayAvailability$,
this.store.deliveryAvailability$,
this.store.deliveryDigAvailability$,
this.store.deliveryB2BAvailability$,
]).pipe(
map(([item, takeAway, delivery, deliveryDig, deliveryB2B]) =>
[item, takeAway, delivery, deliveryDig, deliveryB2B].some((i) => i?.priceMaintained)
)
);
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 +186,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 +269,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;
@@ -104,15 +105,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 '';
})
@@ -126,6 +120,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,
@@ -135,7 +140,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,
@@ -157,6 +163,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>
<ng-container *ngIf="routerEvents$ | async">

View File

@@ -6,9 +6,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, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ActionsSubject } from '@ngrx/store';
import { DomainAvailabilityService } from '@domain/availability';
@Component({
selector: 'page-catalog',
@@ -23,6 +26,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`;
}
@@ -45,23 +50,52 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
showRightOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'right')));
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 _activatedRoute: ActivatedRoute,
private _router: Router
private _router: Router,
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

@@ -6,9 +6,20 @@ import { ArticleDetailsModule } from './article-details/article-details.module';
import { ArticleSearchModule } from './article-search/article-search.module';
import { PageCatalogRoutingModule } from './page-catalog-routing.module';
import { PageCatalogComponent } from './page-catalog.component';
import { UiCommonModule } from '@ui/common';
import { UiTooltipModule } from '@ui/tooltip';
@NgModule({
imports: [CommonModule, PageCatalogRoutingModule, ArticleSearchModule, ArticleDetailsModule, BreadcrumbModule, BranchSelectorComponent],
imports: [
CommonModule,
PageCatalogRoutingModule,
ArticleSearchModule,
ArticleDetailsModule,
BreadcrumbModule,
BranchSelectorComponent,
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

@@ -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,4 @@
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy, ViewChildren, QueryList } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
@@ -6,8 +6,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 } from 'rxjs';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
@@ -17,6 +17,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',
@@ -25,8 +27,12 @@ import { CheckoutReviewStore } from './checkout-review.store';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReviewComponent implements OnInit, OnDestroy {
checkingOla$ = new BehaviorSubject<boolean>(false);
payer$ = this._store.payer$;
buyer$ = this._store.buyer$;
shoppingCart$ = this._store.shoppingCart$;
fetching$ = this._store.fetching$;
@@ -120,12 +126,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 +153,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);
}
@@ -157,6 +168,9 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
private _onDestroy$ = new Subject<void>();
@ViewChildren(ShoppingCartItemComponent)
private _shoppingCartItems: QueryList<ShoppingCartItemComponent>;
constructor(
private domainCheckoutService: DomainCheckoutService,
public applicationService: ApplicationService,
@@ -171,7 +185,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 +196,12 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
await this.removeBreadcrumbs();
await this.updateBreadcrumb();
this.registerOlaCechk();
window['Checkout'] = {
refreshAvailabilities: this.refreshAvailabilities.bind(this),
};
}
ngOnDestroy(): void {
@@ -189,6 +210,32 @@ export class CheckoutReviewComponent implements OnInit, OnDestroy {
this._onDestroy$.complete();
}
registerOlaCechk() {
this.applicationService.activatedProcessId$
.pipe(
takeUntil(this._onDestroy$),
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 +521,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

@@ -165,7 +165,7 @@
[disabled]="showLoader$ | async"
>
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">
Weiter zur Artielsuche
Weiter zur Artikelsuche
</shared-loader>
</button>

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,9 @@
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';
@Component({
selector: 'shared-input-option-date-range',
@@ -9,11 +12,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 +50,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 +111,55 @@ export class FilterInputOptionDateRangeComponent {
this.optionChangeSubscription = new Subscription();
}
setStratValue(date: Date) {
if (!date) {
this.uiStartOption?.setValue(undefined);
this.formGroup.patchValue({ start: undefined });
this.startDatepicker?.setDisplayed(undefined);
this.startDatepicker?.setSelected(undefined);
return;
}
const startDate = new Date(date);
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 !
if (!date) {
this.uiStopOption?.setValue(undefined);
this.formGroup.patchValue({ stop: undefined });
this.stopDatepicker?.setDisplayed(undefined);
this.stopDatepicker?.setSelected(undefined);
return;
}
const stopDate = new Date(date);
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

@@ -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,86 @@
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 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) {
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,25 @@
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') {
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 === 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';
import { IconComponent } from '@shared/components/icon';
@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

@@ -1,5 +1,4 @@
export * from './purchase-options.helpers';
export * from './purchase-options.selectors';
export * from './purchase-options.service';
export * from './purchase-options.state';
export * from './purchase-options.store';

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,5 +1,5 @@
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 { PriceDTO } from '@swagger/checkout';
import { DEFAULT_PRICE_DTO, GIFT_CARD_MAX_PRICE, PURCHASE_OPTIONS } from '../constants';
import { isGiftCard, isItemDTO } from './purchase-options.helpers';
import { PurchaseOptionsState } from './purchase-options.state';
import { ActionType, Availability, Branch, CanAdd, FetchingAvailability, Item, PurchaseOption } from './purchase-options.types';
@@ -198,31 +198,66 @@ export function getAvailabilitiesForItem(
};
}
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 getCanEditPrice(itemId: number): (state: PurchaseOptionsState) => boolean {
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 +294,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 +403,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

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

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

231
package-lock.json generated
View File

@@ -34,8 +34,9 @@
"lodash": "^4.17.21",
"moment": "^2.29.4",
"ng2-pdf-viewer": "^9.1.3",
"parse-duration": "^1.1.0",
"rxjs": "^6.6.7",
"scandit-sdk": "^5.12.1",
"scandit-sdk": "^5.13.2",
"socket.io": "^4.5.4",
"tslib": "^2.0.0",
"uglify-js": "^3.4.9",
@@ -93,19 +94,6 @@
"npm": "8.x"
}
},
"../../@paragondata/ngx-ui/dist/paragondata/ngx-ui": {
"name": "@paragondata/ngx-ui",
"version": "0.0.0-watch+1683643023778",
"extraneous": true,
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/cdk": ">= 13.0.0 < 16.0.0",
"@angular/common": ">= 13.0.0 < 16.0.0",
"@angular/core": ">= 13.0.0 < 16.0.0"
}
},
"node_modules/@ampproject/remapping": {
"version": "2.2.0",
"license": "Apache-2.0",
@@ -2186,16 +2174,22 @@
}
},
"node_modules/@babel/runtime-corejs2": {
"version": "7.20.7",
"license": "MIT",
"version": "7.22.11",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.22.11.tgz",
"integrity": "sha512-6z+Y7otDbBpPbg+eXnYVnrR7J3bq5Xp31QiGf7bJzbBOYUXLDGXnCdmxzH3xGxczTvaSXV/oeXFV4PNq3f64Sg==",
"dependencies": {
"core-js": "^2.6.12",
"regenerator-runtime": "^0.13.11"
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/runtime-corejs2/node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
},
"node_modules/@babel/template": {
"version": "7.18.10",
"license": "MIT",
@@ -2273,9 +2267,8 @@
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
@@ -2287,9 +2280,8 @@
},
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
@@ -2724,8 +2716,6 @@
},
"node_modules/@paragondata/ngx-ui": {
"version": "12.0.0-beta.91",
"resolved": "https://npm.pkg.github.com/download/@paragondata/ngx-ui/12.0.0-beta.91/18864a278241d4874fc1ab1e41b8d43330721ffb",
"integrity": "sha512-Ran2ZOw7tNhSJwaBJPDUwGwxfC/bpaVrUum+Hn7Q/RfIuC3PUmFSJkBaZdAL4eyOASIWD6ZrmNxb2qcVCo7ctA==",
"dependencies": {
"tslib": "^2.3.0"
},
@@ -2832,7 +2822,6 @@
},
"node_modules/@stylelint/postcss-markdown": {
"version": "0.36.2",
"deprecated": "Use the original unforked package instead: postcss-markdown",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2936,33 +2925,29 @@
},
"node_modules/@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@tsconfig/node16": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
@@ -3116,10 +3101,8 @@
},
"node_modules/@types/moment": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz",
"integrity": "sha512-DyuyYGpV6r+4Z1bUznLi/Y7HpGn4iQ4IVcGn8zrr1P4KotKLdH0sbK1TFR6RGyX6B+G8u83wCzL+bpawKU/hdQ==",
"deprecated": "This is a stub types definition for Moment (https://github.com/moment/moment). Moment provides its own type definitions, so you don't need @types/moment installed!",
"dev": true,
"license": "MIT",
"dependencies": {
"moment": "*"
}
@@ -3449,9 +3432,8 @@
},
"node_modules/adm-zip": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
}
@@ -4779,7 +4761,6 @@
},
"node_modules/core-js": {
"version": "2.6.12",
"deprecated": "core-js@<3.4 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Please, upgrade your dependencies to the actual version of core-js.",
"hasInstallScript": true,
"license": "MIT"
},
@@ -4828,9 +4809,8 @@
},
"node_modules/create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
@@ -6903,7 +6883,6 @@
},
"node_modules/har-validator": {
"version": "5.1.5",
"deprecated": "this library is no longer supported",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7211,9 +7190,8 @@
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/http-deceiver": {
"version": "1.2.7",
@@ -9050,8 +9028,6 @@
},
"node_modules/karma/node_modules/ua-parser-js": {
"version": "0.7.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==",
"dev": true,
"funding": [
{
@@ -9063,6 +9039,7 @@
"url": "https://paypal.me/faisalman"
}
],
"license": "MIT",
"engines": {
"node": "*"
}
@@ -11302,6 +11279,10 @@
"node": ">=6"
}
},
"node_modules/parse-duration": {
"version": "1.1.0",
"license": "MIT"
},
"node_modules/parse-entities": {
"version": "2.0.0",
"dev": true,
@@ -12747,6 +12728,7 @@
},
"node_modules/regenerator-runtime": {
"version": "0.13.11",
"dev": true,
"license": "MIT"
},
"node_modules/regenerator-transform": {
@@ -13011,7 +12993,6 @@
},
"node_modules/request": {
"version": "2.88.2",
"deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -13062,7 +13043,6 @@
},
"node_modules/request/node_modules/uuid": {
"version": "3.4.0",
"deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.",
"dev": true,
"license": "MIT",
"bin": {
@@ -13452,10 +13432,11 @@
"license": "ISC"
},
"node_modules/scandit-sdk": {
"version": "5.12.2",
"license": "SEE LICENSE IN LICENSE",
"version": "5.13.3",
"resolved": "https://registry.npmjs.org/scandit-sdk/-/scandit-sdk-5.13.3.tgz",
"integrity": "sha512-lETa3+ZmOXWytmAzb+PtenvU878UvOvsCVP4RhmF/HkyzjhobS/OOsluc0gaWh7U7d+gJIrnlheY8pTehARtqQ==",
"dependencies": {
"@babel/runtime-corejs2": "^7.20.7",
"@babel/runtime-corejs2": "^7.20.13",
"@juggle/resize-observer": "^3.4.0",
"csstype": "^3.1.1",
"eventemitter3": "^5.0.0",
@@ -13463,15 +13444,16 @@
"js-cookie": "^3.0.1",
"objectFitPolyfill": "^2.3.5",
"tslib": "^2.4.1",
"ua-parser-js": "^1.0.32"
"ua-parser-js": "^1.0.33"
},
"engines": {
"node": ">=10.18"
}
},
"node_modules/scandit-sdk/node_modules/eventemitter3": {
"version": "5.0.0",
"license": "MIT"
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
},
"node_modules/schema-utils": {
"version": "4.0.0",
@@ -15195,7 +15177,6 @@
},
"node_modules/tslint": {
"version": "6.1.3",
"deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -15398,8 +15379,6 @@
},
"node_modules/ua-parser-js": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
"funding": [
{
"type": "opencollective",
@@ -15410,6 +15389,7 @@
"url": "https://paypal.me/faisalman"
}
],
"license": "MIT",
"engines": {
"node": "*"
}
@@ -15637,9 +15617,8 @@
},
"node_modules/v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"license": "MIT",
"optional": true,
"peer": true
},
@@ -15749,8 +15728,7 @@
},
"node_modules/web-animations-js": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/web-animations-js/-/web-animations-js-2.3.2.tgz",
"integrity": "sha512-TOMFWtQdxzjWp8qx4DAraTWTsdhxVSiWa6NkPFSaPtZ1diKUxTn4yTix73A1euG1WbSOMMPcY51cnjTIHrGtDA=="
"license": "Apache-2.0"
},
"node_modules/web-streams-polyfill": {
"version": "3.2.1",
@@ -15773,9 +15751,8 @@
},
"node_modules/webdriver-manager": {
"version": "12.1.9",
"resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz",
"integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"adm-zip": "^0.5.2",
"chalk": "^1.1.1",
@@ -15798,24 +15775,27 @@
},
"node_modules/webdriver-manager/node_modules/ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webdriver-manager/node_modules/ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/webdriver-manager/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -15823,8 +15803,9 @@
},
"node_modules/webdriver-manager/node_modules/chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^2.2.1",
"escape-string-regexp": "^1.0.2",
@@ -15838,8 +15819,9 @@
},
"node_modules/webdriver-manager/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
@@ -15857,13 +15839,15 @@
},
"node_modules/webdriver-manager/node_modules/ini": {
"version": "1.3.8",
"dev": true,
"license": "ISC"
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true
},
"node_modules/webdriver-manager/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
@@ -15873,8 +15857,9 @@
},
"node_modules/webdriver-manager/node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"glob": "^7.1.3"
},
@@ -15883,17 +15868,19 @@
}
},
"node_modules/webdriver-manager/node_modules/semver": {
"version": "5.7.1",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/webdriver-manager/node_modules/strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^2.0.0"
},
@@ -15903,8 +15890,9 @@
},
"node_modules/webdriver-manager/node_modules/supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.0"
}
@@ -17702,10 +17690,19 @@
}
},
"@babel/runtime-corejs2": {
"version": "7.20.7",
"version": "7.22.11",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.22.11.tgz",
"integrity": "sha512-6z+Y7otDbBpPbg+eXnYVnrR7J3bq5Xp31QiGf7bJzbBOYUXLDGXnCdmxzH3xGxczTvaSXV/oeXFV4PNq3f64Sg==",
"requires": {
"core-js": "^2.6.12",
"regenerator-runtime": "^0.13.11"
"regenerator-runtime": "^0.14.0"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
}
}
},
"@babel/template": {
@@ -17763,8 +17760,6 @@
},
"@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
"dev": true,
"optional": true,
"peer": true,
@@ -17774,8 +17769,6 @@
"dependencies": {
"@jridgewell/trace-mapping": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
"dev": true,
"optional": true,
"peer": true,
@@ -18048,8 +18041,6 @@
},
"@paragondata/ngx-ui": {
"version": "12.0.0-beta.91",
"resolved": "https://npm.pkg.github.com/download/@paragondata/ngx-ui/12.0.0-beta.91/18864a278241d4874fc1ab1e41b8d43330721ffb",
"integrity": "sha512-Ran2ZOw7tNhSJwaBJPDUwGwxfC/bpaVrUum+Hn7Q/RfIuC3PUmFSJkBaZdAL4eyOASIWD6ZrmNxb2qcVCo7ctA==",
"requires": {
"tslib": "^2.3.0"
}
@@ -18168,32 +18159,24 @@
},
"@tsconfig/node10": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
"integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
"dev": true,
"optional": true,
"peer": true
},
"@tsconfig/node12": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
"dev": true,
"optional": true,
"peer": true
},
"@tsconfig/node14": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
"dev": true,
"optional": true,
"peer": true
},
"@tsconfig/node16": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz",
"integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==",
"dev": true,
"optional": true,
"peer": true
@@ -18327,8 +18310,6 @@
},
"@types/moment": {
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@types/moment/-/moment-2.13.0.tgz",
"integrity": "sha512-DyuyYGpV6r+4Z1bUznLi/Y7HpGn4iQ4IVcGn8zrr1P4KotKLdH0sbK1TFR6RGyX6B+G8u83wCzL+bpawKU/hdQ==",
"dev": true,
"requires": {
"moment": "*"
@@ -18597,8 +18578,6 @@
},
"adm-zip": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz",
"integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==",
"dev": true
},
"agent-base": {
@@ -19468,8 +19447,6 @@
},
"create-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
"dev": true,
"optional": true,
"peer": true
@@ -21032,8 +21009,6 @@
},
"http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"dev": true
},
"http-deceiver": {
@@ -22036,8 +22011,6 @@
},
"ua-parser-js": {
"version": "0.7.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.33.tgz",
"integrity": "sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==",
"dev": true
},
"yargs": {
@@ -23611,6 +23584,9 @@
"callsites": "^3.0.0"
}
},
"parse-duration": {
"version": "1.1.0"
},
"parse-entities": {
"version": "2.0.0",
"dev": true,
@@ -24529,7 +24505,8 @@
}
},
"regenerator-runtime": {
"version": "0.13.11"
"version": "0.13.11",
"dev": true
},
"regenerator-transform": {
"version": "0.15.1",
@@ -24968,9 +24945,11 @@
"dev": true
},
"scandit-sdk": {
"version": "5.12.2",
"version": "5.13.3",
"resolved": "https://registry.npmjs.org/scandit-sdk/-/scandit-sdk-5.13.3.tgz",
"integrity": "sha512-lETa3+ZmOXWytmAzb+PtenvU878UvOvsCVP4RhmF/HkyzjhobS/OOsluc0gaWh7U7d+gJIrnlheY8pTehARtqQ==",
"requires": {
"@babel/runtime-corejs2": "^7.20.7",
"@babel/runtime-corejs2": "^7.20.13",
"@juggle/resize-observer": "^3.4.0",
"csstype": "^3.1.1",
"eventemitter3": "^5.0.0",
@@ -24978,11 +24957,13 @@
"js-cookie": "^3.0.1",
"objectFitPolyfill": "^2.3.5",
"tslib": "^2.4.1",
"ua-parser-js": "^1.0.32"
"ua-parser-js": "^1.0.33"
},
"dependencies": {
"eventemitter3": {
"version": "5.0.0"
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
}
}
},
@@ -26240,9 +26221,7 @@
"version": "4.8.4"
},
"ua-parser-js": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ=="
"version": "1.0.33"
},
"uglify-js": {
"version": "3.17.4"
@@ -26373,8 +26352,6 @@
},
"v8-compile-cache-lib": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
"dev": true,
"optional": true,
"peer": true
@@ -26451,9 +26428,7 @@
}
},
"web-animations-js": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/web-animations-js/-/web-animations-js-2.3.2.tgz",
"integrity": "sha512-TOMFWtQdxzjWp8qx4DAraTWTsdhxVSiWa6NkPFSaPtZ1diKUxTn4yTix73A1euG1WbSOMMPcY51cnjTIHrGtDA=="
"version": "2.3.2"
},
"web-streams-polyfill": {
"version": "3.2.1"
@@ -26468,8 +26443,6 @@
},
"webdriver-manager": {
"version": "12.1.9",
"resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.9.tgz",
"integrity": "sha512-Yl113uKm8z4m/KMUVWHq1Sjtla2uxEBtx2Ue3AmIlnlPAKloDn/Lvmy6pqWCUersVISpdMeVpAaGbNnvMuT2LQ==",
"dev": true,
"requires": {
"adm-zip": "^0.5.2",
@@ -26487,14 +26460,20 @@
"dependencies": {
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==",
"dev": true
},
"ansi-styles": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
"integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==",
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -26503,6 +26482,8 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==",
"dev": true,
"requires": {
"ansi-styles": "^2.2.1",
@@ -26514,6 +26495,8 @@
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
@@ -26526,10 +26509,14 @@
},
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
"dev": true
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
@@ -26537,17 +26524,23 @@
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dev": true,
"requires": {
"glob": "^7.1.3"
}
},
"semver": {
"version": "5.7.1",
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==",
"dev": true,
"requires": {
"ansi-regex": "^2.0.0"
@@ -26555,6 +26548,8 @@
},
"supports-color": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
"integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==",
"dev": true
}
}

View File

@@ -88,6 +88,7 @@
"lodash": "^4.17.21",
"moment": "^2.29.4",
"ng2-pdf-viewer": "^9.1.3",
"parse-duration": "^1.1.0",
"rxjs": "^6.6.7",
"scandit-sdk": "^5.13.2",
"socket.io": "^4.5.4",