Merged PR 1575: #3399 #3398 #3397 Responsive Design Checkout Cart Review

#3399 #3398 #3397 Responsive Design Checkout Cart Review
This commit is contained in:
Nino Righi
2023-07-06 14:53:39 +00:00
committed by Lorenz Hilpert
parent 8a4fe7aedd
commit fc76f34d38
35 changed files with 903 additions and 652 deletions

View File

@@ -1,11 +1,12 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationService } from '@core/application';
import { CheckoutNavigationService } from '@shared/services';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCartGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _router: Router) {}
constructor(private readonly _applicationService: ApplicationService, private _checkoutNavigationService: CheckoutNavigationService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
@@ -21,7 +22,7 @@ export class CanActivateCartGuard implements CanActivate {
name: `Vorgang ${processes.length + 1}`,
});
}
await this._router.navigate(['/kunde', lastActivatedProcessId, 'cart']);
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: lastActivatedProcessId });
return false;
}
}

View File

@@ -8,7 +8,7 @@ import { BranchDTO } from '@swagger/checkout';
import { UiModalService } from '@ui/modal';
import { ModalReviewsComponent } from '@modal/reviews';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
import { debounceTime, filter, first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { ArticleDetailsStore } from './article-details.store';
import { ModalImagesComponent } from 'apps/modal/images/src/public-api';
import { ProductImageService } from 'apps/cdn/product-image/src/public-api';
@@ -21,7 +21,7 @@ import { DatePipe } from '@angular/common';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
import { DomainAvailabilityService } from '@domain/availability';
import { EnvironmentService } from '@core/environment';
import { ProductCatalogNavigationService } from '@shared/services';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { DomainCheckoutService } from '@domain/checkout';
@Component({
@@ -147,6 +147,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _availability: DomainAvailabilityService,
private _navigationService: ProductCatalogNavigationService,
private _checkoutNavigationService: CheckoutNavigationService,
private _environment: EnvironmentService,
private _router: Router,
private _domainCheckoutService: DomainCheckoutService
@@ -345,9 +346,9 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
.pipe(first())
.toPromise();
if (customer) {
this.navigateToShoppingCart();
await this.navigateToShoppingCart();
} else {
this.navigateToCustomerSearch();
await this.navigateToCustomerSearch();
}
} else if (result?.data === 'continue-shopping') {
this.navigateToResultList();
@@ -355,8 +356,8 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
});
}
navigateToShoppingCart() {
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/cart/review`]);
async navigateToShoppingCart() {
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: this.applicationService.activatedProcessId });
}
async navigateToCustomerSearch() {

View File

@@ -2,7 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { ProductCatalogNavigationService } from '@shared/services';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { UiModalRef } from '@ui/modal';
@Component({
@@ -21,7 +21,8 @@ export class AddedToCartModalComponent {
private readonly _router: Router,
private readonly _applicationService: ApplicationService,
private readonly _navigation: ProductCatalogNavigationService,
private readonly _environment: EnvironmentService
private readonly _environment: EnvironmentService,
private readonly _checkoutNavigationService: CheckoutNavigationService
) {}
async continue() {
if (this.isTablet) {
@@ -30,8 +31,8 @@ export class AddedToCartModalComponent {
this.ref.close();
}
toCart() {
this._router.navigate([`/kunde/${this._applicationService.activatedProcessId}/cart/review`]);
async toCart() {
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: this._applicationService.activatedProcessId });
this.ref.close();
}
}

View File

@@ -187,12 +187,23 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
const params = state.filter.getQueryParams();
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
const item = state.items.find((f) => f);
const ean = this.route?.snapshot?.params?.ean;
const itemId = this.route?.snapshot?.params?.id ? Number(this.route?.snapshot?.params?.id) : item.id; // Nicht zum ersten Item der Liste springen wenn bereits eines selektiert ist
this._navigationService.navigateToDetails({
processId,
itemId,
queryParams: this.isTablet ? undefined : params,
});
// Navigation from Cart uses ean
if (!!ean) {
this._navigationService.navigateToDetails({
processId,
ean,
queryParams: this.isTablet ? undefined : params,
});
} else {
this._navigationService.navigateToDetails({
processId,
itemId,
queryParams: this.isTablet ? undefined : params,
});
}
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
this._navigationService.navigateToResults({
processId,

View File

@@ -9,6 +9,7 @@ import { Subject } from 'rxjs';
import { first, shareReplay, takeUntil } from 'rxjs/operators';
import { CheckoutDummyData } from './checkout-dummy-data';
import { CheckoutDummyStore } from './checkout-dummy.store';
import { CheckoutNavigationService } from '@shared/services';
@Component({
selector: 'page-checkout-dummy',
@@ -43,7 +44,8 @@ export class CheckoutDummyComponent implements OnInit, OnDestroy {
private _modal: UiModalService,
private _store: CheckoutDummyStore,
private _ref: UiModalRef<any, CheckoutDummyData>,
private readonly _applicationService: ApplicationService
private readonly _applicationService: ApplicationService,
private readonly _checkoutNavigationService: CheckoutNavigationService
) {}
ngOnInit() {
@@ -198,7 +200,7 @@ export class CheckoutDummyComponent implements OnInit, OnDestroy {
queryParams: { customertype: filter.customertype },
});
} else {
this._router.navigate(['/kunde', this._applicationService.activatedProcessId, 'cart', 'review']);
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: this._applicationService.activatedProcessId });
}
this._ref?.close();
});
@@ -215,7 +217,7 @@ export class CheckoutDummyComponent implements OnInit, OnDestroy {
queryParams: { customertype: filter.customertype },
});
} else {
this._router.navigate(['/kunde', this._applicationService.activatedProcessId, 'cart', 'review']);
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: this._applicationService.activatedProcessId });
}
this._ref?.close();
});

View File

@@ -1,8 +1,8 @@
<ng-container *ngIf="(groupedItems$ | async)?.length <= 0 && !(fetching$ | async); else shoppingCart">
<div class="card stretch card-empty">
<div class="empty-message">
<span class="cart-icon">
<ui-icon icon="cart" size="16px"></ui-icon>
<span class="cart-icon flex items-center justify-center">
<shared-icon icon="shopping-cart-bold" [size]="24"></shared-icon>
</span>
<h1>Ihr Warenkorb ist leer.</h1>
@@ -13,7 +13,7 @@
</p>
<div class="btn-wrapper">
<a class="cta-primary" [routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search']">Artikel suchen</a>
<a class="cta-primary" [routerLink]="productSearchBasePath">Artikel suchen</a>
<button class="cta-secondary" (click)="openDummyModal()">Neuanlage</button>
</div>
</div>
@@ -33,110 +33,75 @@
</button>
</div>
<h1 class="header">Warenkorb</h1>
<h5 class="sub-header">Überprüfen Sie die Details.</h5>
<ng-container *ngIf="payer$ | async">
<hr />
<div class="row">
<ng-container *ngIf="showBillingAddress$ | async; else customerName">
<div class="label">
Rechnungsadresse
</div>
<div class="value">
{{ payer$ | async | payerAddress | trim: 55 }}
</div>
</ng-container>
<ng-template #customerName>
<div class="label">
Name, Vorname
</div>
<div class="value" *ngIf="payer$ | async; let payer">{{ payer.lastName }}, {{ payer.firstName }}</div>
</ng-template>
<ng-container *ngIf="!(isDesktop$ | async)">
<page-checkout-review-details></page-checkout-review-details>
</ng-container>
<div class="grow"></div>
<div>
<button *ngIf="payer$ | async" (click)="changeAddress()" class="cta-edit">
Ändern
</button>
</div>
</div>
</ng-container>
<hr />
<ng-container *ngIf="showNotificationChannels$ | async">
<form *ngIf="control" [formGroup]="control">
<shared-notification-channel-control
[communicationDetails]="communicationDetails$ | async"
(channelActionEvent)="onNotificationChange($event)"
[channelActionName]="'Speichern'"
[channelActionLoading]="notificationChannelLoading$ | async"
formGroupName="notificationChannel"
>
</shared-notification-channel-control>
</form>
<hr />
</ng-container>
<page-special-comment [ngModel]="specialComment$ | async" (ngModelChange)="setAgentComment($event)"> </page-special-comment>
<ng-container *ngFor="let group of groupedItems$ | async; let lastGroup = last">
<ng-container *ngIf="group?.orderType !== undefined">
<hr />
<div class="row item-group-header">
<ui-icon
<div class="row item-group-header bg-[#F5F7FA]">
<shared-icon
*ngIf="group.orderType !== 'Dummy'"
class="icon-order-type"
[size]="group.orderType === 'B2B-Versand' ? '50px' : '25px'"
[size]="group.orderType === 'B2B-Versand' ? 36 : 24"
[icon]="
group.orderType === 'Abholung'
? 'box_out'
? 'isa-box-out'
: group.orderType === 'Versand'
? 'truck'
? 'isa-truck'
: group.orderType === 'Rücklage'
? 'shopping_bag'
? 'isa-shopping-bag'
: group.orderType === 'B2B-Versand'
? 'truck_b2b'
? 'isa-b2b-truck'
: group.orderType === 'Download'
? 'download'
: 'truck'
? 'isa-download'
: 'isa-truck'
"
></ui-icon>
></shared-icon>
<div class="label" [class.dummy]="group.orderType === 'Dummy'">
{{ group.orderType !== 'Dummy' ? group.orderType : 'Manuelle Anlage / Dummy Bestellung' }}
<button *ngIf="group.orderType === 'Dummy'" class="cta-secondary" (click)="openDummyModal()">Hinzufügen</button>
<button
*ngIf="group.orderType === 'Dummy'"
class="text-brand border-none font-bold text-p1 outline-none pl-4"
(click)="openDummyModal()"
>
Hinzufügen
</button>
</div>
<div class="grow"></div>
<div *ngIf="group.orderType !== 'Download' && group.orderType !== 'Dummy'">
<div class="pl-4" *ngIf="group.orderType !== 'Download' && group.orderType !== 'Dummy'">
<button class="cta-edit" (click)="showPurchasingListModal(group.items)">
Ändern
Lieferung Ändern
</button>
</div>
</div>
<hr *ngIf="group.orderType === 'Download'" />
</ng-container>
<ng-container *ngIf="group.orderType === 'Versand' || group.orderType === 'B2B-Versand' || group.orderType === 'DIG-Versand'">
<hr />
<div class="row">
<div class="label">
Lieferadresse
</div>
<div class="value">
{{ shippingAddress$ | async | shippingAddress | trim: 55 }}
<div class="flex flex-row items-center px-5 pt-0 pb-[0.875rem] -mt-2 bg-[#F5F7FA]">
<div class="text-p2">
{{ shippingAddress$ | async | shippingAddress }}
</div>
<div class="grow"></div>
<div>
<div class="pl-4">
<button (click)="changeAddress()" class="cta-edit">
Ändern
Adresse Ändern
</button>
</div>
</div>
<hr />
</ng-container>
<hr />
<ng-container *ngFor="let item of group.items; let lastItem = last; let i = index">
<ng-container
*ngIf="group?.orderType !== undefined && (item.features?.orderType === 'Abholung' || item.features?.orderType === 'Rücklage')"
>
<ng-container *ngIf="item?.destination?.data?.targetBranch?.data; let targetBranch">
<ng-container *ngIf="i === 0 || targetBranch.id !== group.items[i - 1].destination?.data?.targetBranch?.data.id">
<div class="row">
<span class="branch-label">Filiale</span>
<div class="flex flex-row items-center px-5 pt-0 pb-[0.875rem] -mt-2 bg-[#F5F7FA]">
<span class="branch-name">{{ targetBranch?.name }} | {{ targetBranch | branchAddress }}</span>
</div>
<hr />
@@ -158,35 +123,35 @@
<hr *ngIf="!lastItem" />
</ng-container>
</ng-container>
<div class="h-[8.9375rem]"></div>
</div>
<div class="card footer row">
<ng-container *ngIf="totalItemCount$ | async; let totalItemCount">
<div *ngIf="totalReadingPoints$ | async; let totalReadingPoints" class="total-item-reading-points">
{{ totalItemCount }} Artikel | {{ totalReadingPoints }} Lesepunkte
</div>
</ng-container>
<div class="grow"></div>
<div class="total-cta-container">
<div class="total-container">
<div class="card footer flex flex-col justify-center items-center">
<div class="flex flex-row items-start justify-between w-full mb-1">
<ng-container *ngIf="totalItemCount$ | async; let totalItemCount">
<div *ngIf="totalReadingPoints$ | async; let totalReadingPoints" class="total-item-reading-points w-full">
{{ totalItemCount }} Artikel | {{ totalReadingPoints }} Lesepunkte
</div>
</ng-container>
<div class="flex flex-col w-full">
<strong class="total-value">
Zwischensumme {{ shoppingCart?.total?.value | currency: shoppingCart?.total?.currency:'code' }}
</strong>
<span class="shipping-cost-info">ohne Versandkosten</span>
</div>
<button
class="cta-primary"
(click)="order()"
[disabled]="
showOrderButtonSpinner ||
((primaryCtaLabel$ | async) === 'Bestellen' && !(checkNotificationChannelControl$ | async)) ||
control.invalid
"
>
<ui-spinner [show]="showOrderButtonSpinner">
{{ primaryCtaLabel$ | async }}
</ui-spinner>
</button>
</div>
<button
class="cta-primary"
(click)="order()"
[disabled]="
showOrderButtonSpinner ||
((primaryCtaLabel$ | async) === 'Bestellen' && !(checkNotificationChannelControl$ | async)) ||
notificationsControl?.invalid
"
>
<ui-spinner [show]="showOrderButtonSpinner">
{{ primaryCtaLabel$ | async }}
</ui-spinner>
</button>
</div>
</ng-container>
</ng-template>

View File

@@ -1,12 +1,9 @@
:host {
@apply block box-border relative;
height: calc(100vh - 285px);
@apply box-border relative block h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
.stretch {
@apply overflow-scroll;
height: 100vh;
max-height: calc(100vh - 390px);
@apply overflow-scroll h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)];
}
button {
@@ -45,10 +42,8 @@ button {
}
.cart-icon {
@apply justify-center items-center ml-auto mr-auto bg-wild-blue-yonder text-white mb-px-10;
@apply justify-center items-center ml-auto mr-auto bg-wild-blue-yonder text-white mb-px-10 w-10 h-10;
border-radius: 50%;
width: 32px;
height: 32px;
ui-icon {
@apply justify-center;
@@ -58,7 +53,7 @@ button {
}
.cta-print-wrapper {
@apply pl-4 pr-4 pt-4 text-right;
@apply px-5 pt-5 text-right;
}
.cta-print,
@@ -83,16 +78,12 @@ button {
}
.header {
@apply text-center text-3xl my-0 mb-2;
}
.sub-header {
@apply text-center text-h3 font-normal my-0 mb-8;
@apply text-center text-h2 desktop:pb-10 -mt-2;
}
hr {
height: 2px;
@apply bg-disabled-customer;
@apply bg-[#EDEFF0];
}
h1 {
@@ -121,12 +112,11 @@ h1 {
}
.icon-order-type {
@apply text-font-customer mr-3;
@apply text-black mr-2;
}
.item-group-header {
@apply py-0 px-4 text-lg;
height: 80px;
@apply px-5 py-[0.875rem] text-p1;
}
.branch-label {
@@ -134,7 +124,7 @@ h1 {
}
.branch-name {
@apply text-p2 overflow-hidden overflow-ellipsis ml-4;
@apply text-p2 overflow-hidden overflow-ellipsis;
}
.book-icon {
@@ -143,26 +133,18 @@ h1 {
}
.footer {
@apply absolute bottom-0 left-0 right-0 p-7;
@apply absolute bottom-0 left-0 right-0 p-5;
box-shadow: 0px -2px 24px 0px #dce2e9;
}
.total-container {
@apply flex flex-col ml-4;
}
.total-cta-container {
@apply flex flex-row whitespace-nowrap;
}
.shipping-cost-info {
@apply text-p3 mr-4 self-end;
@apply text-p3 self-end;
}
.total-value {
@apply text-lg mr-4;
@apply text-p1 self-end;
}
.total-item-reading-points {
@apply text-p2 font-bold text-ucla-blue;
@apply text-p2 font-bold text-black;
}

View File

@@ -1,29 +1,22 @@
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit } from '@angular/core';
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, DestinationDTO, NotificationChannel, ShoppingCartItemDTO, ShoppingCartDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal';
import { AvailabilityDTO, DestinationDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer';
import { AuthService } from '@core/auth';
import { first, map, shareReplay, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { first, map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { emailNotificationValidator, mobileNotificationValidator } from '@shared/components/notification-channel-control';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { CheckoutDummyData } from '../checkout-dummy/checkout-dummy-data';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
export interface CheckoutReviewComponentState {
shoppingCart: ShoppingCartDTO;
shoppingCartItems: ShoppingCartItemDTO[];
fetching: boolean;
}
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { EnvironmentService } from '@core/environment';
import { CheckoutReviewStore } from './checkout-review.store';
@Component({
selector: 'page-checkout-review',
@@ -31,53 +24,24 @@ export interface CheckoutReviewComponentState {
styleUrls: ['checkout-review.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewComponentState> implements OnInit {
private _orderCompleted = new Subject<void>();
export class CheckoutReviewComponent implements OnInit, OnDestroy {
payer$ = this._store.payer$;
checkNotificationChannelControl$ = new BehaviorSubject<boolean>(true);
shoppingCart$ = this._store.shoppingCart$;
get shoppingCart() {
return this.get((s) => s.shoppingCart);
}
set shoppingCart(shoppingCart: ShoppingCartDTO) {
this.patchState({ shoppingCart });
}
readonly shoppingCart$ = this.select((s) => s.shoppingCart);
fetching$ = this._store.fetching$;
get shoppingCartItems() {
return this.get((s) => s.shoppingCartItems);
}
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
this.patchState({ shoppingCartItems });
}
readonly shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
get fetching() {
return this.get((s) => s.fetching);
}
set fetching(fetching: boolean) {
this.patchState({ fetching });
}
readonly fetching$ = this.select((s) => s.fetching);
payer$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
switchMap((processId) => this.domainCheckoutService.getPayer({ processId })),
shareReplay()
);
notificationsControl = this._store.notificationsControl;
shippingAddress$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
switchMap((processId) => this.domainCheckoutService.getShippingAddress({ processId }))
);
shoppingCartItemsWithoutOrderType$ = this.shoppingCartItems$.pipe(
takeUntil(this._orderCompleted),
shoppingCartItemsWithoutOrderType$ = this._store.shoppingCartItems$.pipe(
map((items) => items?.filter((item) => item?.features?.orderType === undefined))
);
groupedItems$ = this.shoppingCartItems$.pipe(
takeUntil(this._orderCompleted),
groupedItems$ = this._store.shoppingCartItems$.pipe(
map((items) =>
items.reduce((grouped, item) => {
let index = grouped.findIndex((g) =>
@@ -118,16 +82,9 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
)
);
specialComment$ = this.applicationService.activatedProcessId$.pipe(
switchMap((processId) => this.domainCheckoutService.getSpecialComment({ processId }))
);
totalItemCount$ = this._store.shoppingCartItems$.pipe(map((items) => items.reduce((total, item) => total + item.quantity, 0)));
totalItemCount$ = this.shoppingCartItems$.pipe(
takeUntil(this._orderCompleted),
map((items) => items.reduce((total, item) => total + item.quantity, 0))
);
totalReadingPoints$ = this.shoppingCartItems$.pipe(
totalReadingPoints$ = this._store.shoppingCartItems$.pipe(
switchMap((displayOrders) => {
if (displayOrders.length === 0) {
return NEVER;
@@ -155,47 +112,9 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
})
);
customerFeatures$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
switchMap((processId) => this.domainCheckoutService.getCustomerFeatures({ processId }))
);
customerFeatures$ = this._store.customerFeatures$;
control: UntypedFormGroup;
showBillingAddress$ = this.shoppingCartItems$.pipe(
takeUntil(this._orderCompleted),
withLatestFrom(this.customerFeatures$),
map(
([items, customerFeatures]) =>
items.some(
(item) =>
item.features?.orderType === 'Versand' ||
item.features?.orderType === 'B2B-Versand' ||
item.features?.orderType === 'DIG-Versand'
) || !!customerFeatures?.b2b
)
);
showNotificationChannels$ = combineLatest([this.shoppingCartItems$, this.payer$]).pipe(
takeUntil(this._orderCompleted),
map(
([items, payer]) =>
!!payer && items.some((item) => item.features?.orderType === 'Rücklage' || item.features?.orderType === 'Abholung')
)
);
notificationChannel$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
switchMap((processId) => this.domainCheckoutService.getNotificationChannels({ processId }))
);
communicationDetails$ = this.applicationService.activatedProcessId$.pipe(
takeUntil(this._orderCompleted),
switchMap((processId) => this.domainCheckoutService.getBuyerCommunicationDetails({ processId })),
map((communicationDetails) => communicationDetails ?? { email: undefined, mobile: undefined })
);
notificationChannelLoading$ = new Subject<boolean>();
checkNotificationChannelControl$ = this._store.checkNotificationChannelControl$;
showQuantityControlSpinnerItemId: number;
quantityError$ = new BehaviorSubject<{ [key: string]: string }>({});
@@ -227,76 +146,57 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
loadingOnQuantityChangeById$ = new Subject<number>();
showOrderButtonSpinner: boolean;
get productSearchBasePath() {
return this._productNavigationService.getArticleSearchBasePath(this.applicationService.activatedProcessId);
}
get isDesktop$() {
return this._environmentService.matchDesktop$.pipe(
map((state) => {
return state.matches;
}),
shareReplay()
);
}
private _onDestroy$ = new Subject<void>();
constructor(
private domainCheckoutService: DomainCheckoutService,
public applicationService: ApplicationService,
private availabilityService: DomainAvailabilityService,
private uiModal: UiModalService,
private auth: AuthService,
private router: Router,
private cdr: ChangeDetectorRef,
private domainCatalogService: DomainCatalogService,
private breadcrumb: BreadcrumbService,
private domainPrinterService: DomainPrinterService,
private _fb: UntypedFormBuilder,
private _purchaseOptionsModalService: PurchaseOptionsModalService
) {
super({
shoppingCart: undefined,
shoppingCartItems: [],
fetching: false,
});
}
private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _productNavigationService: ProductCatalogNavigationService,
private _navigationService: CheckoutNavigationService,
private _environmentService: EnvironmentService,
private _store: CheckoutReviewStore
) {}
async ngOnInit() {
this.applicationService.activatedProcessId$.pipe(takeUntil(this._orderCompleted)).subscribe((_) => {
this.loadShoppingCart();
this.applicationService.activatedProcessId$.pipe(takeUntil(this._onDestroy$)).subscribe((_) => {
this._store.loadShoppingCart();
});
await this.removeBreadcrumbs();
await this.updateBreadcrumb();
await this.initNotificationsControl();
}
loadShoppingCart = this.effect(($) =>
$.pipe(
tap(() => (this.fetching = true)),
withLatestFrom(this.applicationService.activatedProcessId$),
switchMap(([_, processId]) => {
return this.domainCheckoutService.getShoppingCart({ processId, latest: true }).pipe(
tapResponse(
(shoppingCart) => {
const shoppingCartItems = shoppingCart?.items?.map((item) => item.data) || [];
this.patchState({
shoppingCart,
shoppingCartItems,
});
// this.checkQuantityErrors(shoppingCartItems);
},
(err) => {},
() => {}
)
);
}),
tap(() => (this.fetching = false))
)
);
// checkQuantityErrors(shoppingCartItems: ShoppingCartItemDTO[]) {
// shoppingCartItems.forEach((item) => {
// if (item.features?.orderType === 'Abholung') {
// this.setQuantityError(item, item.availability, item.quantity > item.availability?.inStock);
// } else {
// this.setQuantityError(item, item.availability, false);
// }
// });
// }
ngOnDestroy(): void {
this._onDestroy$.next();
this._onDestroy$.complete();
}
async updateBreadcrumb() {
await this.breadcrumb.addOrUpdateBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: 'Warenkorb',
path: `/kunde/${this.applicationService.activatedProcessId}/cart/review`,
path: this._navigationService.getCheckoutReviewPath(this.applicationService.activatedProcessId),
tags: ['checkout', 'cart'],
section: 'customer',
});
@@ -312,101 +212,6 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
});
}
async initNotificationsControl() {
const fb = this._fb;
const notificationChannel = await this.notificationChannel$.pipe(first()).toPromise();
const communicationDetails = await this.communicationDetails$.pipe(first()).toPromise();
let selectedNotificationChannel = 0;
if ((notificationChannel & 1) === 1 && communicationDetails.email) {
selectedNotificationChannel += 1;
}
if ((notificationChannel & 2) === 2 && communicationDetails.mobile) {
selectedNotificationChannel += 2;
}
// #1967 Wenn E-Mail und SMS als NotificationChannel gesetzt sind, nur E-Mail anhaken
if ((selectedNotificationChannel & 3) === 3) {
selectedNotificationChannel = 1;
}
this.control = fb.group({
notificationChannel: new UntypedFormGroup({
selected: new UntypedFormControl(selectedNotificationChannel),
email: new UntypedFormControl(communicationDetails ? communicationDetails.email : '', emailNotificationValidator),
mobile: new UntypedFormControl(communicationDetails ? communicationDetails.mobile : '', mobileNotificationValidator),
}),
});
}
async onNotificationChange(notificationChannels?: NotificationChannel[]) {
this.notificationChannelLoading$.next(true);
try {
const control = this.control?.getRawValue();
const notificationChannel = notificationChannels
? (notificationChannels.reduce((val, current) => val | current, 0) as NotificationChannel)
: control?.notificationChannel?.selected || 0;
const processId = await this.applicationService.activatedProcessId$.pipe(first()).toPromise();
const email = control?.notificationChannel?.email;
const mobile = control?.notificationChannel?.mobile;
// Check if E-Mail and Mobilnumber is available if E-Mail or SMS checkbox is active
if (notificationChannel === 3 && (!email || !mobile)) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 2 && !mobile) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 1 && !email) {
this.checkNotificationChannelControl$.next(false);
} else {
this.checkNotificationChannelControl$.next(true);
}
// NotificationChannel nur speichern, wenn Haken und Value gesetzt
let setNotificationChannel = 0;
if ((notificationChannel & 1) === 1 && email) {
setNotificationChannel += 1;
}
if ((notificationChannel & 2) === 2 && mobile) {
setNotificationChannel += 2;
}
if (notificationChannel > 0) {
this.setCommunicationDetails({ processId, notificationChannel, email, mobile });
}
this.domainCheckoutService.setNotificationChannels({
processId,
notificationChannels: (setNotificationChannel as NotificationChannel) || 0,
});
} catch (error) {
this.uiModal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim setzen des Benachrichtigungskanals' });
}
this.notificationChannelLoading$.next(false);
}
setCommunicationDetails({
processId,
notificationChannel,
email,
mobile,
}: {
processId: number;
notificationChannel: number;
email: string;
mobile: string;
}) {
const emailValid = this.control?.get('notificationChannel')?.get('email')?.valid;
const mobileValid = this.control?.get('notificationChannel')?.get('mobile')?.valid;
if (notificationChannel === 3 && emailValid && mobileValid) {
this.domainCheckoutService.setBuyerCommunicationDetails({ processId, email, mobile });
} else if (notificationChannel === 1 && emailValid) {
this.domainCheckoutService.setBuyerCommunicationDetails({ processId, email });
} else if (notificationChannel === 2 && mobileValid) {
this.domainCheckoutService.setBuyerCommunicationDetails({ processId, mobile });
}
}
openDummyModal(data?: CheckoutDummyData) {
this.uiModal.open({
content: CheckoutDummyComponent,
@@ -415,18 +220,6 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
}
changeDummyItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
// const data: CheckoutDummyData = {
// itemType: shoppingCartItem.itemType,
// price: shoppingCartItem?.availability?.price?.value?.value,
// vat: shoppingCartItem?.availability?.price?.vat?.vatType,
// supplier: shoppingCartItem?.availability?.supplier?.id,
// estimatedShippingDate: shoppingCartItem?.estimatedShippingDate,
// manufacturer: shoppingCartItem?.product?.manufacturer,
// name: shoppingCartItem?.product?.name,
// contributors: shoppingCartItem?.product?.contributors,
// ean: shoppingCartItem?.product?.ean,
// quantity: shoppingCartItem?.quantity,
// };
this.openDummyModal(shoppingCartItem);
}
@@ -438,10 +231,6 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
});
}
setAgentComment(agentComment: string) {
this.domainCheckoutService.setSpecialComment({ processId: this.applicationService.activatedProcessId, agentComment });
}
async openPrintModal() {
let shoppingCart = await this.shoppingCart$.pipe(first()).toPromise();
this.uiModal.open({
@@ -563,7 +352,6 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
},
})
.toPromise();
// this.setQuantityError(shoppingCartItem, availability, false);
} else if (availability) {
// Wenn das Ergebnis der Availability Abfrage keinen Preis zurückliefert (z.B. HFI Geschenkkarte), wird der Preis aus der
// Availability vor der Abfrage verwendet
@@ -594,17 +382,6 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
this.loadingOnQuantityChangeById$.next(undefined);
}
// setQuantityError(item: ShoppingCartItemDTO, availability: AvailabilityDTO, error: boolean) {
// const quantityErrors: { [key: string]: string } = this.quantityError$.value;
// if (error) {
// quantityErrors[item.product.catalogProductNumber] = `${availability.inStock} Exemplar(e) sofort lieferbar`;
// this.quantityError$.next({ ...quantityErrors });
// } else {
// delete quantityErrors[item.product.catalogProductNumber];
// this.quantityError$.next({ ...quantityErrors });
// }
// }
// Bei unbekannten Kunden und DIG Bestellung findet ein Vergleich der Preise statt
compareDeliveryAndCatalogPrice(availability: AvailabilityDTO, orderType: string, shoppingCartItemPrice: number) {
if (['Versand', 'DIG-Versand'].includes(orderType) && shoppingCartItemPrice < availability?.price?.value?.value) {
@@ -622,17 +399,6 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
return availability;
}
async changeAddress() {
const processId = this.applicationService.activatedProcessId;
const customer = await this.domainCheckoutService.getBuyer({ processId }).pipe(first()).toPromise();
if (!customer) {
this.navigateToCustomerSearch(processId);
return;
}
const customerId = customer.source;
this.router.navigate(['/kunde', this.applicationService.activatedProcessId, 'customer', `${customerId}`]);
}
async navigateToCustomerSearch(processId: number) {
try {
const response = await this.customerFeatures$
@@ -660,6 +426,17 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
});
}
async changeAddress() {
const processId = this.applicationService.activatedProcessId;
const customer = await this.domainCheckoutService.getBuyer({ processId }).pipe(first()).toPromise();
if (!customer) {
this.navigateToCustomerSearch(processId);
return;
}
const customerId = customer.source;
this.router.navigate(['/kunde', this.applicationService.activatedProcessId, 'customer', `${customerId}`]);
}
async order() {
const shoppingCartItemsWithoutOrderType = await this.shoppingCartItemsWithoutOrderType$.pipe(first()).toPromise();
@@ -676,12 +453,11 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
try {
this.showOrderButtonSpinner = true;
// Ticket #3287 Um nur E-Mail und SMS Benachrichtigungen zu setzen und um alle anderen Benachrichtigungskanäle wie z.B. Brief zu deaktivieren
await this.onNotificationChange();
await this._store.onNotificationChange();
const orders = await this.domainCheckoutService.completeCheckout({ processId }).toPromise();
const orderIds = orders.map((order) => order.id).join(',');
this._orderCompleted.next();
await this.patchProcess(processId);
await this.router.navigate(['/kunde', processId, 'cart', 'summary', orderIds]);
await this._navigationService.navigateToCheckoutSummary({ processId, orderIds });
} catch (error) {
const response = error?.error;
let message: string = response?.message ?? '';
@@ -699,9 +475,8 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
}
if (error.status === 409) {
this._orderCompleted.next();
await this.patchProcess(processId);
await this.router.navigate(['/kunde', processId, 'cart', 'summary']);
await this._navigationService.navigateToCheckoutSummary({ processId });
}
} finally {
this.showOrderButtonSpinner = false;

View File

@@ -3,7 +3,6 @@ import { CommonModule } from '@angular/common';
import { CheckoutReviewComponent } from './checkout-review.component';
import { PageCheckoutPipeModule } from '../pipes/page-checkout-pipe.module';
import { UiIconModule } from '@ui/icon';
import { ProductImageModule } from 'apps/cdn/product-image/src/public-api';
import { RouterModule } from '@angular/router';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@@ -17,6 +16,10 @@ import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { SharedNotificationChannelControlModule } from '@shared/components/notification-channel-control';
import { UiCommonModule } from '@ui/common';
import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-item.component';
import { CheckoutReviewDetailsComponent } from './details/checkout-review-details.component';
import { CheckoutReviewStore } from './checkout-review.store';
import { IconModule } from '@shared/components/icon';
import { TextFieldModule } from '@angular/cdk/text-field';
@NgModule({
imports: [
@@ -24,7 +27,7 @@ import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-it
UiCommonModule,
RouterModule,
PageCheckoutPipeModule,
UiIconModule,
IconModule,
UiQuantityDropdownModule,
ProductImageModule,
FormsModule,
@@ -35,8 +38,10 @@ import { ShoppingCartItemComponent } from './shopping-cart-item/shopping-cart-it
UiInputModule,
UiCheckboxModule,
SharedNotificationChannelControlModule,
TextFieldModule,
],
exports: [CheckoutReviewComponent],
declarations: [CheckoutReviewComponent, SpecialCommentComponent, ShoppingCartItemComponent],
exports: [CheckoutReviewComponent, CheckoutReviewDetailsComponent],
declarations: [CheckoutReviewComponent, SpecialCommentComponent, ShoppingCartItemComponent, CheckoutReviewDetailsComponent],
providers: [CheckoutReviewStore],
})
export class CheckoutReviewModule {}

View File

@@ -0,0 +1,172 @@
import { Injectable } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { NotificationChannel, PayerDTO, ShoppingCartDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { BehaviorSubject, Subject } from 'rxjs';
import { first, map, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
export interface CheckoutReviewState {
payer: PayerDTO;
shoppingCart: ShoppingCartDTO;
shoppingCartItems: ShoppingCartItemDTO[];
fetching: boolean;
}
@Injectable()
export class CheckoutReviewStore extends ComponentStore<CheckoutReviewState> {
get shoppingCart() {
return this.get((s) => s.shoppingCart);
}
set shoppingCart(shoppingCart: ShoppingCartDTO) {
this.patchState({ shoppingCart });
}
readonly shoppingCart$ = this.select((s) => s.shoppingCart);
get shoppingCartItems() {
return this.get((s) => s.shoppingCartItems);
}
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
this.patchState({ shoppingCartItems });
}
readonly shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
get fetching() {
return this.get((s) => s.fetching);
}
set fetching(fetching: boolean) {
this.patchState({ fetching });
}
readonly fetching$ = this.select((s) => s.fetching);
customerFeatures$ = this._application.activatedProcessId$.pipe(
switchMap((processId) => this._domainCheckoutService.getCustomerFeatures({ processId })),
shareReplay()
);
payer$ = this._application.activatedProcessId$.pipe(
switchMap((processId) => this._domainCheckoutService.getPayer({ processId })),
shareReplay()
);
showBillingAddress$ = this.shoppingCartItems$.pipe(
withLatestFrom(this.customerFeatures$),
map(
([items, customerFeatures]) =>
items.some(
(item) =>
item.features?.orderType === 'Versand' ||
item.features?.orderType === 'B2B-Versand' ||
item.features?.orderType === 'DIG-Versand'
) || !!customerFeatures?.b2b
)
);
checkNotificationChannelControl$ = new BehaviorSubject<boolean>(true);
notificationChannelLoading$ = new Subject<boolean>();
notificationsControl: UntypedFormGroup;
constructor(
private _domainCheckoutService: DomainCheckoutService,
private _application: ApplicationService,
private _uiModal: UiModalService
) {
super({ payer: undefined, shoppingCart: undefined, shoppingCartItems: [], fetching: false });
}
loadShoppingCart = this.effect(($) =>
$.pipe(
tap(() => (this.fetching = true)),
withLatestFrom(this._application.activatedProcessId$),
switchMap(([_, processId]) => {
return this._domainCheckoutService.getShoppingCart({ processId, latest: true }).pipe(
tapResponse(
(shoppingCart) => {
const shoppingCartItems = shoppingCart?.items?.map((item) => item.data) || [];
this.patchState({
shoppingCart,
shoppingCartItems,
});
},
(err) => {},
() => {}
)
);
}),
tap(() => (this.fetching = false))
)
);
async onNotificationChange(notificationChannels?: NotificationChannel[]) {
this.notificationChannelLoading$.next(true);
try {
const control = this.notificationsControl?.getRawValue();
const notificationChannel = notificationChannels
? (notificationChannels.reduce((val, current) => val | current, 0) as NotificationChannel)
: control?.notificationChannel?.selected || 0;
const processId = await this._application.activatedProcessId$.pipe(first()).toPromise();
const email = control?.notificationChannel?.email;
const mobile = control?.notificationChannel?.mobile;
// Check if E-Mail and Mobilnumber is available if E-Mail or SMS checkbox is active
if (notificationChannel === 3 && (!email || !mobile)) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 2 && !mobile) {
this.checkNotificationChannelControl$.next(false);
} else if (notificationChannel === 1 && !email) {
this.checkNotificationChannelControl$.next(false);
} else {
this.checkNotificationChannelControl$.next(true);
}
// NotificationChannel nur speichern, wenn Haken und Value gesetzt
let setNotificationChannel = 0;
if ((notificationChannel & 1) === 1 && email) {
setNotificationChannel += 1;
}
if ((notificationChannel & 2) === 2 && mobile) {
setNotificationChannel += 2;
}
if (notificationChannel > 0) {
this.setCommunicationDetails({ processId, notificationChannel, email, mobile });
}
this._domainCheckoutService.setNotificationChannels({
processId,
notificationChannels: (setNotificationChannel as NotificationChannel) || 0,
});
} catch (error) {
this._uiModal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim setzen des Benachrichtigungskanals' });
}
this.notificationChannelLoading$.next(false);
}
setCommunicationDetails({
processId,
notificationChannel,
email,
mobile,
}: {
processId: number;
notificationChannel: number;
email: string;
mobile: string;
}) {
const emailValid = this.notificationsControl?.get('notificationChannel')?.get('email')?.valid;
const mobileValid = this.notificationsControl?.get('notificationChannel')?.get('mobile')?.valid;
if (notificationChannel === 3 && emailValid && mobileValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, email, mobile });
} else if (notificationChannel === 1 && emailValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, email });
} else if (notificationChannel === 2 && mobileValid) {
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, mobile });
}
}
}

View File

@@ -0,0 +1,50 @@
<h1 class="text-center text-h3 desktop:text-h2 font-normal desktop:font-bold pb-10 desktop:py-10 px-12">Überprüfen Sie die Details.</h1>
<ng-container *ngIf="payer$ | async; let payer">
<div *ngIf="!(showBillingAddress$ | async)" class="flex flex-row items-start justify-between p-5">
<div class="flex flex-row flex-wrap pr-4">
<div class="mr-3">Nachname, Vorname</div>
<div class="font-bold" *ngIf="payer">{{ payer.lastName }}, {{ payer.firstName }}</div>
</div>
<button *ngIf="payer" (click)="changeAddress()" class="text-p1 font-bold text-[#F70400]">
Ändern
</button>
</div>
</ng-container>
<ng-container *ngIf="showNotificationChannels$ | async">
<form *ngIf="control" [formGroup]="control">
<shared-notification-channel-control
[communicationDetails]="communicationDetails$ | async"
(channelActionEvent)="updateNotifications($event)"
[channelActionName]="'Speichern'"
[channelActionLoading]="notificationChannelLoading$ | async"
formGroupName="notificationChannel"
>
</shared-notification-channel-control>
</form>
</ng-container>
<ng-container *ngIf="payer$ | async; let payer">
<div *ngIf="showBillingAddress$ | async" class="flex flex-row items-start justify-between p-5">
<div class="flex flex-row flex-wrap pr-4">
<div class="mr-3">Rechnungsadresse</div>
<div class="font-bold">
{{ payer | payerAddress }}
</div>
</div>
<button *ngIf="payer" (click)="changeAddress()" class="text-p1 font-bold text-[#F70400]">
Ändern
</button>
</div>
</ng-container>
<page-special-comment
class="mb-6 mt-4"
[hasPayer]="!!(payer$ | async)"
[ngModel]="specialComment$ | async"
(ngModelChange)="setAgentComment($event)"
>
</page-special-comment>

View File

@@ -0,0 +1,3 @@
:host {
@apply desktop:bg-white box-border flex flex-col desktop:overflow-y-scroll h-auto desktop:h-[calc(100vh-15.1rem)] desktop:rounded;
}

View File

@@ -0,0 +1,138 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { emailNotificationValidator, mobileNotificationValidator } from '@shared/components/notification-channel-control';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { combineLatest } from 'rxjs';
import { CheckoutReviewStore } from '../checkout-review.store';
import { first, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators';
import { ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { Router } from '@angular/router';
import { NotificationChannel } from '@swagger/checkout';
@Component({
selector: 'page-checkout-review-details',
templateUrl: 'checkout-review-details.component.html',
styleUrls: ['checkout-review-details.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckoutReviewDetailsComponent implements OnInit {
control = this._store.notificationsControl;
customerFeatures$ = this._store.customerFeatures$;
payer$ = this._store.payer$.pipe(shareReplay());
showBillingAddress$ = this._store.shoppingCartItems$.pipe(
withLatestFrom(this.customerFeatures$),
map(
([items, customerFeatures]) =>
items.some(
(item) =>
item.features?.orderType === 'Versand' ||
item.features?.orderType === 'B2B-Versand' ||
item.features?.orderType === 'DIG-Versand'
) || !!customerFeatures?.b2b
),
shareReplay()
);
showNotificationChannels$ = combineLatest([this._store.shoppingCartItems$, this.payer$]).pipe(
map(
([items, payer]) =>
!!payer && items.some((item) => item.features?.orderType === 'Rücklage' || item.features?.orderType === 'Abholung')
)
);
notificationChannel$ = this._application.activatedProcessId$.pipe(
switchMap((processId) => this._domainCheckoutService.getNotificationChannels({ processId }))
);
communicationDetails$ = this._application.activatedProcessId$.pipe(
switchMap((processId) => this._domainCheckoutService.getBuyerCommunicationDetails({ processId })),
map((communicationDetails) => communicationDetails ?? { email: undefined, mobile: undefined })
);
specialComment$ = this._application.activatedProcessId$.pipe(
switchMap((processId) => this._domainCheckoutService.getSpecialComment({ processId }))
);
notificationChannelLoading$ = this._store.notificationChannelLoading$;
constructor(
private _fb: UntypedFormBuilder,
private _store: CheckoutReviewStore,
private _application: ApplicationService,
private _domainCheckoutService: DomainCheckoutService,
private _router: Router
) {}
async ngOnInit() {
await this.initNotificationsControl();
}
async initNotificationsControl() {
const fb = this._fb;
const notificationChannel = await this.notificationChannel$.pipe(first()).toPromise();
const communicationDetails = await this.communicationDetails$.pipe(first()).toPromise();
let selectedNotificationChannel = 0;
if ((notificationChannel & 1) === 1 && communicationDetails.email) {
selectedNotificationChannel += 1;
}
if ((notificationChannel & 2) === 2 && communicationDetails.mobile) {
selectedNotificationChannel += 2;
}
// #1967 Wenn E-Mail und SMS als NotificationChannel gesetzt sind, nur E-Mail anhaken
if ((selectedNotificationChannel & 3) === 3) {
selectedNotificationChannel = 1;
}
this.control = fb.group({
notificationChannel: new UntypedFormGroup({
selected: new UntypedFormControl(selectedNotificationChannel),
email: new UntypedFormControl(communicationDetails ? communicationDetails.email : '', emailNotificationValidator),
mobile: new UntypedFormControl(communicationDetails ? communicationDetails.mobile : '', mobileNotificationValidator),
}),
});
this._store.notificationsControl = this.control;
}
setAgentComment(agentComment: string) {
this._domainCheckoutService.setSpecialComment({ processId: this._application.activatedProcessId, agentComment });
}
updateNotifications(notificationChannels?: NotificationChannel[]) {
this._store.onNotificationChange(notificationChannels);
}
async changeAddress() {
const processId = this._application.activatedProcessId;
const customer = await this._domainCheckoutService.getBuyer({ processId }).pipe(first()).toPromise();
if (!customer) {
this.navigateToCustomerSearch(processId);
return;
}
const customerId = customer.source;
this._router.navigate(['/kunde', this._application.activatedProcessId, 'customer', `${customerId}`]);
}
async navigateToCustomerSearch(processId: number) {
try {
const response = await this.customerFeatures$
.pipe(
first(),
switchMap((customerFeatures) => {
return this._domainCheckoutService.canSetCustomer({ processId, customerFeatures });
})
)
.toPromise();
this._router.navigate(['/kunde', this._application.activatedProcessId, 'customer', 'search'], {
queryParams: { filter_customertype: response.filter.customertype },
});
} catch (error) {
this._router.navigate(['/kunde', this._application.activatedProcessId, 'customer', 'search']);
}
}
}

View File

@@ -1,5 +1,5 @@
<div class="item-thumbnail">
<a [routerLink]="['/kunde', application.activatedProcessId, 'product', 'details', 'ean', item?.product?.ean]">
<a [routerLink]="productSearchDetailsPath" [queryParams]="{ main_qs: item?.product?.ean }">
<img loading="lazy" *ngIf="item?.product?.ean | productImage; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
</a>
</div>
@@ -7,7 +7,7 @@
<div class="item-contributors">
<a
*ngFor="let contributor of contributors$ | async; let last = last"
[routerLink]="['/kunde', application.activatedProcessId, 'product', 'search', 'results']"
[routerLink]="productSearchResultsPath"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
@@ -16,16 +16,13 @@
</div>
<div
class="item-title"
[class.xl]="item?.product?.name?.length >= 35"
[class.lg]="item?.product?.name?.length >= 40"
[class.md]="item?.product?.name?.length >= 50"
[class.sm]="item?.product?.name?.length >= 60"
[class.xs]="item?.product?.name?.length >= 100"
class="item-title font-bold text-h2 mb-4"
[class.text-h3]="item?.product?.name?.length >= 40 && isTablet"
[class.text-p1]="item?.product?.name?.length >= 50 || !isTablet"
[class.text-p2]="item?.product?.name?.length >= 60 && isTablet"
[class.text-p3]="item?.product?.name?.length >= 100"
>
<a [routerLink]="['/kunde', application.activatedProcessId, 'product', 'details', 'ean', item?.product?.ean]">{{
item?.product?.name
}}</a>
<a [routerLink]="productSearchDetailsPath" [queryParams]="{ main_qs: item?.product?.ean }">{{ item?.product?.name }}</a>
</div>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
@@ -37,10 +34,12 @@
{{ item?.product?.formatDetail }}
</div>
<div class="item-info">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ item?.product?.publicationDate | date }}
<div class="item-info text-p2">
<div class="mb-1">{{ item?.product?.manufacturer | substr: 25 }} | {{ item?.product?.ean }}</div>
<div class="mb-1">
{{ 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'">
@@ -57,9 +56,9 @@
</div>
</div>
<div class="item-price-stock">
<div>{{ item?.availability?.price?.value?.value | currency: 'EUR':'code' }}</div>
<div>
<div class="item-price-stock flex flex-col">
<div class="text-p2 font-bold">{{ item?.availability?.price?.value?.value | currency: 'EUR':'code' }}</div>
<div class="text-p2 font-normal">
<ui-quantity-dropdown
*ngIf="!(isDummy$ | async); else quantityDummy"
[ngModel]="item?.quantity"
@@ -70,7 +69,7 @@
>
</ui-quantity-dropdown>
<ng-template #quantityDummy>
{{ item?.quantity }}
<div class="mt-2">{{ item?.quantity }}x</div>
</ng-template>
</div>
<div class="quantity-error" *ngIf="quantityError">
@@ -85,7 +84,7 @@
*ngIf="!(hasOrderType$ | async)"
>
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">
Auswählen
Lieferung Auswählen
</ui-spinner>
</button>
<button

View File

@@ -1,15 +1,12 @@
:host {
@apply text-black no-underline grid p-4;
grid-template-columns: 102px 50% auto;
@apply text-black no-underline grid p-5 gap-x-4 gap-y-1;
grid-template-columns: 3.75rem auto;
grid-template-rows: auto;
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-contributors item-price-stock'
'item-thumbnail item-title item-price-stock'
'item-thumbnail item-format item-price-stock'
'item-thumbnail item-info actions'
'item-thumbnail item-date actions'
'item-thumbnail item-ssc actions'
'item-thumbnail item-availability actions';
'item-thumbnail item-format item-format'
'item-thumbnail item-info actions';
}
button {
@@ -18,61 +15,35 @@ button {
.item-thumbnail {
grid-area: item-thumbnail;
width: 70px;
@apply mr-8;
@apply mr-8 w-[3.75rem] h-[5.9375rem];
img {
max-width: 100%;
max-height: 150px;
@apply rounded shadow-cta;
@apply w-[3.75rem] h-[5.9375rem] rounded shadow-cta;
}
}
.item-contributors {
grid-area: item-contributors;
height: 22px;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
white-space: nowrap;
a {
@apply text-active-customer font-bold no-underline;
@apply text-[#0556B4] font-bold no-underline;
}
}
.item-title {
grid-area: item-title;
@apply font-bold text-lg mb-4;
max-height: 64px;
a {
@apply text-active-customer no-underline;
@apply text-black no-underline;
}
}
.item-title.xl {
@apply font-bold text-xl;
}
.item-title.lg {
@apply font-bold text-lg;
}
.item-title.md {
@apply font-bold text-p2;
}
.item-title.sm {
@apply font-bold text-p3;
}
.item-title.xs {
@apply font-bold text-xs;
}
.item-format {
grid-area: item-format;
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
@apply flex flex-row items-center font-bold text-p2 whitespace-nowrap;
img {
@apply mr-2;
@@ -81,7 +52,7 @@ button {
.item-price-stock {
grid-area: item-price-stock;
@apply font-bold text-xl text-right;
@apply text-right;
.quantity {
@apply flex flex-row justify-end items-center;
@@ -92,7 +63,7 @@ button {
}
ui-quantity-dropdown {
@apply flex justify-end mt-2;
@apply flex justify-end mt-2 font-normal;
}
}
@@ -101,7 +72,7 @@ button {
@apply flex flex-row justify-end items-baseline font-bold text-lg;
ui-icon {
@apply text-active-customer mr-2;
@apply text-active-customer mr-1;
}
}
@@ -117,35 +88,8 @@ button {
}
}
.item-availability {
@apply flex flex-row items-center mt-4;
grid-area: item-availability;
.fetching {
@apply w-52 h-px-20;
background-color: #e6eff9;
animation: load 0.75s linear infinite;
}
span {
@apply mr-4;
}
ui-icon {
@apply text-dark-cerulean mx-1;
}
div {
@apply ml-2 flex items-center;
}
.truck {
@apply -mb-px-5 -mt-px-5;
}
}
.actions {
@apply flex items-center justify-end;
@apply flex items-end justify-end;
grid-area: actions;
button {
@@ -156,3 +100,9 @@ button {
}
}
}
::ng-deep page-shopping-cart-item ui-quantity-dropdown {
.current-quantity {
font-weight: normal !important;
}
}

View File

@@ -1,8 +1,10 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { ComponentStore } from '@ngrx/component-store';
import { ProductCatalogNavigationService } from '@shared/services';
import { ItemType, ShoppingCartItemDTO } from '@swagger/checkout';
import { combineLatest } from 'rxjs';
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
@@ -110,10 +112,27 @@ export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemCo
.getOlaErrors({ processId: this.application.activatedProcessId })
.pipe(map((ids) => ids?.find((id) => id === this.item.id)));
get productSearchResultsPath() {
return this._productNavigationService.getArticleSearchResultsPath(this.application.activatedProcessId);
}
get productSearchDetailsPath() {
return this._productNavigationService.getArticleDetailsPath({
processId: this.application.activatedProcessId,
ean: this.item?.product?.ean,
});
}
get isTablet() {
return this._environment.matchTablet();
}
constructor(
private availabilityService: DomainAvailabilityService,
private checkoutService: DomainCheckoutService,
public application: ApplicationService
public application: ApplicationService,
private _productNavigationService: ProductCatalogNavigationService,
private _environment: EnvironmentService
) {
super({ item: undefined, orderType: '' });
}

View File

@@ -1,17 +1,37 @@
<label for="agent-comment">Anmerkung</label>
<textarea
#input
type="text"
id="agent-comment"
name="agent-comment"
[(ngModel)]="value"
[rows]="rows"
(ngModelChange)="check()"
(blur)="save()"
></textarea>
<div class="page-special-comment__wrapper flex flex-col items-start px-5">
<div class="mb-[0.375rem]">Anmerkung</div>
<div class="action-wrapper">
<button type="button" *ngIf="!disabled && !!value" (click)="clear()">
<ui-icon icon="close" size="14px"></ui-icon>
</button>
<div class="flex flex-row w-full mb-[0.375rem]">
<textarea
#input
matInput
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
maxlength="200"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
#specialCommentInput
(keydown.delete)="triggerResize()"
(keydown.backspace)="triggerResize()"
type="text"
id="agent-comment"
name="agent-comment"
placeholder="Eine Anmerkung hinzufügen"
[(ngModel)]="value"
[rows]="rows"
(ngModelChange)="check()"
(blur)="save()"
></textarea>
<div class="comment-actions py-4">
<button type="reset" class="clear pl-4" *ngIf="!disabled && !!value" (click)="clear(); triggerResize()">
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
<button class="cta-save ml-4" type="submit" *ngIf="!disabled && isDirty" (click)="save()">
Speichern
</button>
</div>
</div>
<div *ngIf="!hasPayer" class="text-p3">Zur Info: Sie haben dem Warenkorb noch keinen Kunden hinzugefügt.</div>
</div>

View File

@@ -1,25 +1,45 @@
:host {
@apply flex flex-row box-border p-4 items-center;
}
.page-special-comment__wrapper {
textarea {
@apply w-full flex-grow font-bold rounded bg-[#EDEFF0] border-[#AEB7C1] border border-solid outline-none text-p2 p-4;
resize: none;
height: auto !important;
}
.action-wrapper {
@apply self-start flex flex-row items-center;
height: 28px;
}
textarea.inactive {
@apply text-warning font-bold;
@apply w-full flex-grow rounded bg-[#EDEFF0] border-[#AEB7C1] border border-solid outline-none text-p2 p-4 text-warning font-bold;
// ipad color fix
-webkit-text-fill-color: rgb(190, 129, 0);
opacity: 1;
}
label {
@apply font-bold self-start;
min-width: 200px;
}
textarea::placeholder,
textarea.inactive::placeholder {
@apply text-[#89949E] font-normal;
-webkit-text-fill-color: #89949e;
}
textarea {
@apply flex-grow text-p2 border-none outline-none p-0 resize-none;
}
input {
@apply flex-grow bg-transparent border-none outline-none text-p2 mx-4;
}
button {
@apply text-brand font-bold text-p1 outline-none border-none bg-transparent ml-1;
input.inactive {
@apply text-warning font-bold;
@apply flex-grow bg-transparent border-none outline-none text-p2 mx-4 text-warning font-bold;
// ipad color fix
-webkit-text-fill-color: rgb(190, 129, 0);
opacity: 1;
}
ui-icon {
@apply text-ucla-blue;
button {
@apply bg-transparent text-brand font-bold text-p1 outline-none border-none;
}
button.clear {
@apply text-black;
}
.comment-actions {
@apply flex justify-center items-center;
}
}

View File

@@ -1,13 +1,5 @@
import {
Component,
ChangeDetectionStrategy,
EventEmitter,
ViewChild,
ElementRef,
ChangeDetectorRef,
forwardRef,
Output,
} from '@angular/core';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { Component, ChangeDetectionStrategy, EventEmitter, ViewChild, ChangeDetectorRef, forwardRef, Output, Input } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
@Component({
@@ -18,6 +10,8 @@ import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SpecialCommentComponent), multi: true }],
})
export class SpecialCommentComponent implements ControlValueAccessor {
@ViewChild('autosize') autosize: CdkTextareaAutosize;
private initialValue = '';
value = '';
@@ -33,6 +27,9 @@ export class SpecialCommentComponent implements ControlValueAccessor {
isDirty = false;
@Input()
hasPayer: boolean;
@Output()
isDirtyChange = new EventEmitter<boolean>();
@@ -89,4 +86,8 @@ export class SpecialCommentComponent implements ControlValueAccessor {
this.isDirty = isDirty;
this.isDirtyChange.emit(isDirty);
}
triggerResize() {
this.autosize.reset();
}
}

View File

@@ -62,10 +62,15 @@
<ng-container *ngFor="let order of displayOrder.items">
<div class="row between">
<div class="product-name">
<img class="thumbnail" [src]="order.product?.ean | productImage: 30:50:true" />
<a class="name" [routerLink]="['/kunde', processId, 'product', 'details', 'ean', order?.product?.ean]">{{
order?.product?.name
}}</a>
<a [routerLink]="getProductSearchDetailsPath(order?.product?.ean)" [queryParams]="{ main_qs: order?.product?.ean }">
<img class="thumbnail" [src]="order.product?.ean | productImage: 30:50:true" />
</a>
<a
class="name"
[routerLink]="getProductSearchDetailsPath(order?.product?.ean)"
[queryParams]="{ main_qs: order?.product?.ean }"
>{{ order?.product?.name }}</a
>
</div>
<div class="product-details">

View File

@@ -13,6 +13,7 @@ import { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer';
import { BehaviorSubject, combineLatest, NEVER, of, Subject } from 'rxjs';
import { DateAdapter } from '@ui/common';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
@Component({
selector: 'page-checkout-summary',
@@ -122,7 +123,9 @@ export class CheckoutSummaryComponent implements OnDestroy {
private breadcrumb: BreadcrumbService,
public applicationService: ApplicationService,
private domainPrinterService: DomainPrinterService,
private dateAdapter: DateAdapter
private dateAdapter: DateAdapter,
private _navigation: CheckoutNavigationService,
private _productNavigationService: ProductCatalogNavigationService
) {
this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['checkout'])
@@ -134,7 +137,10 @@ export class CheckoutSummaryComponent implements OnDestroy {
this.breadcrumb.addBreadcrumbIfNotExists({
key: this.applicationService.activatedProcessId,
name: 'Bestellbestätigung',
path: `/kunde/${this.applicationService.activatedProcessId}/cart/summary/${this._route.snapshot.params.orderIds}`,
path: this._navigation.getCheckoutSummaryPath({
processId: this.applicationService.activatedProcessId,
orderIds: this._route.snapshot.params.orderIds,
}),
tags: ['checkout', 'cart'],
section: 'customer',
});
@@ -156,6 +162,13 @@ export class CheckoutSummaryComponent implements OnDestroy {
this._onDestroy$.complete();
}
getProductSearchDetailsPath(ean: string) {
return this._productNavigationService.getArticleDetailsPath({
processId: this.processId,
ean,
});
}
openPrintModal(id: number) {
this.uiModal.open({
content: PrintModalComponent,

View File

@@ -3,6 +3,30 @@ import { RouterModule, Routes } from '@angular/router';
import { CheckoutReviewComponent } from './checkout-review/checkout-review.component';
import { CheckoutSummaryComponent } from './checkout-summary/checkout-summary.component';
import { PageCheckoutComponent } from './page-checkout.component';
import { CheckoutReviewDetailsComponent } from './checkout-review/details/checkout-review-details.component';
const auxiliaryRoutes = [
{
path: 'details',
component: CheckoutReviewDetailsComponent,
outlet: 'left',
},
{
path: 'review',
component: CheckoutReviewComponent,
outlet: 'right',
},
{
path: 'summary',
component: CheckoutSummaryComponent,
outlet: 'main',
},
{
path: 'summary/:orderIds',
component: CheckoutSummaryComponent,
outlet: 'main',
},
];
const routes: Routes = [
{
@@ -12,6 +36,7 @@ const routes: Routes = [
{ path: 'summary', component: CheckoutSummaryComponent },
{ path: 'summary/:orderIds', component: CheckoutSummaryComponent },
{ path: 'review', component: CheckoutReviewComponent },
...auxiliaryRoutes,
{ path: '', pathMatch: 'full', redirectTo: 'review' },
],
},

View File

@@ -1 +1,23 @@
<shared-breadcrumb [key]="breadcrumbKey$ | async" [tags]="['checkout']"></shared-breadcrumb> <router-outlet></router-outlet>
<shared-breadcrumb class="my-4" [key]="breadcrumbKey$ | async" [tags]="['checkout']"></shared-breadcrumb>
<ng-container *ngIf="routerEvents$ | async">
<ng-container *ngIf="!(isDesktop$ | async); else desktop">
<router-outlet></router-outlet>
</ng-container>
<ng-template #desktop>
<ng-container *ngIf="showMainOutlet$ | async">
<router-outlet name="main"></router-outlet>
</ng-container>
<div class="grid grid-cols-[minmax(31rem,.5fr)_1fr] gap-6">
<div *ngIf="showLeftOutlet$ | async" class="block">
<router-outlet name="left"></router-outlet>
</div>
<div *ngIf="showRightOutlet$ | async">
<router-outlet name="right"></router-outlet>
</div>
</div>
</ng-template>
</ng-container>

View File

@@ -1,6 +1,8 @@
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { map } from 'rxjs/operators';
import { EnvironmentService } from '@core/environment';
import { map, shareReplay } from 'rxjs/operators';
@Component({
selector: 'page-checkout',
@@ -11,7 +13,29 @@ import { map } from 'rxjs/operators';
export class PageCheckoutComponent implements OnInit {
readonly breadcrumbKey$ = this.applicationService.activatedProcessId$.pipe(map((processId) => String(processId)));
constructor(private applicationService: ApplicationService) {}
get isDesktop$() {
return this._environmentService.matchDesktop$.pipe(
map((state) => {
return state.matches;
}),
shareReplay()
);
}
routerEvents$ = this._router.events.pipe(shareReplay());
showMainOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'main')));
showLeftOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'left')));
showRightOutlet$ = this.routerEvents$.pipe(map((_) => !!this._activatedRoute?.children?.find((child) => child?.outlet === 'right')));
constructor(
private applicationService: ApplicationService,
private _environmentService: EnvironmentService,
private _router: Router,
private _activatedRoute: ActivatedRoute
) {}
ngOnInit() {}
}

View File

@@ -197,6 +197,7 @@
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
maxlength="200"
#specialCommentInput
(keydown.delete)="triggerResize()"
(keydown.backspace)="triggerResize()"

View File

@@ -28,6 +28,7 @@ import { catchError, first, take, map, shareReplay, switchMap, tap, takeUntil }
import { CantAddCustomerToCartModalComponent } from '../modals/cant-add-customer-to-cart-modal/cant-add-customer-to-cart.component';
import { CantAddCustomerToCartData } from '../modals/cant-add-customer-to-cart-modal/cant-add-customer-to-cart.data';
import { CantSelectGuestModalComponent } from '../modals/cant-select-guest/cant-select-guest-modal.component';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
@Component({
selector: 'page-customer-details',
@@ -81,7 +82,9 @@ export class CustomerDetailsComponent implements OnInit, OnDestroy {
private checkoutService: DomainCheckoutService,
private router: Router,
private modal: UiModalService,
private cdr: ChangeDetectorRef
private cdr: ChangeDetectorRef,
private _productNavigationService: ProductCatalogNavigationService,
private _checkoutNavigationService: CheckoutNavigationService
) {}
ngOnInit() {
@@ -409,9 +412,9 @@ export class CustomerDetailsComponent implements OnInit, OnDestroy {
// Navigate To Catalog Or Cart
if (await this.cartExists$.pipe(first()).toPromise()) {
this.router.navigate(['/kunde', this.application.activatedProcessId, 'cart', 'review']);
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: this.application.activatedProcessId });
} else {
this.router.navigate(['/kunde', this.application.activatedProcessId, 'product', 'search']);
await this._productNavigationService.navigateToProductSearch({ processId: this.application.activatedProcessId });
}
this.showSpinner = false;

View File

@@ -1,5 +1,5 @@
<div class="nc-heading">
<label for="notificationChannel">
<label class="mr-10" for="notificationChannel">
Benachrichtigung
</label>
<ui-checkbox-group
@@ -12,21 +12,21 @@
</ui-checkbox-group>
<div class="expand"></div>
<button *ngIf="displayToggle" type="button" class="more-toggle" (click)="toggle()" [class.open]="open$ | async">
<ui-icon icon="arrow_head" size="16px"></ui-icon>
<shared-icon icon="chevron-right" [size]="24"></shared-icon>
</button>
</div>
<div class="nc-content" [class.open]="open$ | async">
<div class="nc-control-wrapper" *ngIf="displayEmail && open$ | async">
<label for="email">E-Mail</label>
<div class="nc-control-wrapper mb-3" *ngIf="displayEmail && open$ | async">
<div class="input-wrapper" [class.has-error]="emailControl.touched && emailControl?.errors">
<input type="email" name="email" id="email" [formControl]="emailControl" placeholder="E-Mail*" />
<input autocomplete="off" type="email" name="email" id="email" [formControl]="emailControl" placeholder="E-Mail*" />
<ng-container *ngIf="emailControl.touched && emailControl?.errors; let errors">
<span class="error" *ngIf="errors.required">Das Feld E-Mail ist ein Pflichtfeld</span>
<span class="error" *ngIf="errors.pattern">Keine gültige E-Mail Adresse</span>
</ng-container>
</div>
<div class="nc-generate" *ngIf="channelActionName && notificationChannels.length !== 2">
<div class="pl-4" *ngIf="channelActionName && notificationChannels.length !== 2">
<button
class="text-p1 font-bold text-brand outline-none border-none bg-transparent right-0"
[disabled]="channelActionLoading || emailControl?.errors?.required || emailControl?.errors?.pattern"
type="button"
(click)="channelActionEvent.emit(notificationChannels)"
@@ -37,16 +37,16 @@
</div>
<div class="nc-control-wrapper" *ngIf="displayMobile && open$ | async">
<label for="mobile">SMS</label>
<div class="input-wrapper" [class.has-error]="mobileControl.touched && mobileControl?.errors">
<input type="tel" name="mobile" id="mobile" [formControl]="mobileControl" placeholder="SMS*" />
<input autocomplete="off" type="tel" name="mobile" id="mobile" [formControl]="mobileControl" placeholder="SMS*" />
<ng-container *ngIf="mobileControl.touched && mobileControl?.errors; let errors">
<span class="error" *ngIf="errors.required">Das Feld SMS ist ein Pflichtfeld</span>
<span class="error" *ngIf="errors.pattern">Keine gültige Mobilnummer</span>
</ng-container>
</div>
<div class="nc-generate" *ngIf="channelActionName">
<div class="pl-4" *ngIf="channelActionName">
<button
class="text-p1 font-bold text-brand outline-none border-none bg-transparent right-0"
[disabled]="
channelActionLoading ||
mobileControl?.errors?.required ||

View File

@@ -3,29 +3,17 @@
}
.nc-heading {
@apply flex flex-row items-center px-4 py-6 bg-white;
label {
@apply font-bold;
width: 200px;
}
@apply flex flex-row items-center px-5 pb-3 bg-white;
.expand {
@apply flex-grow;
}
.more-toggle {
@apply bg-transparent outline-none border-none;
margin-top: 2px;
ui-icon {
@apply transition-all transform rotate-90;
}
@apply bg-transparent outline-none border-none transition-all transform -rotate-90;
&.open {
ui-icon {
@apply -rotate-90;
}
@apply rotate-90;
}
}
}
@@ -39,22 +27,17 @@
}
.nc-control-wrapper {
@apply flex flex-row items-start pb-6;
label {
@apply mt-1 font-bold;
width: 200px;
}
@apply flex flex-row items-center justify-center;
.input-wrapper {
@apply flex flex-col flex-grow pb-1;
@apply flex flex-col flex-grow;
input {
@apply flex-grow outline-none text-p2 font-bold border-0 border-b-2 border-solid pb-px-2;
@apply w-full flex-grow font-bold rounded bg-[#EDEFF0] border-[#AEB7C1] border border-solid outline-none text-p1 p-4;
}
input:disabled {
@apply bg-white;
input::placeholder {
@apply text-[#89949E] font-normal text-p2;
}
&.has-error input {
@@ -67,44 +50,18 @@
}
}
.nc-generate {
button {
@apply text-p2 font-bold text-brand outline-none border-none bg-transparent mr-4 absolute right-0;
}
button:disabled {
@apply text-[#596470] cursor-not-allowed;
}
::ng-deep .customer shared-notification-channel-control {
.nc-control-wrapper {
.input-wrapper {
input {
@apply border-glitter;
}
}
::ng-deep shared-notification-channel-control ui-checkbox {
ui-icon {
@apply rounded;
color: #aeb7c1 !important;
}
.nc-generate {
button:disabled {
@apply text-disabled-customer cursor-not-allowed;
}
}
}
::ng-deep .branch shared-notification-channel-control {
.nc-control-wrapper {
.input-wrapper {
input {
@apply border-munsell;
}
ui-icon {
@apply text-white;
}
}
}
.nc-generate {
button:disabled {
@apply text-disabled-branch cursor-not-allowed;
}
ui-icon[icon='checked'] {
color: white !important;
background-color: #596470 !important;
}
}

View File

@@ -2,12 +2,12 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { UiCheckboxModule } from '@ui/checkbox';
import { UiIconModule } from '@ui/icon';
import { SharedNotificationChannelControlComponent } from './notification-channel-control.component';
import { IconModule } from '@shared/components/icon';
@NgModule({
declarations: [SharedNotificationChannelControlComponent],
imports: [CommonModule, UiCheckboxModule, FormsModule, ReactiveFormsModule, UiIconModule],
imports: [CommonModule, IconModule, UiCheckboxModule, FormsModule, ReactiveFormsModule],
exports: [SharedNotificationChannelControlComponent],
})
export class SharedNotificationChannelControlModule {}

View File

@@ -0,0 +1,15 @@
import { TestBed } from '@angular/core/testing';
import { CheckoutNavigationService } from './checkout-navigation.service';
xdescribe('CheckoutNavigationService', () => {
let service: CheckoutNavigationService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CheckoutNavigationService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,63 @@
import { Injectable } from '@angular/core';
import { NavigationService } from './navigation.service';
import { Router } from '@angular/router';
import { EnvironmentService } from '@core/environment';
import { UiModalService } from '@ui/modal';
@Injectable({ providedIn: 'root' })
export class CheckoutNavigationService extends NavigationService {
constructor(_router: Router, _environment: EnvironmentService, _uiModal: UiModalService) {
super(_router, _environment, _uiModal);
}
getCheckoutReviewPath(processId: number): any[] {
return ['/kunde', processId, 'cart', { outlets: { primary: 'review', main: null, left: 'details', right: 'review' } }];
}
getCheckoutSummaryPath({ processId, orderIds }: { processId: number; orderIds?: string }): any[] {
if (!!orderIds) {
return [
'/kunde',
processId,
'cart',
{ outlets: { primary: ['summary', orderIds], main: ['summary', orderIds], left: null, right: null } },
];
} else {
return ['/kunde', processId, 'cart', { outlets: { primary: 'summary', main: 'summary', left: null, right: null } }];
}
}
async navigateToCheckoutReview({
processId,
queryParams,
queryParamsHandling,
}: {
processId: number;
queryParams?: Record<string, string>;
queryParamsHandling?: 'merge' | 'preserve' | '';
}) {
await this._navigateTo({
routerLink: this.getCheckoutReviewPath(processId),
queryParams,
queryParamsHandling,
});
}
async navigateToCheckoutSummary({
processId,
orderIds,
queryParams,
queryParamsHandling,
}: {
processId: number;
orderIds?: string;
queryParams?: Record<string, string>;
queryParamsHandling?: 'merge' | 'preserve' | '';
}) {
await this._navigateTo({
routerLink: this.getCheckoutSummaryPath({ processId, orderIds }),
queryParams,
queryParamsHandling,
});
}
}

View File

@@ -5,5 +5,6 @@
export * from './lib/base-navigation.service';
export * from './lib/product-catalog-navigation.service';
export * from './lib/customer-orders-navigation.service';
export * from './lib/checkout-navigation.service';
export * from './lib/navigation.service';
export * from './lib/defs';

View File

@@ -16,7 +16,7 @@
type="button"
class="rounded-full px-3 h-[2.375rem] font-bold text-p1 flex flex-row items-center justify-between shopping-cart-count ml-4"
[class.active]="isActive$ | async"
[routerLink]="['/kunde', (process$ | async)?.id, 'cart', 'review']"
[routerLink]="getCheckoutPath((process$ | async)?.id)"
(click)="$event?.preventDefault(); $event?.stopPropagation()"
>
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>

View File

@@ -9,13 +9,14 @@ import { Router } from '@angular/router';
import { MockComponent } from 'ng-mocks';
import { UISvgIconComponent } from '@ui/icon';
import { DomainCheckoutService } from '@domain/checkout';
import { UiModalService } from '@ui/modal';
describe('ShellProcessBarItemComponent', () => {
let testScheduler: TestScheduler;
let spectator: Spectator<ShellProcessBarItemComponent>;
const createComponent = createComponentFactory({
component: ShellProcessBarItemComponent,
mocks: [DomainCheckoutService],
mocks: [DomainCheckoutService, UiModalService],
imports: [RouterTestingModule],
declarations: [MockComponent(UISvgIconComponent)],
});

View File

@@ -13,6 +13,7 @@ import { Router } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { Breadcrumb, BreadcrumbService } from '@core/breadcrumb';
import { DomainCheckoutService } from '@domain/checkout';
import { CheckoutNavigationService } from '@shared/services';
import { BehaviorSubject, NEVER, Observable, combineLatest, isObservable } from 'rxjs';
import { first, map, switchMap } from 'rxjs/operators';
@@ -51,7 +52,8 @@ export class ShellProcessBarItemComponent implements OnInit, OnDestroy, OnChange
private _breadcrumb: BreadcrumbService,
private _app: ApplicationService,
private _router: Router,
private _checkout: DomainCheckoutService
private _checkout: DomainCheckoutService,
private _checkoutNavigationService: CheckoutNavigationService
) {}
ngOnChanges({ process }: SimpleChanges): void {
@@ -69,6 +71,10 @@ export class ShellProcessBarItemComponent implements OnInit, OnDestroy, OnChange
this.initCartItemCount$();
}
getCheckoutPath(processId: number) {
return this._checkoutNavigationService.getCheckoutReviewPath(processId);
}
initLatestBreadcrumb$() {
this.latestBreadcrumb$ = this.process$.pipe(switchMap((process) => this._breadcrumb.getLastActivatedBreadcrumbByKey$(process?.id)));
}