mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-31 09:37:15 +01:00
committed by
Lorenz Hilpert
parent
8a4fe7aedd
commit
fc76f34d38
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '' });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@
|
||||
#autosize="cdkTextareaAutosize"
|
||||
cdkAutosizeMinRows="1"
|
||||
cdkAutosizeMaxRows="5"
|
||||
maxlength="200"
|
||||
#specialCommentInput
|
||||
(keydown.delete)="triggerResize()"
|
||||
(keydown.backspace)="triggerResize()"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
63
apps/shared/services/src/lib/checkout-navigation.service.ts
Normal file
63
apps/shared/services/src/lib/checkout-navigation.service.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)],
|
||||
});
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user