#4269 Preis wird nicht von Shipping AVA übernommen

This commit is contained in:
Lorenz Hilpert
2023-08-28 17:01:20 +02:00
parent 8b6188a6b5
commit 23b77c7e48
11 changed files with 209 additions and 193 deletions

View File

@@ -155,7 +155,6 @@ export class DomainAvailabilityService {
quantity: number;
branch?: BranchDTO;
}): Observable<AvailabilityDTO> {
console.log('getTakeAwayAvailability', item, quantity, branch);
const request = !!branch ? this.getStockByBranch(branch.id) : this.getDefaultStock();
return request.pipe(
switchMap((s) =>

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

@@ -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 manual-price">
<ui-select
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-8]="manualPriceFormControl?.invalid && manualPriceFormControl?.dirty">
<shared-input-control-indicator>
<ui-svg-icon *ngIf="manualPriceFormControl?.invalid && manualPriceFormControl?.dirty" icon="mat-info"></ui-svg-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>
<ui-svg-icon *ngIf="priceFormControl?.invalid && priceFormControl?.dirty" icon="mat-info"></ui-svg-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, NgIf } from '@angular/common';
import {
Component,
ChangeDetectionStrategy,
Input,
OnInit,
OnDestroy,
OnChanges,
SimpleChanges,
AfterContentInit,
ElementRef,
Renderer2,
} from '@angular/core';
import { CommonModule } from '@angular/common';
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 { UiIconModule } from '@ui/icon';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { UiSpinnerModule } from '@ui/spinner';
import { UiTooltipModule } from '@ui/tooltip';
import { combineLatest, ReplaySubject, Subscription } from 'rxjs';
import { map, take, shareReplay, startWith, switchMap, withLatestFrom, last } from 'rxjs/operators';
import { map, take, shareReplay, startWith, switchMap, withLatestFrom, last, tap } from 'rxjs/operators';
import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
import { Item, PurchaseOptionsStore } from '../store';
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
import { UiSelectModule } from '@ui/select';
import { KeyValueDTOOfStringAndString } from '@swagger/cat';
@Component({
selector: 'scale-content, [scaleContent]',
template: '<ng-content></ng-content>',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
styles: [
`
:host {
overflow-y: hidden;
}
`,
],
})
export class ScaleContentComponent implements AfterContentInit {
// TODO: Bessere Lösung finden? Falls keine bessere Lösung gefunden wird, dann muss die Komponente auslagen
fontSizeInEm = 1;
adjustmentSteps = 0.05;
constructor(private _elementRef: ElementRef<HTMLElement>, private _renderer: Renderer2) {}
ngAfterContentInit(): void {
this.adjustFontSize();
}
adjustFontSize() {
const element = this._elementRef.nativeElement;
const clientRect = element?.getClientRects();
const scrollHeight = element?.scrollHeight;
const domRect = clientRect && clientRect[0];
if (domRect && Math.ceil(domRect?.height) < scrollHeight) {
this.fontSizeInEm -= this.adjustmentSteps;
} else {
return;
}
this._renderer.setStyle(element, 'font-size', `${this.fontSizeInEm}em`);
setTimeout(() => this.adjustFontSize(), 1);
}
}
import { ScaleContentComponent } from '@shared/components/scale-content';
@Component({
selector: 'shared-purchase-options-list-item',
@@ -115,14 +59,22 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
quantityFormControl = new FormControl<number>(null);
priceFormControl = new FormControl<string>(null, [
private readonly _giftCardValidators = [
Validators.required,
Validators.min(1),
Validators.max(GIFT_CARD_MAX_PRICE),
Validators.pattern(PRICE_PATTERN),
]);
];
private readonly _defaultValidators = [
Validators.required,
Validators.min(0.01),
Validators.max(999.99),
Validators.pattern(PRICE_PATTERN),
];
priceFormControl = new FormControl<string>(null);
manualPriceFormControl = new FormControl<string>(null, [Validators.required, Validators.max(999.99), Validators.pattern(PRICE_PATTERN)]);
manualVatFormControl = new FormControl<string>('', [Validators.required]);
selectedFormControl = new FormControl<boolean>(false);
@@ -158,10 +110,20 @@ export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnCh
})
);
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') {
@@ -203,6 +165,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)) {
@@ -223,10 +193,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) {
@@ -240,14 +211,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() {
@@ -277,7 +250,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);
}
});
@@ -303,34 +275,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?.value?.value !== 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

@@ -83,7 +83,7 @@ export class PurchaseOptionsModalComponent implements OnInit, OnDestroy {
hasDownload$ = this.purchasingOptions$.pipe(map((purchasingOptions) => purchasingOptions.includes('download')));
canContinue$ = this.store.canContinue$.pipe(tap((canContinue) => console.log('canContinue', canContinue)));
canContinue$ = this.store.canContinue$;
private _onDestroy$ = new Subject<void>();

View File

@@ -60,6 +60,14 @@ export function isGiftCard(item: Item, type: ActionType): boolean {
}
}
export function isArchive(item: Item, type: ActionType): boolean {
if (isItemDTO(item, type)) {
return item?.features?.some((f) => f.key === 'ARC');
} else {
return !!item?.features?.['ARC'];
}
}
export function mapToItemPayload({
item,
quantity,

View File

@@ -1,6 +1,6 @@
import { PriceDTO, PriceValueDTO } from '@swagger/checkout';
import { DEFAULT_PRICE_DTO, DEFAULT_PRICE_VALUE, GIFT_CARD_MAX_PRICE, GIFT_CARD_TYPE, PURCHASE_OPTIONS } from '../constants';
import { isGiftCard, isItemDTO } from './purchase-options.helpers';
import { isArchive, isGiftCard, isItemDTO } from './purchase-options.helpers';
import { PurchaseOptionsState } from './purchase-options.state';
import { ActionType, Availability, Branch, CanAdd, FetchingAvailability, Item, PurchaseOption } from './purchase-options.types';
@@ -198,43 +198,51 @@ 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 getCanEditPrice(itemId: number): (state: PurchaseOptionsState) => boolean {
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) ||
isArchive(
getItems(state).find((item) => item.id === itemId),
getType(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);
@@ -271,13 +279,29 @@ export function getPriceForPurchaseOption(
}
if (isItemDTO(item, type)) {
return item?.catalogAvailability?.price ?? DEFAULT_PRICE_DTO;
return item?.catalogAvailability?.price;
} else {
return item?.unitPrice ?? DEFAULT_PRICE_DTO;
return item?.unitPrice;
}
};
}
export function getPriceForPurchaseOption(
itemId: number,
purchaseOption: PurchaseOption
): (state: PurchaseOptionsState) => PriceDTO & { fromCatalogue?: boolean } {
return (state) => {
if (getCanEditPrice(itemId)(state)) {
const price = getPrices(state)[itemId];
if (price) {
return price;
}
}
return getAvailabilityPriceForPurchaseOption(itemId, purchaseOption)(state) ?? DEFAULT_PRICE_DTO;
};
}
export function getQuantityForItem(itemId: number): (state: PurchaseOptionsState) => number {
return (state) => {
const item = getItems(state).find((item) => item.id === itemId);
@@ -357,12 +381,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,6 +640,18 @@ 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];

View File

@@ -24,7 +24,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;