Compare commits

...

26 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
Nino
878bf44d0b #3968 Article Search Details display vat and if priceMaintained true 2023-08-28 16:51:52 +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
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
5bdfec7c3f #4222 Packstückprüfung aktiviert 2023-08-02 10:55:54 +02:00
Michael Auer
6bc265a358 Merge branch 'release/2.3' 2023-07-11 12:20:14 +02:00
49 changed files with 1000 additions and 115 deletions

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

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

@@ -107,12 +107,12 @@
<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>
<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">
@@ -315,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

@@ -127,6 +127,18 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
})
);
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$,

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

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

@@ -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,6 +27,8 @@ 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$;
@@ -149,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);
}
@@ -159,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,
@@ -173,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() {
@@ -183,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 {
@@ -191,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,
@@ -476,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

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

@@ -6,7 +6,14 @@
[attr.data-selected]="uiStartOption?.selected"
>
<shared-input-control>
<input placeholder="TT.MM.JJJJ" sharedInputControlInput uiDateInput type="text" formControlName="start" />
<input
placeholder="TT.MM.JJJJ"
sharedInputControlInput
sharedDateInput
type="text"
formControlName="start"
(blur)="setStratValue($event.target['value'])"
/>
<shared-input-control-suffix>
<button
type="button"
@@ -27,7 +34,14 @@
[attr.data-selected]="uiStopOption?.selected"
>
<shared-input-control>
<input placeholder="TT.MM.JJJJ" sharedInputControlInput uiDateInput type="text" formControlName="stop" />
<input
placeholder="TT.MM.JJJJ"
sharedInputControlInput
sharedDateInput
type="text"
formControlName="stop"
(blur)="setStopValue($event.target['value'])"
/>
<shared-input-control-suffix>
<button
type="button"
@@ -40,25 +54,27 @@
</shared-input-control-suffix>
</shared-input-control>
</shared-form-control>
</div>
<ui-datepicker
class="dp-left"
#dpStart
yPosition="below"
xPosition="after"
[ngModel]="uiStartOption?.value"
saveLabel="Übernehmen"
(save)="uiStartOption?.setValue($event)"
>
</ui-datepicker>
<ui-datepicker
class="dp-right"
yPosition="below"
xPosition="after"
#dpStop
[ngModel]="uiStopOption?.value"
(save)="setStopValue($event)"
saveLabel="Übernehmen"
>
</ui-datepicker>
<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,8 +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 { UiValidators } from '@ui/validators';
import { DateValidator } from '@shared/forms';
import { UiDatepickerComponent } from '@ui/datepicker';
@Component({
selector: 'shared-input-option-date-range',
@@ -11,11 +12,15 @@ import { UiValidators } from '@ui/validators';
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, UiValidators.date),
stop: new FormControl<Date>(undefined, UiValidators.date),
start: new FormControl<Date>(undefined, DateValidator),
stop: new FormControl<Date>(undefined, DateValidator),
});
get startControl(): FormControl<Date> {
@@ -47,6 +52,22 @@ export class FilterInputOptionDateRangeComponent {
optionChangeSubscription: Subscription;
get startDate() {
return this.uiStartOption?.value ? new Date(this.uiStartOption?.value) : undefined;
}
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() {
@@ -55,44 +76,33 @@ export class FilterInputOptionDateRangeComponent {
this.formGroup.patchValue({ start: (this.uiStartOption.value as any) as Date });
this.optionChangeSubscription.add(
this.uiStartOption.changes.subscribe(() => {
if (new Date(this.uiStartOption.value) !== new Date(this.formGroup.get('start').value)) {
this.formGroup.patchValue({ start: (this.uiStartOption.value as any) as Date });
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();
})
);
this.optionChangeSubscription.add(
this.formGroup.get('start').valueChanges.subscribe((value) => {
if (new Date(this.uiStartOption.value) !== new Date(this.formGroup.get('start').value)) {
this.uiStartOption.setValue(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(() => {
if (new Date(this.uiStopOption.value) !== new Date(this.formGroup.get('stop').value)) {
this.formGroup.patchValue({ stop: (this.uiStopOption.value as any) as Date });
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();
})
);
this.optionChangeSubscription.add(
this.formGroup.get('stop').valueChanges.subscribe((value) => {
if (new Date(this.uiStopOption.value) !== new Date(this.formGroup.get('stop').value)) {
this.uiStopOption.setValue(value);
}
this.cdr.markForCheck();
})
);
}
}
@@ -101,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) {
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

@@ -10,6 +10,7 @@ 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: [
@@ -22,6 +23,7 @@ import { UiDateInputDirective } from '@ui/input';
FormsModule,
IconComponent,
UiDateInputDirective,
DateInputDirective,
],
exports: [FilterInputOptionDateRangeComponent],
declarations: [FilterInputOptionDateRangeComponent],

View File

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

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

@@ -1,6 +1,6 @@
import { PriceDTO, PriceValueDTO } from '@swagger/checkout';
import { DEFAULT_PRICE_DTO, DEFAULT_PRICE_VALUE, GIFT_CARD_MAX_PRICE, GIFT_CARD_TYPE, PURCHASE_OPTIONS } from '../constants';
import { isArchive, isGiftCard, isItemDTO } from './purchase-options.helpers';
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,6 +198,14 @@ 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);
@@ -241,6 +249,13 @@ export function getAvailabilityPriceForPurchaseOption(
purchaseOption: PurchaseOption
): (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);

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

@@ -85,9 +85,8 @@ export class UiDatepickerComponent extends Datepicker implements UiOverlayTrigge
}
this.setSelected(date, { emit: false });
if (isDate(date)) {
this.setDisplayed(date, { emit: false });
}
this.setDisplayed(date ?? new Date(), { emit: false });
this._cdr.markForCheck();
}
@@ -101,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();

1
package-lock.json generated
View File

@@ -34,6 +34,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",

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