Compare commits

...

14 Commits

Author SHA1 Message Date
Nino Righi
d303b1444b Zoom Update and Article Result List Styling Update 2023-04-03 17:54:08 +02:00
Nino Righi
150e7965ee Zwischencommit 2023-03-29 12:57:05 +02:00
Nino Righi
3fcf3d9396 Responsive Design Splitscreen Article Search Results Update 2023-03-28 15:08:31 +02:00
Nino Righi
52278b8baf Initial Responsive Design Implementation based on Article Search 2023-03-24 17:19:59 +01:00
Lorenz Hilpert
1de342fd3b Update Add To Shopping Cart from PDP
(cherry picked from commit 4a5dd23561fdec458447c19c93faa1654ab80c7c)
2023-03-21 11:21:35 +01:00
Lorenz Hilpert
f4c1c3dd7f Merged PR 1513: Kaufoptionen
Related work items: #3365, #3366, #3385, #3386, #3391
2023-03-20 17:11:53 +00:00
Nino Righi
80bfc59356 Merged PR 1500: #3506 Remission Wanne Scanner nach Start der Remission
#3506 Remission Wanne Scanner nach Start der Remission
2023-03-17 09:25:43 +00:00
Nino Righi
ef967b66e8 Merged PR 1512: #3887 Removed Scrollbar until Responsive Design
#3887 Removed Scrollbar until Responsive Design
2023-03-16 13:59:47 +00:00
Nino Righi
e4823950df Merged PR 1511: #3887 HSC Details Kundenbestellungen 360 Grad
#3887 HSC Details Kundenbestellungen 360 Grad
2023-03-15 16:41:57 +00:00
Lorenz Hilpert
8f9923ba5d #3673 ArrivalStatus Prüfung 2023-03-14 18:22:17 +01:00
Lorenz Hilpert
72bbd2c36e Checkbox Styling with Sub Options 2023-03-14 14:15:07 +01:00
Lorenz Hilpert
9bdb902a56 #3673 #3905 2023-03-14 13:55:38 +01:00
Nino Righi
bbc2e55ae3 Merged PR 1510: #3878 Updated Section Toggle if Role CallCenter is Active
#3878 Updated Section Toggle if Role CallCenter is Active
2023-03-14 09:16:44 +00:00
Nino Righi
6995bdb527 Merged PR 1508: Filter Doku Readme angelegt mit 2 konkreten Anwendungsbeispielen der Filter I...
Filter Doku Readme angelegt mit 2 konkreten Anwendungsbeispielen der Filter IST Implementierung

Co-authored-by: lorenzh <lorenzh@users.noreply.github.com>
2023-03-13 15:43:59 +00:00
176 changed files with 6471 additions and 6300 deletions

View File

@@ -34,6 +34,11 @@ export class DomainAvailabilityService {
private _branchService: StoreCheckoutBranchService
) {}
@memorize({ ttl: 10000 })
memorizedAvailabilityShippingAvailability(request: Array<AvailabilityRequestDTO>) {
return this._availabilityService.AvailabilityShippingAvailability(request).pipe(shareReplay(1));
}
@memorize()
getSuppliers(): Observable<SupplierDTO[]> {
return this._supplierService.StoreCheckoutSupplierGetSuppliers({}).pipe(
@@ -249,57 +254,53 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this._availabilityService
.AvailabilityShippingAvailability([
{
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
qty: quantity,
},
])
.pipe(
timeout(5000),
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
shareReplay(1)
);
return this.memorizedAvailabilityShippingAvailability([
{
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
qty: quantity,
},
]).pipe(
timeout(5000),
map((r) => this._mapToShippingAvailability(r.result)?.find((_) => true)),
shareReplay(1)
);
}
@memorize({ ttl: 10000 })
getDigDeliveryAvailability({ item, quantity }: { item: ItemData; quantity: number }): Observable<AvailabilityDTO> {
return this._availabilityService
.AvailabilityShippingAvailability([
{
qty: quantity,
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
},
])
.pipe(
timeout(5000),
map((r) => {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
return this.memorizedAvailabilityShippingAvailability([
{
qty: quantity,
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
},
]).pipe(
timeout(5000),
map((r) => {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
estimatedDelivery: preferred?.estimatedDelivery,
price: preferred?.price,
logistician: { id: preferred?.logisticianId },
supplierProductNumber: preferred?.supplierProductNumber,
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
shareReplay(1)
);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
estimatedDelivery: preferred?.estimatedDelivery,
price: preferred?.price,
logistician: { id: preferred?.logisticianId },
supplierProductNumber: preferred?.supplierProductNumber,
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
shareReplay(1)
);
}
@memorize({ ttl: 10000 })
@@ -333,37 +334,35 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDownloadAvailability({ item }: { item: ItemData }): Observable<AvailabilityDTO> {
return this._availabilityService
.AvailabilityShippingAvailability([
{
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
qty: 1,
},
])
.pipe(
map((r) => {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
return this.memorizedAvailabilityShippingAvailability([
{
ean: item?.ean,
itemId: item?.itemId ? String(item?.itemId) : null,
price: item?.price,
qty: 1,
},
]).pipe(
map((r) => {
const availabilities = r.result;
const preferred = availabilities?.find((f) => f.preferred === 1);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
price: preferred?.price,
supplierProductNumber: preferred?.supplierProductNumber,
logistician: { id: preferred?.logisticianId },
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
shareReplay(1)
);
const availability: AvailabilityDTO = {
availabilityType: preferred?.status,
ssc: preferred?.ssc,
sscText: preferred?.sscText,
supplier: { id: preferred?.supplierId },
isPrebooked: preferred?.isPrebooked,
estimatedShippingDate: preferred?.requestStatusCode === '32' ? preferred?.altAt : preferred?.at,
price: preferred?.price,
supplierProductNumber: preferred?.supplierProductNumber,
logistician: { id: preferred?.logisticianId },
supplierInfo: preferred?.requestStatusCode,
lastRequest: preferred?.requested,
};
return availability;
}),
shareReplay(1)
);
}
@memorize({ ttl: 10000 })
@@ -401,7 +400,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result))
);
@@ -409,7 +408,7 @@ export class DomainAvailabilityService {
@memorize({ ttl: 10000 })
getDigDeliveryAvailabilities(payload: AvailabilityRequestDTO[]) {
return this._availabilityService.AvailabilityShippingAvailability(payload).pipe(
return this.memorizedAvailabilityShippingAvailability(payload).pipe(
timeout(20000),
map((response) => this._mapToShippingAvailability(response.result))
);
@@ -447,6 +446,9 @@ export class DomainAvailabilityService {
}
isAvailable({ availability }: { availability: AvailabilityDTO }) {
if (availability?.supplier?.id === 16 && availability?.inStock == 0) {
return false;
}
return [2, 32, 256, 1024, 2048, 4096].some((code) => availability?.availabilityType === code);
}

View File

@@ -26,6 +26,7 @@ import {
StoreCheckoutBuyerService,
StoreCheckoutPayerService,
StoreCheckoutBranchService,
ItemsResult,
} from '@swagger/checkout';
import { DisplayOrderDTO, DisplayOrderItemDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
import { isNullOrUndefined, memorize } from '@utils/common';
@@ -198,7 +199,15 @@ export class DomainCheckoutService {
);
}
canAddItems({ processId, payload, orderType }: { processId: number; payload: ItemPayload[]; orderType: string }) {
canAddItems({
processId,
payload,
orderType,
}: {
processId: number;
payload: ItemPayload[];
orderType: string;
}): Observable<ItemsResult[]> {
return this.getShoppingCart({ processId }).pipe(
first(),
withLatestFrom(this.store.select(DomainCheckoutSelectors.selectCustomerFeaturesByProcessId, { processId })),
@@ -217,7 +226,8 @@ export class DomainCheckoutService {
})
.pipe(
map((response) => {
return response.result;
// TODO: remove this when the API is fixed
return (response.result as unknown) as ItemsResult[];
})
);
})

View File

@@ -107,6 +107,25 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
aliases: [
{ alias: 'd-account', name: 'account' },
{ alias: 'd-no-account', name: 'package-variant-closed' },
{ name: 'isa-audio', alias: 'AU' },
{ name: 'isa-audio', alias: 'CAS' },
{ name: 'isa-audio', alias: 'DL' },
{ name: 'isa-audio', alias: 'KAS' },
{ name: 'isa-hard-cover', alias: 'BUCH' },
{ name: 'isa-hard-cover', alias: 'GEB' },
{ name: 'isa-hard-cover', alias: 'HC' },
{ name: 'isa-hard-cover', alias: 'KT' },
{ name: 'isa-ebook', alias: 'EB' },
{ name: 'isa-non-book', alias: 'GLO' },
{ name: 'isa-non-book', alias: 'HDL' },
{ name: 'isa-non-book', alias: 'NB' },
{ name: 'isa-non-book', alias: 'SPL' },
{ name: 'isa-calendar', alias: 'KA' },
{ name: 'isa-scroll', alias: 'MA' },
{ name: 'isa-software', alias: 'SW' },
{ name: 'isa-soft-cover', alias: 'TB' },
{ name: 'isa-video', alias: 'VI' },
{ name: 'isa-news-paper', alias: 'ZS' },
],
}),
],

View File

@@ -1,14 +1,20 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainCheckoutService } from '@domain/checkout';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateProductGuard implements CanActivate {
get isTablet() {
return this._environment.isTablet();
}
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _environment: EnvironmentService,
private readonly _router: Router
) {}
@@ -57,7 +63,17 @@ export class CanActivateProductGuard implements CanActivate {
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
});
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
if (this.isTablet) {
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(newProcessId)]));
} else {
// TODO: Sollte auch von getUrlFromSnapshot kommen
await this._router.navigate([
'/kunde',
String(newProcessId),
'product',
{ outlets: { main: null, left: 'search', right: 'filter' } },
]);
}
}
// Bei offener Warenausgabe und Klick auf Footer Artikelsuche

View File

@@ -39,7 +39,7 @@
<div class="shell-footer-wrapper">
<shell-footer *ngIf="section$ | async; let section">
<ng-container *ngIf="section === 'customer'">
<a [routerLink]="[customerBasePath$ | async, 'product']" routerLinkActive="active">
<a [routerLink]="productRoutePath$ | async" routerLinkActive="active">
<ui-icon icon="catalog" size="30px"></ui-icon>
Artikelsuche
</a>

View File

@@ -6,17 +6,16 @@
@apply fixed right-0 left-0 overflow-auto;
top: 8.375rem;
bottom: 5rem;
main {
@apply w-full max-w-content mx-auto px-4 self-stretch;
}
}
main {
@apply w-full mx-auto max-w-desktop px-px-15 desktop:px-6 self-stretch;
}
.shell-header-wrapper {
@apply fixed top-0 left-0 right-0 bg-white;
shell-header {
@apply w-full max-w-content mx-auto;
@apply w-full max-w-desktop px-px-15 desktop:px-6 mx-auto;
}
button.notifications-btn {
@@ -36,7 +35,7 @@
top: 5.125rem;
shell-process {
@apply w-full max-w-content mx-auto;
@apply w-full max-w-desktop px-px-15 desktop:px-6 mx-auto;
height: 52px;
}
}
@@ -50,7 +49,7 @@ shell-process {
@apply fixed bottom-0 left-0 right-0 bg-white z-fixed shadow-card;
shell-footer {
@apply w-full max-w-content mx-auto;
@apply w-full max-w-desktop px-px-15 desktop:px-6 mx-auto;
.active {
@apply font-bold;

View File

@@ -12,6 +12,7 @@ import { DomainAvailabilityService } from '@domain/availability';
import { ShellProcessTabComponent } from '@shell/process';
import { Config } from '@core/config';
import { WrongDestinationModalService } from 'apps/page/package-inspection/src/lib/components/wrong-destination-modal/wrong-destination-modal.service';
import { EnvironmentService } from '@core/environment';
@Component({
selector: 'app-shell',
@@ -52,6 +53,16 @@ export class ShellComponent {
})
);
productRoutePath$ = this.customerBasePath$.pipe(
map((basePath) => {
if (this.isTablet) {
return [basePath, 'product'];
} else {
return [basePath, 'product', { outlets: { main: null, left: 'search', right: 'filter' } }];
}
})
);
get section$() {
return this._appService.getSection$().pipe(shareReplay());
}
@@ -90,6 +101,10 @@ export class ShellComponent {
return this._availabilityService.getDefaultBranch();
}
get isTablet() {
return this._environment.isTablet();
}
constructor(
private readonly _appService: ApplicationService,
private readonly _config: Config,
@@ -100,7 +115,8 @@ export class ShellComponent {
private readonly _authService: AuthService,
private readonly _availabilityService: DomainAvailabilityService,
private readonly _zone: NgZone,
private readonly _wrongDestinationModalService: WrongDestinationModalService
private readonly _wrongDestinationModalService: WrongDestinationModalService,
private readonly _environment: EnvironmentService
) {}
async setSection(section: 'customer' | 'branch') {

View File

@@ -20,7 +20,7 @@
<div class="page-price-update-item__item-details">
<div class="page-price-update-item__item-contributors flex flex-row">
{{ environment.isTablet() ? (item?.product?.contributors | substr: 42) : item?.product?.contributors }}
{{ environment.isTablet() ? (item?.product?.contributors | substr: 38) : item?.product?.contributors }}
</div>
<div
@@ -43,7 +43,7 @@
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
{{ environment.isTablet() ? (item?.product?.formatDetail | substr: 25) : item?.product?.formatDetail }}
</div>
</div>

View File

@@ -4,11 +4,9 @@ import { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer';
import { ItemDTO as PrinterItemDTO } from '@swagger/print';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { AvailabilityDTO, BranchDTO } from '@swagger/checkout';
import { BranchDTO } from '@swagger/checkout';
import { UiModalService } from '@ui/modal';
import { ModalReviewsComponent } from '@modal/reviews';
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal';
import { PurchasingOptions } from 'apps/page/checkout/src/lib/modals/purchasing-options-modal/purchasing-options-modal.store';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
import { ArticleDetailsStore } from './article-details.store';
@@ -20,6 +18,7 @@ import { BreadcrumbService } from '@core/breadcrumb';
import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { DatePipe } from '@angular/common';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
import { DomainAvailabilityService } from '@domain/availability';
@Component({
@@ -125,6 +124,7 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
private _dateAdapter: DateAdapter,
private _datePipe: DatePipe,
public elementRef: ElementRef,
private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _availability: DomainAvailabilityService
) {}
@@ -262,58 +262,12 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
}
async showPurchasingModal(selectedBranch?: BranchDTO) {
let availableOptions: PurchasingOptions[] = [];
const availabilities: { [key: string]: AvailabilityDTO } = {};
const item = await this.store.item$.pipe(first()).toPromise();
const takeNow = await this.store.isTakeAwayAvailabilityAvailable$.pipe(first()).toPromise();
if (takeNow) {
availableOptions.push('take-away');
availabilities['take-away'] = await this.store.takeAwayAvailability$.pipe(first()).toPromise();
}
const download = await this.store.isDownloadAvailabilityAvailable$.pipe(first()).toPromise();
if (download) {
availableOptions.push('download');
availabilities['download'] = await this.store.downloadAvailability$.pipe(first()).toPromise();
}
const pickup = await this.store.isPickUpAvailabilityAvailable$.pipe(first()).toPromise();
if (pickup) {
availableOptions.push('pick-up');
availabilities['pick-up'] = await this.store.pickUpAvailability$.pipe(first()).toPromise();
}
const digDelivery = await this.store.isDeliveryDigAvailabilityAvailable$.pipe(first()).toPromise();
if (digDelivery) {
availableOptions.push('dig-delivery');
availabilities['dig-delivery'] = await this.store.deliveryDigAvailability$.pipe(first()).toPromise();
}
const b2b = await this.store.isDeliveryB2BAvailabilityAvailable$.pipe(first()).toPromise();
if (b2b) {
availableOptions.push('b2b-delivery');
availabilities['b2b-delivery'] = await this.store.deliveryB2BAvailability$.pipe(first()).toPromise();
}
if (availableOptions.includes('dig-delivery') && availableOptions.includes('b2b-delivery')) {
availableOptions.push('delivery');
availabilities['delivery'] = await this.store.deliveryAvailability$.pipe(first()).toPromise();
availableOptions = availableOptions.filter((option) => !(option === 'dig-delivery' || option === 'b2b-delivery'));
}
const branch = selectedBranch || (await this.store.branch$.pipe(first()).toPromise());
this.uiModal.open({
content: PurchasingOptionsModalComponent,
data: {
availableOptions,
option: selectedBranch ? 'take-away' : undefined,
item: await this.store.item$.pipe(first()).toPromise(),
branchId: branch?.id,
processId: this.applicationService.activatedProcessId,
availabilities,
} as PurchasingOptionsModalData,
this._purchaseOptionsModalService.open({
type: 'add',
processId: this.applicationService.activatedProcessId,
items: [item],
});
}

View File

@@ -1,4 +1,4 @@
<button class="filter" [class.active]="hasFilter$ | async" (click)="filterActive$.next(true); shellFilterOverlay.open()">
<button *ngIf="isTablet" class="filter" [class.active]="hasFilter$ | async" (click)="filterActive$.next(true); shellFilterOverlay.open()">
<ui-icon size="20px" icon="filter_alit"></ui-icon>
<span class="label">Filter</span>
</button>

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { UiFilterAutocompleteProvider } from '@ui/filter';
import { isEqual } from 'lodash';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
@@ -15,7 +16,6 @@ import { ArticleSearchMainAutocompleteProvider } from './providers';
styleUrls: ['article-search.component.scss'],
providers: [
FocusSearchboxEvent,
ArticleSearchService,
{
provide: UiFilterAutocompleteProvider,
useClass: ArticleSearchMainAutocompleteProvider,
@@ -39,11 +39,17 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
);
filterActive$ = new BehaviorSubject<boolean>(false);
get isTablet() {
return this._environment.isTablet();
}
constructor(
private _breadcrumb: BreadcrumbService,
private _router: Router,
private _articleSearch: ArticleSearchService,
private _activatedRoute: ActivatedRoute
private _activatedRoute: ActivatedRoute,
private _environment: EnvironmentService
) {}
ngOnInit() {
@@ -65,9 +71,19 @@ export class ArticleSearchComponent implements OnInit, OnDestroy {
this._router.navigate(['/kunde', processId, 'product', 'details', item.id]);
} else {
const params = state.filter.getQueryParams();
this._router.navigate(['/kunde', processId, 'product', 'search', 'results'], {
queryParams: params,
});
if (this.isTablet) {
this._router.navigate(['/kunde', processId, 'product', 'search', 'results'], {
queryParams: params,
});
} else {
const item = state.items.find((f) => f);
this._router.navigate(
['/kunde', processId, 'product', { outlets: { main: null, left: 'results', right: ['details', item.id] } }],
{
queryParams: params,
}
);
}
}
}
});

View File

@@ -7,11 +7,12 @@ import { SearchResultsModule } from './search-results/search-results.module';
import { SearchMainModule } from './search-main/search-main.module';
import { SearchFilterModule } from './search-filter/search-filter.module';
import { ShellFilterOverlayModule } from '@shell/filter-overlay';
import { ArticleSearchService } from './article-search.store';
@NgModule({
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, ShellFilterOverlayModule],
exports: [ArticleSearchComponent],
declarations: [ArticleSearchComponent],
providers: [],
providers: [ArticleSearchService],
})
export class ArticleSearchModule {}

View File

@@ -1,9 +1,25 @@
<ng-container *ngIf="filter$ | async; let filter">
<div class="catalog-search-filter-content">
<button class="btn-close" type="button" (click)="close.emit()">
<button *ngIf="isTablet; else desktop" class="btn-close" type="button" (click)="close.emit()">
<ui-icon icon="close" size="20px"></ui-icon>
</button>
<ng-template #desktop>
<a
class="btn-close"
type="button"
[routerLink]="[
'/kunde',
application.activatedProcessId,
'product',
{ outlets: { main: null, left: 'results', right: ['details', item?.id] } }
]"
queryParamsHandling="preserve"
>
<ui-icon icon="close" size="20px"></ui-icon>
</a>
</ng-template>
<div class="catalog-search-filter-content-main">
<h1 class="text-3xl font-bold text-center py-4">Filter</h1>
<ui-filter

View File

@@ -17,9 +17,7 @@
}
.cta-wrapper {
@apply fixed bottom-8 whitespace-nowrap;
left: 50%;
transform: translateX(-50%);
@apply text-center whitespace-nowrap;
}
.cta-reset-filter,

View File

@@ -1,4 +1,6 @@
import { ChangeDetectionStrategy, Component, EventEmitter, OnInit, Output, ViewChild } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { UiFilter, UiFilterComponent } from '@ui/filter';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
@@ -23,7 +25,19 @@ export class ArticleSearchFilterComponent implements OnInit {
@ViewChild(UiFilterComponent, { static: false })
uiFilterComponent: UiFilterComponent;
constructor(private articleSearch: ArticleSearchService) {}
get isTablet() {
return this._environment.isTablet();
}
get item() {
return this.articleSearch.items.find((_) => true);
}
constructor(
private articleSearch: ArticleSearchService,
private _environment: EnvironmentService,
public application: ApplicationService
) {}
ngOnInit() {
this.fetching$ = this.articleSearch.fetching$;

View File

@@ -1,5 +1,6 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { UiFilterNextModule } from '@ui/filter';
import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner';
@@ -7,7 +8,7 @@ import { UiSpinnerModule } from '@ui/spinner';
import { ArticleSearchFilterComponent } from './search-filter.component';
@NgModule({
imports: [CommonModule, UiFilterNextModule, UiIconModule, UiSpinnerModule],
imports: [CommonModule, RouterModule, UiFilterNextModule, UiIconModule, UiSpinnerModule],
exports: [ArticleSearchFilterComponent],
declarations: [ArticleSearchFilterComponent],
providers: [],

View File

@@ -1,5 +1,5 @@
:host {
@apply flex flex-row rounded-card bg-white mb-2 p-4;
@apply flex flex-row rounded-card bg-white mb-2 p-4 desktop:w-[496px];
height: 187px;
}

View File

@@ -1,81 +1,112 @@
<a class="product-list-result-content" [routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', item?.id]">
<div class="item-thumbnail">
<img loading="lazy" *ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
</div>
<div class="item-contributors">
<a
*ngFor="let contributor of contributors; let last = last"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
</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"
>
{{ item?.product?.name }}
</div>
<div class="item-price">
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
<div *ngIf="selectable" class="item-data-selector">
<ui-select-bullet [ngModel]="selected" (ngModelChange)="setSelected($event)"></ui-select-bullet>
</div>
<div class="item-stock z-dropdown" [uiOverlayTrigger]="tooltip" [overlayTriggerDisabled]="!(stockTooltipText$ | async)">
<ng-container *ngIf="isOrderBranch$ | async">
<div class="flex flex-row items-center justify-between">
<ui-icon icon="home" size="1em"></ui-icon>
<span
*ngIf="inStock$ | async; let stock"
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
</div>
</ng-container>
<ng-container *ngIf="!(isOrderBranch$ | async)">
<div class="flex flex-row items-center justify-between z-dropdown">
<ui-icon class="block" icon="home" size="1em"></ui-icon>
<span class="min-w-[1rem] text-center inline-block">-</span>
<span>x</span>
</div>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<!-- <div class="item-stock"><ui-icon icon="home" size="1em"></ui-icon> {{ item?.stockInfos | stockInfos }} x</div> -->
<div class="item-ssc" [class.xs]="item?.catalogAvailability?.sscText?.length >= 60">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
</div>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<a
class="page-search-result-item__item-card p-5 desktop:p-px-10 h-[212px] desktop:h-[181px] bg-white rounded-card"
[routerLink]="detailsLink"
[queryParamsHandling]="isTablet ? 'preserve' : ''"
>
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
<img
*ngIf="item?.product?.format !== '--'"
class="page-search-result-item__item-image w-[50px] h-[79px]"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
*ngIf="item?.imageId | thumbnailUrl; let thumbnailUrl"
[src]="thumbnailUrl"
[alt]="item?.product?.name"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="item-misc">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }} <br />
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ publicationDate }}
<div class="grid-container-test">
<div
class="page-search-result-item__item-contributors desktop:text-sm font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
>
<a
*ngFor="let contributor of contributors; let last = last"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'search', 'results']"
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
(click)="$event?.stopPropagation()"
>
{{ contributor }}{{ last ? '' : ';' }}
</a>
</div>
<div
class="page-search-result-item__item-title font-bold text-2xl"
[class.text-xl]="item?.product?.name?.length >= 35"
[class.text-lg]="item?.product?.name?.length >= 40"
[class.text-md]="item?.product?.name?.length >= 50"
[class.text-sm]="item?.product?.name?.length >= 60 || !isTablet"
[class.text-xs]="item?.product?.name?.length >= 100 || (!isTablet && item?.product?.name?.length >= 70)"
>
{{ item?.product?.name }}
</div>
<div class="page-search-result-item__item-format desktop:text-sm">
<div *ngIf="item?.product?.format && item?.product?.formatDetail" class="font-bold flex flex-row">
<img
class="mr-3"
*ngIf="item?.product?.format !== '--'"
loading="lazy"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail | substr: 25 }}
</div>
</div>
<div class="page-search-result-item__item-manufacturer desktop:text-sm">
{{ item?.product?.manufacturer | substr: 18 }} | {{ item?.product?.ean }}
</div>
<div class="page-search-result-item__item-misc desktop:text-sm">
{{ item?.product?.volume }} <span *ngIf="item?.product?.volume && item?.product?.publicationDate">|</span>
{{ publicationDate }}
</div>
<div class="page-search-result-item__item-price desktop:text-sm font-bold justify-self-end">
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
<div class="page-search-result-item__item-select-bullet justify-self-end">
<input
*ngIf="selectable"
(click)="$event.stopPropagation()"
[ngModel]="selected$ | async"
(ngModelChange)="setSelected()"
class="isa-select-bullet"
type="checkbox"
/>
</div>
<div
class="page-search-result-item__item-stock desktop:text-sm font-bold z-dropdown justify-self-end"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
>
<ng-container *ngIf="isOrderBranch$ | async">
<div class="flex flex-row items-center justify-between">
<ui-icon icon="home" size="1em"></ui-icon>
<span
*ngIf="inStock$ | async; let stock"
[class.skeleton]="stock.inStock === undefined"
class="min-w-[1rem] text-right inline-block"
>{{ stock?.inStock }}</span
>
<span>x</span>
</div>
</ng-container>
<ng-container *ngIf="!(isOrderBranch$ | async)">
<div class="flex flex-row items-center justify-between z-dropdown">
<ui-icon class="block" icon="home" size="1em"></ui-icon>
<span class="min-w-[1rem] text-center inline-block">-</span>
<span>x</span>
</div>
</ng-container>
</div>
<ui-tooltip #tooltip yPosition="above" xPosition="after" [yOffset]="-8" [closeable]="true">
{{ stockTooltipText$ | async }}
</ui-tooltip>
<div class="page-search-result-item__item-ssc desktop:text-sm" [class.xs]="item?.catalogAvailability?.sscText?.length >= 60">
<strong>{{ item?.catalogAvailability?.ssc }}</strong> -
{{ !isTablet ? item?.catalogAvailability?.sscText : (item?.catalogAvailability?.sscText | substr: 18) }}
</div>
</div>
</a>

View File

@@ -1,113 +1,61 @@
.product-list-result-content {
@apply text-black no-underline grid;
grid-template-columns: 102px 50% auto;
grid-template-rows: auto;
:host {
@apply flex flex-col w-full h-[212px] desktop:h-[181px];
}
.page-search-result-item__item-card {
@apply grid grid-flow-col;
grid-template-columns: 63px auto;
box-shadow: 0px 0px 10px rgba(220, 226, 233, 0.5);
}
.grid-container-test {
@apply grid grid-flow-row gap-[6px];
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-title item-price'
'item-thumbnail item-title item-data-selector'
'item-thumbnail item-format item-stock'
'item-thumbnail item-misc item-ssc';
'contributors contributors contributors'
'title title price'
'title title price'
'title title select'
'format format select'
'manufacturer manufacturer stock'
'misc ssc ssc';
}
.item-thumbnail {
grid-area: item-thumbnail;
width: 70px;
@apply mr-8;
img {
max-width: 100%;
max-height: 150px;
@apply rounded-card shadow-cta;
}
.page-search-result-item__item-contributors {
grid-area: contributors;
}
.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;
}
.page-search-result-item__item-price {
grid-area: price;
}
.item-title {
grid-area: item-title;
@apply font-bold text-2xl;
height: 64px;
max-height: 64px;
.page-search-result-item__item-title {
grid-area: title;
}
.item-title.xl {
@apply font-bold text-xl;
.page-search-result-item__item-format {
grid-area: format;
}
.item-title.lg {
@apply font-bold text-lg;
.page-search-result-item__item-manufacturer {
grid-area: manufacturer;
}
.item-title.md {
@apply font-bold text-base;
.page-search-result-item__item-misc {
grid-area: misc;
}
.item-title.sm {
@apply font-bold text-sm;
.page-search-result-item__item-select-bullet {
grid-area: select;
}
.item-title.xs {
@apply font-bold text-xs;
.page-search-result-item__item-stock {
grid-area: stock;
}
.item-price {
grid-area: item-price;
@apply font-bold text-xl text-right;
.page-search-result-item__item-ssc {
@apply justify-self-end;
grid-area: ssc;
}
.item-format {
grid-area: item-format;
@apply flex flex-row items-center font-bold text-lg whitespace-nowrap;
img {
@apply mr-2;
}
}
.item-stock {
grid-area: item-stock;
@apply flex flex-row justify-end items-baseline font-bold text-lg;
ui-icon {
@apply text-active-customer mr-2;
}
}
.item-misc {
grid-area: item-misc;
}
.item-ssc {
grid-area: item-ssc;
@apply font-bold text-right;
}
.item-ssc.xs {
@apply font-bold text-xs;
}
.item-data-selector {
@apply w-full flex justify-end;
grid-area: item-data-selector;
}
ui-select-bullet {
@apply p-4 -m-4 z-dropdown;
}
@media (min-width: 1025px) {
.item-contributors {
max-width: 780px;
}
.page-search-result-item__item-image {
box-shadow: 0px 6px 18px rgba(0, 0, 0, 0.197935);
}

View File

@@ -1,13 +1,15 @@
import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
import { ComponentStore } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { debounceTime, switchMap, map, tap, shareReplay } from 'rxjs/operators';
import { debounceTime, switchMap, map, shareReplay } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
export interface SearchResultItemComponentState {
@@ -36,16 +38,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
readonly item$ = this.select((s) => s.item);
@Input()
get selected() {
return this.get((s) => s.selected);
}
set selected(selected: boolean) {
if (this.selected !== selected) {
this.patchState({ selected });
}
}
readonly selected$ = this.select((s) => s.selected);
selected$ = this._articleSearchService.selectedItemIds$.pipe(map((selectedItemIds) => selectedItemIds.includes(this.item?.id)));
@Input()
get selectable() {
@@ -77,6 +70,23 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return '';
}
get isTablet() {
return this._environment.isTablet();
}
get detailsLink() {
if (this.isTablet) {
return ['/kunde', this.applicationService.activatedProcessId, 'product', 'details', this.item?.id];
} else {
return [
'/kunde',
this.applicationService.activatedProcessId,
'product',
{ outlets: { main: null, left: 'results', right: ['details', this.item?.id] } },
];
}
}
defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
@@ -121,7 +131,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _articleSearchService: ArticleSearchService,
public applicationService: ApplicationService,
private _stockService: DomainInStockService,
private _availability: DomainAvailabilityService
private _availability: DomainAvailabilityService,
private _environment: EnvironmentService
) {
super({
selected: false,
@@ -129,7 +140,26 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
});
}
setSelected(selected: boolean) {
this._articleSearchService.setSelected({ selected, itemId: this.item?.id });
setSelected() {
const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });
// this._articleSearchService.setSelected({ selected, itemId: this.item?.id });
// setSelected({ selected, itemUid }: { selected: boolean; itemUid: string }) {
// if (selected) {
// this.patchState({
// selectedItemUids: [...this.selectedItemUids, itemUid],
// });
// } else if (!selected) {
// this.patchState({
// selectedItemUids: this.selectedItemUids.filter((id) => id !== itemUid),
// });
// }
// }
}
// setSelected() {
// const isSelected = this._store.selectedItemUids.includes(this.item?.uId);
// this._store.setSelected({ selected: !isSelected, itemUid: this.item?.uId });
// }
}

View File

@@ -1,27 +1,43 @@
<div class="filter-wrapper">
<div class="hits" *ngIf="hits$ | async; let hits">{{ hits }} Titel</div>
<div class="page-search-results__header bg-background-liste flex flex-col items-end pb-4">
<a
[class.active]="hasFilter$ | async"
class="page-search-results__filter h-14 rounded-card font-bold px-5 mb-4 text-lg bg-cadet-blue flex flex-row flex-nowrap items-center justify-center"
[routerLink]="['/kunde', application.activatedProcessId, 'product', { outlets: { main: null, left: 'results', right: 'filter' } }]"
queryParamsHandling="preserve"
>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
<div *ngIf="hits$ | async; let hits" class="page-search-results__items-count inline-flex flex-row items-center pr-5 text-sm">
{{ hits ??
0 }}
Titel
</div>
</div>
<div class="page-search-results__order-by h-[53px] flex flex-row items-center justify-center bg-white rounded-t-card">
<ui-order-by-filter [orderBy]="(filter$ | async)?.orderBy" (selectedOrderByChange)="search(); updateBreadcrumbs()"> </ui-order-by-filter>
</div>
<cdk-virtual-scroll-viewport
#scrollContainer
class="product-list scroll-bar scroll-bar-margin"
class="product-list"
[itemSize]="187"
minBufferPx="1200"
maxBufferPx="1200"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<div class="product-list-result" *cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId">
<search-result-item
[selected]="item | searchResultSelected: searchService.selectedItemIds"
[selectable]="isSelectable(item)"
[item]="item"
></search-result-item>
</div>
<search-result-item
class="mb-px-10"
*cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
[selectable]="isSelectable(item)"
[item]="item"
></search-result-item>
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport>
<div class="actions">
<div class="actions z-fixed">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"

View File

@@ -1,18 +1,24 @@
:host {
@apply box-border grid;
max-height: calc(100vh - 364px);
height: 100vh;
grid-template-rows: auto 1fr;
@apply box-border grid h-[100vh] max-h-[calc(100vh-364px)] desktop:max-h-[calc(100vh-300px)];
grid-template-rows: auto auto 1fr;
}
.product-list {
@apply m-0 p-0 mt-2;
@apply m-0 p-0 mt-px-2;
}
.product-list-result {
@apply list-none bg-white rounded-card p-4 mb-2;
height: 187px;
max-height: 187px;
.filter {
@apply justify-self-end font-sans flex self-end items-center font-bold bg-wild-blue-yonder border-0 text-regular py-px-8 px-px-15 rounded-filter justify-center z-sticky;
width: 106px;
min-width: 106px;
.label {
@apply ml-px-5;
}
&.active {
@apply bg-active-customer text-white ml-px-5;
}
}
.filter-wrapper {

View File

@@ -3,6 +3,7 @@ import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewC
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { EnvironmentService } from '@core/environment';
import { DomainCheckoutService } from '@domain/checkout';
import { ItemDTO } from '@swagger/cat';
import { AddToShoppingCartDTO } from '@swagger/checkout';
@@ -11,7 +12,7 @@ import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { CacheService } from 'apps/core/cache/src/public-api';
import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, switchMap } from 'rxjs/operators';
import { debounceTime, filter, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { AddedToCartModalComponent } from './added-to-cart-modal/added-to-cart-modal.component';
import { SearchResultItemComponent } from './search-result-item.component';
@@ -47,14 +48,29 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
trackByItemId: TrackByFunction<ItemDTO> = (index, item) => item.id;
get isTablet() {
return this._environment.isTablet();
}
initialFilter$ = this.filter$.pipe(
filter((filter) => !!filter),
first()
);
hasFilter$ = this.filter$.pipe(
withLatestFrom(this.initialFilter$),
map(([filter, initialFilter]) => !isEqual(filter?.getQueryParams(), initialFilter?.getQueryParams()))
);
constructor(
public searchService: ArticleSearchService,
private route: ActivatedRoute,
private application: ApplicationService,
public application: ApplicationService,
private breadcrumb: BreadcrumbService,
private cache: CacheService,
private _uiModal: UiModalService,
private _checkoutService: DomainCheckoutService
private _checkoutService: DomainCheckoutService,
private _environment: EnvironmentService
) {}
ngOnInit() {
@@ -241,6 +257,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
// Zeige Select Radio Button nicht an wenn Item Archivartikel oder Fortsetzungsartikel ist
const isArchiv = item?.catalogAvailability?.status === 1;
const isFortsetzung = item?.features?.find((i) => i?.key === 'PFO');
return !(isArchiv || isFortsetzung);
}

View File

@@ -2,10 +2,55 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ArticleDetailsComponent } from './article-details/article-details.component';
import { ArticleSearchComponent } from './article-search/article-search.component';
import { ArticleSearchFilterComponent } from './article-search/search-filter/search-filter.component';
import { ArticleSearchMainComponent } from './article-search/search-main/search-main.component';
import { ArticleSearchResultsComponent } from './article-search/search-results/search-results.component';
import { PageCatalogComponent } from './page-catalog.component';
const auxiliaryRoutes = [
{
path: 'search',
component: ArticleSearchComponent,
outlet: 'left',
children: [
{
path: '',
component: ArticleSearchMainComponent,
},
],
},
{
path: 'filter',
component: ArticleSearchFilterComponent,
outlet: 'right',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'right',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'left',
},
{
path: 'results',
component: ArticleSearchResultsComponent,
outlet: 'main',
},
{
path: 'details/ean/:ean',
component: ArticleDetailsComponent,
outlet: 'right',
},
{
path: 'details/:id',
component: ArticleDetailsComponent,
outlet: 'right',
},
];
const routes: Routes = [
{
path: '',
@@ -33,6 +78,7 @@ const routes: Routes = [
path: 'details/:id',
component: ArticleDetailsComponent,
},
...auxiliaryRoutes,
{
path: '',
pathMatch: 'full',

View File

@@ -2,4 +2,21 @@
<shared-branch-selector [branchType]="1" [value]="selectedBranch$ | async" (valueChange)="patchProcessData($event)">
</shared-branch-selector>
</shared-breadcrumb>
<router-outlet></router-outlet>
<ng-container *ngIf="isTablet; else desktop">
<router-outlet></router-outlet>
</ng-container>
<ng-template #desktop>
<router-outlet name="main"></router-outlet>
<div class="grid desktop:grid-cols-[10.5rem_31rem_45.5rem]">
<div class="bg-white hidden desktop:block mr-6"></div>
<div class="mr-6 hidden desktop:block">
<router-outlet name="left"></router-outlet>
</div>
<div>
<router-outlet name="right"></router-outlet>
</div>
</div>
</ng-template>

View File

@@ -27,6 +27,10 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
_onDestroy$ = new Subject<boolean>();
get isTablet() {
return this._environmentService.isTablet();
}
constructor(
public application: ApplicationService,
private _uiModal: UiModalService,
@@ -41,7 +45,7 @@ export class PageCatalogComponent implements OnInit, AfterViewInit, OnDestroy {
}
ngAfterViewInit(): void {
if (this._environmentService.isTablet()) {
if (this.isTablet) {
fromEvent(this.branchSelectorRef.nativeElement, 'focusin')
.pipe(takeUntil(this._onDestroy$))
.subscribe((_) => {

View File

@@ -6,8 +6,6 @@ import { DomainCheckoutService } from '@domain/checkout';
import { AvailabilityDTO, DestinationDTO, NotificationChannel, ShoppingCartItemDTO, ShoppingCartDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiMessageModalComponent, UiModalService } from '@ui/modal';
import { PrintModalData, PrintModalComponent } from '@modal/printer';
import { PurchasingOptionsModalComponent, PurchasingOptionsModalData } from '../modals/purchasing-options-modal';
import { PurchasingOptions } from '../modals/purchasing-options-modal/purchasing-options-modal.store';
import { AuthService } from '@core/auth';
import { first, map, shareReplay, switchMap, take, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
import { Subject, NEVER, combineLatest, BehaviorSubject } from 'rxjs';
@@ -15,13 +13,11 @@ import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import { DomainPrinterService } from '@domain/printer';
import { CheckoutDummyComponent } from '../checkout-dummy/checkout-dummy.component';
import { ResponseArgsOfItemDTO } from '@swagger/cat';
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
import { emailNotificationValidator, mobileNotificationValidator } from '@shared/components/notification-channel-control';
import { PurchasingOptionsListModalComponent } from '../modals/purchasing-options-list-modal';
import { PurchasingOptionsListModalData } from '../modals/purchasing-options-list-modal/purchasing-options-list-modal.data';
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;
@@ -242,7 +238,8 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
private domainCatalogService: DomainCatalogService,
private breadcrumb: BreadcrumbService,
private domainPrinterService: DomainPrinterService,
private _fb: UntypedFormBuilder
private _fb: UntypedFormBuilder,
private _purchaseOptionsModalService: PurchaseOptionsModalService
) {
super({
shoppingCart: undefined,
@@ -434,171 +431,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
}
async changeItem({ shoppingCartItem }: { shoppingCartItem: ShoppingCartItemDTO }) {
this.loadingOnItemChangeById$.next(shoppingCartItem.id);
const quantity = shoppingCartItem.quantity;
const branchNo = this.auth.getClaimByKey('branch_no');
const branchId = shoppingCartItem?.destination?.data?.targetBranch?.id;
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
let branch = await this.domainCheckoutService
.getBranches()
.pipe(map((branches) => branches.find((branch) => (branchId ? branch.id === branchId : branch.branchNumber === branchNo))))
.toPromise();
if (!branch) {
branch = await this.applicationService.getSelectedBranch$().pipe(take(1)).toPromise();
}
let catalogItem: ResponseArgsOfItemDTO;
if (Number.isInteger(shoppingCartItem?.product?.catalogProductNumber)) {
catalogItem = await this.domainCatalogService
.getDetailsById({ id: Number(shoppingCartItem.product.catalogProductNumber) })
.toPromise();
} else if (shoppingCartItem?.product?.ean) {
catalogItem = await this.domainCatalogService.getDetailsByEan({ ean: shoppingCartItem.product.ean }).toPromise();
}
let takeAwayAvailability: AvailabilityDTO;
if (!!catalogItem?.result?.product) {
takeAwayAvailability = await this.availabilityService
.getTakeAwayAvailability({
item: {
itemId: catalogItem.result.id,
ean: catalogItem.result.product.ean,
price: catalogItem.result.catalogAvailability?.price,
},
quantity,
})
.toPromise();
}
const pickupAvailability = await this.availabilityService
.getPickUpAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
branch,
quantity,
})
.toPromise();
const digAvailability = await this.availabilityService
.getDigDeliveryAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
quantity,
})
.toPromise();
const b2bAvailability = await this.availabilityService
.getB2bDeliveryAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
quantity,
})
.toPromise();
const downloadAvailability = await this.availabilityService
.getDownloadAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
})
.toPromise();
let availableOptions: PurchasingOptions[] = [];
const availabilities: { [key: string]: AvailabilityDTO } = {};
if (takeAwayAvailability && this.availabilityService.isAvailable({ availability: takeAwayAvailability })) {
availableOptions.push('take-away');
availabilities['take-away'] = takeAwayAvailability;
}
if (downloadAvailability && this.availabilityService.isAvailable({ availability: downloadAvailability })) {
availableOptions.push('download');
availabilities['download'] = downloadAvailability;
}
if (pickupAvailability && this.availabilityService.isAvailable({ availability: pickupAvailability[0] })) {
if (pickupAvailability[1].availableFor) {
if ((pickupAvailability[1].availableFor & 2) === 2) {
availableOptions.push('pick-up');
availabilities['pick-up'] = pickupAvailability[0];
}
} else {
availableOptions.push('pick-up');
availabilities['pick-up'] = pickupAvailability[0];
}
if (!customerFeatures?.webshop && this.availabilityService.isAvailable({ availability: b2bAvailability })) {
availableOptions.push('b2b-delivery');
availabilities['b2b-delivery'] = b2bAvailability;
}
}
if (digAvailability && this.availabilityService.isAvailable({ availability: digAvailability }) && !customerFeatures?.b2b) {
availableOptions.push('dig-delivery');
availabilities['dig-delivery'] = digAvailability;
}
if (availableOptions.includes('dig-delivery') && availableOptions.includes('b2b-delivery')) {
let shippingAvailability = await this.availabilityService
.getDeliveryAvailability({
item: {
itemId: Number(shoppingCartItem.product.catalogProductNumber),
ean: shoppingCartItem.product.ean,
price: shoppingCartItem.availability.price,
},
quantity,
})
.toPromise();
if (shippingAvailability && this.availabilityService.isAvailable({ availability: shippingAvailability })) {
availableOptions.push('delivery');
availabilities['delivery'] = shippingAvailability;
availableOptions = availableOptions.filter((option) => !(option === 'dig-delivery' || option === 'b2b-delivery'));
}
}
this.loadingOnItemChangeById$.next(undefined);
this.cdr.markForCheck();
const itemId = Number(shoppingCartItem.product.catalogProductNumber);
const modal = this.uiModal.open({
content: PurchasingOptionsModalComponent,
data: {
availableOptions,
item: {
id: itemId,
itemId: itemId,
product: shoppingCartItem.product,
price: shoppingCartItem.availability.price,
catalogAvailability: {
status: shoppingCartItem.availability.availabilityType,
price: shoppingCartItem.availability.price,
},
},
shoppingCartItem,
branchId: branch?.id,
processId: this.applicationService.activatedProcessId,
availabilities,
} as PurchasingOptionsModalData,
});
modal.afterClosed$.pipe(takeUntil(this._orderCompleted)).subscribe(() => {
this.setQuantityError(shoppingCartItem, undefined, false);
this._purchaseOptionsModalService.open({
processId: this.applicationService.activatedProcessId,
items: [shoppingCartItem],
type: 'update',
});
}
@@ -816,16 +652,10 @@ export class CheckoutReviewComponent extends ComponentStore<CheckoutReviewCompon
}
async showPurchasingListModal(shoppingCartItems: ShoppingCartItemDTO[]) {
const customerFeatures = await this.customerFeatures$.pipe(first()).toPromise();
this.uiModal.open({
content: PurchasingOptionsListModalComponent,
title: 'Wie möchten Sie die Artikel erhalten?',
config: { showScrollbarY: false },
data: {
processId: this.applicationService.activatedProcessId,
shoppingCartItems: shoppingCartItems,
customerFeatures,
} as PurchasingOptionsListModalData,
this._purchaseOptionsModalService.open({
processId: this.applicationService.activatedProcessId,
items: shoppingCartItems,
type: 'update',
});
}

View File

@@ -1,4 +1,3 @@
// start:ng42.barrel
export * from './page-checkout.module';
export * from './page-checkout-modals.module';
// end:ng42.barrel

View File

@@ -1,13 +0,0 @@
<div class="option-icon">
<ui-icon size="50px" icon="truck"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('delivery')"
[class.selected]="(selectedOption$ | async) === 'delivery'"
>
Versand
</button>
<p>Möchten Sie die Artikel<br />geliefert bekommen?</p>
<p>Versandkostenfrei</p>

View File

@@ -1,23 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-delivery-option-list',
templateUrl: 'delivery-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeliveryOptionListComponent {
selectedOption$ = this._store.selectedFilterOption$;
optionChipDisabled$ = this._store.fetchingAvailabilities$;
constructor(private _store: PurchasingOptionsListModalStore) {}
optionChange(option: string) {
if (this._store.selectedFilterOption === option) {
this._store.selectedFilterOption = undefined;
} else {
this._store.selectedFilterOption = option;
}
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './delivery-option-list.component';
// end:ng42.barrel

View File

@@ -1,7 +0,0 @@
// start:ng42.barrel
export * from './delivery-option';
export * from './pick-up-option';
export * from './take-away-option';
export * from './purchasing-options-list-modal.component';
export * from './purchasing-options-list-modal.module';
// end:ng42.barrel

View File

@@ -1,44 +0,0 @@
:host {
@apply block w-72;
}
.option-icon {
@apply text-ucla-blue mx-auto;
width: 40px;
.truck-b2b {
margin-top: -21px;
margin-bottom: -12px;
width: 70px;
}
}
.option-chip {
@apply rounded-full text-base px-4 py-3 bg-glitter text-inactive-customer border-none font-bold;
&.selected {
@apply bg-active-customer text-white;
}
}
.option-description {
@apply my-2;
}
.option-select {
@apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6;
}
p {
@apply my-4;
}
::ng-deep page-purchasing-options-list-modal ui-branch-dropdown .wrapper {
@apply mx-auto;
width: 80%;
}
::ng-deep page-pick-up-option-list .option-chip:disabled,
::ng-deep page-take-away-option-list .option-chip:disabled {
@apply bg-disabled-branch border-disabled-branch text-white;
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './pick-up-option-list.component';
// end:ng42.barrel

View File

@@ -1,18 +0,0 @@
<div class="option-icon">
<ui-icon size="50px" icon="box_out"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('pick-up')"
[class.selected]="(selectedOption$ | async) === 'pick-up'"
>
Abholung
</button>
<p>Möchten Sie die Artikel<br />in einer unserer Filialen<br />abholen?</p>
<ui-branch-dropdown
[branches]="branches$ | async"
[selected]="selectedBranch$ | async"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>

View File

@@ -1,48 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BranchDTO } from '@swagger/checkout';
import { combineLatest } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-pick-up-option-list',
templateUrl: 'pick-up-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PickUpOptionListComponent {
branches$ = this._store.branches$;
selectedBranch$ = this._store.selectedPickUpBranch$.pipe(
map((branch) => {
// Determins if branch is targetBranch
if (branch?.branchType === 1) {
return branch.name;
}
})
);
selectedOption$ = this._store.selectedFilterOption$;
optionChipDisabled$ = combineLatest([this._store.fetchingAvailabilities$, this.selectedBranch$]).pipe(
map(([fetching, selectedBranch]) => {
return fetching || !selectedBranch;
})
);
constructor(private _store: PurchasingOptionsListModalStore) {}
optionChange(option: string) {
if (this._store.selectedFilterOption === option) {
this._store.selectedFilterOption = undefined;
} else {
this._store.selectedFilterOption = option;
}
}
async selectBranch(branch: BranchDTO) {
this._store.lastSelectedFilterOption$.next(undefined);
this._store.selectedPickUpBranch = branch;
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
shoppingCartItems.forEach((item) => this._store.loadPickUpAvailability({ item }));
}
}

View File

@@ -1,115 +0,0 @@
<div class="item-thumbnail">
<img loading="lazy" *ngIf="item?.product?.ean | productImage; let thumbnailUrl" [src]="thumbnailUrl" [alt]="item?.product?.name" />
</div>
<div class="item-contributors">
{{ item.product.contributors }}
</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"
>
{{ item?.product?.name }}
</div>
<ng-container *ngIf="canAdd$ | async; let canAdd">
<div class="item-can-add" *ngIf="canAdd !== true">
{{ canAdd }}
</div>
</ng-container>
<div class="item-format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ 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: 'dd. MMMM yyyy' }}
</div>
<div class="item-price-stock">
<div class="price">
<ng-container *ngIf="showTooltip$ | async">
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
i
</button>
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
Günstigerer Preis aus Hugendubel Katalog wird übernommen
</ui-tooltip>
</ng-container>
<div *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</div>
</div>
<div>
<ui-quantity-dropdown
[disabled]="fetchingAvailabilities$ | async"
[ngModel]="item.quantity"
(ngModelChange)="changeQuantity($event)"
[range]="quantityRange$ | async"
>
</ui-quantity-dropdown>
</div>
</div>
<div class="item-select">
<ui-select-bullet
*ngIf="selectVisible$ | async"
[disabled]="selectDisabled$ | async"
[ngModel]="isSelected$ | async"
(ngModelChange)="selected($event)"
></ui-select-bullet>
</div>
<div class="item-availability">
<div class="fetching" *ngIf="fetchingAvailabilities$ | async; else availabilities"></div>
<ng-template #availabilities>
<ng-container *ngIf="notAvailable$ | async; else available">
<span class="hint">Derzeit nicht bestellbar</span>
</ng-container>
<ng-template #available>
<span>Verfügbar als</span>
<div *ngIf="takeAwayAvailabilities$ | async; let takeAwayAvailabilites">
<ui-icon icon="shopping_bag" size="18px"></ui-icon>
<span class="instock">{{ takeAwayAvailabilites?.inStock }}x</span> ab sofort
</div>
<div *ngIf="!!(pickUpAvailabilities$ | async)">
<ui-icon icon="box_out" size="18px"></ui-icon>
{{ (pickUpAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</div>
<div *ngIf="!!(deliveryDigAvailabilities$ | async); else b2b">
<ui-icon class="truck" icon="truck" size="30px"></ui-icon>
<ng-container *ngIf="deliveryDigAvailabilities$ | async; let deliveryDigAvailabilities">
<ng-container *ngIf="deliveryDigAvailabilities?.estimatedDelivery; else estimatedShippingDate">
{{ (deliveryDigAvailabilities?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} -
{{ (deliveryDigAvailabilities?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate>
{{ deliveryDigAvailabilities.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</ng-template>
</ng-container>
</div>
<ng-template #b2b>
<div *ngIf="!!(deliveryB2bAvailabilities$ | async)">
<ui-icon class="truck-b2b" icon="truck_b2b" size="40px"></ui-icon>
{{ (deliveryB2bAvailabilities$ | async)?.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
</div>
</ng-template>
</ng-template>
</ng-template>
</div>

View File

@@ -1,180 +0,0 @@
:host {
@apply text-black no-underline grid py-4;
grid-template-columns: 102px 60% auto;
grid-template-rows: auto;
grid-template-areas:
'item-thumbnail item-contributors item-contributors'
'item-thumbnail item-title item-price-stock'
'item-thumbnail item-can-add item-price-stock'
'item-thumbnail item-format item-price-stock'
'item-thumbnail item-info item-select'
'item-thumbnail item-date item-select'
'item-thumbnail item-ssc item-select'
'item-thumbnail item-availability item-select';
}
.item-thumbnail {
grid-area: item-thumbnail;
width: 70px;
@apply mr-8;
img {
max-width: 100%;
max-height: 150px;
@apply rounded-card shadow-cta;
}
}
.item-contributors {
@apply font-bold no-underline;
grid-area: item-contributors;
height: 22px;
text-overflow: ellipsis;
overflow: hidden;
max-width: 600px;
white-space: nowrap;
}
.item-title {
grid-area: item-title;
@apply font-bold text-lg mb-4;
max-height: 64px;
}
.item-title.xl {
@apply font-bold text-xl;
}
.item-title.lg {
@apply font-bold text-lg;
}
.item-title.md {
@apply font-bold text-base;
}
.item-title.sm {
@apply font-bold text-sm;
}
.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;
img {
@apply mr-2;
}
}
.item-price-stock {
grid-area: item-price-stock;
@apply font-bold text-xl text-right;
.price {
@apply flex flex-row justify-end items-center;
}
.info-tooltip-button {
@apply border-font-customer border-solid border-2 bg-white rounded-full text-base font-bold mr-3;
border-style: outset;
width: 31px;
height: 31px;
margin-left: 10px;
}
.quantity-btn {
@apply flex flex-row items-center p-0 w-full text-right outline-none border-none bg-transparent text-lg;
}
.quantity-btn-icon {
@apply inline-flex ml-2;
transition: transform 200ms ease-in-out;
}
ui-quantity-dropdown {
@apply flex justify-end mt-2;
&.disabled {
@apply cursor-not-allowed bg-inactive-branch;
}
}
}
.item-stock {
grid-area: item-stock;
@apply flex flex-row justify-end items-baseline font-bold text-lg;
ui-icon {
@apply text-active-customer mr-2;
}
}
.item-info {
grid-area: item-info;
}
.item-availability {
@apply flex flex-row items-center mt-4 whitespace-nowrap text-sm;
grid-area: item-availability;
.fetching {
@apply w-52 h-px-20;
background-color: #e6eff9;
animation: load 0.75s linear infinite;
}
span {
@apply mr-4;
}
.instock {
@apply mr-2 font-bold;
}
ui-icon {
@apply text-dark-cerulean mx-2;
}
div {
@apply mr-4 flex items-center;
}
.truck {
@apply -mb-px-5 -mt-px-5;
}
.truck-b2b {
@apply -mb-px-10 -mt-px-10;
}
}
.item-can-add {
@apply text-xl text-dark-goldenrod font-semibold;
grid-area: item-can-add;
}
.item-select {
@apply flex items-center justify-end;
grid-area: item-select;
ui-select-bullet {
@apply cursor-pointer p-4 -m-4 z-dropdown;
&.disabled {
@apply cursor-not-allowed;
}
}
}
.hint {
@apply text-xl text-dark-goldenrod font-semibold;
}
@screen desktop {
.item-availability {
@apply text-base;
}
}

View File

@@ -1,250 +0,0 @@
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { combineLatest, Observable } from 'rxjs';
import { filter, map, shareReplay, withLatestFrom } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-purchasing-options-list-item',
templateUrl: 'purchasing-options-list-item.component.html',
styleUrls: ['purchasing-options-list-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PurchasingOptionsListItemComponent {
@Input()
item: ShoppingCartItemDTO;
isSelected$ = this._store.selectedShoppingCartItems$.pipe(
map((selectedShoppingCartItems) => !!selectedShoppingCartItems?.find((item) => item.id === this.item.id))
);
fetchingAvailabilities$ = combineLatest([
this._store.takeAwayAvailabilities$,
this._store.pickUpAvailabilities$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$,
]).pipe(
map(
([takeAway, pickUp, delivery, digDelivery, b2bDelivery]) =>
!takeAway ||
takeAway[this.item.product.catalogProductNumber] === true ||
!pickUp ||
pickUp[this.item.product.catalogProductNumber] === true ||
!delivery ||
delivery[this.item.product.catalogProductNumber] === true ||
!digDelivery ||
digDelivery[this.item.product.catalogProductNumber] === true ||
!b2bDelivery ||
b2bDelivery[this.item.product.catalogProductNumber] === true
)
);
takeAwayAvailabilities$ = this._store.takeAwayAvailabilities$.pipe(
map((takeAwayAvailabilities) => {
if (takeAwayAvailabilities) {
const availability = takeAwayAvailabilities[this.item.product?.catalogProductNumber];
if (typeof availability === 'boolean') {
return undefined;
}
return availability;
}
return undefined;
}),
shareReplay()
);
pickUpAvailabilities$: Observable<AvailabilityDTO> = this._store.pickUpAvailabilities$.pipe(
map((pickUpAvailabilities) => {
if (pickUpAvailabilities) {
const availability = pickUpAvailabilities[this.item.product?.catalogProductNumber];
if (typeof availability === 'boolean') {
return undefined;
}
return availability;
}
return undefined;
}),
shareReplay()
);
deliveryAvailabilities$ = this._store.deliveryAvailabilities$.pipe(
map((shippingAvailabilities) => (!!shippingAvailabilities ? shippingAvailabilities[this.item.product?.catalogProductNumber] : [])),
shareReplay()
);
deliveryDigAvailabilities$: Observable<AvailabilityDTO> = this._store.deliveryDigAvailabilities$.pipe(
map((shippingAvailabilities) => {
if (shippingAvailabilities) {
const availability = shippingAvailabilities[this.item.product?.catalogProductNumber];
if (typeof availability === 'boolean') {
return undefined;
}
return availability;
}
return undefined;
}),
shareReplay()
);
deliveryB2bAvailabilities$ = this._store.deliveryB2bAvailabilities$.pipe(
map((shippingAvailabilities) => {
if (shippingAvailabilities) {
const availability = shippingAvailabilities[this.item.product?.catalogProductNumber];
if (typeof availability === 'boolean') {
return undefined;
}
return availability;
}
return undefined;
}),
shareReplay()
);
notAvailable$ = combineLatest([
this.fetchingAvailabilities$,
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryDigAvailabilities$,
this.deliveryB2bAvailabilities$,
]).pipe(
map(
([fetching, takeAway, store, delivery, deliveryDig, deliveryB2b]) =>
!fetching && !takeAway && !store && !delivery && !deliveryDig && !deliveryB2b
)
);
showTooltip$ = this._store.selectedFilterOption$.pipe(
withLatestFrom(this.deliveryAvailabilities$, this.deliveryDigAvailabilities$),
map(([option, delivery, deliveryDig]) => {
if (option === 'delivery') {
const deliveryAvailability = (deliveryDig as AvailabilityDTO) || (delivery as AvailabilityDTO);
const shippingPrice = deliveryAvailability?.price?.value?.value;
const catalogPrice = this.item?.availability?.price?.value?.value;
return catalogPrice < shippingPrice;
}
return false;
})
);
price$ = combineLatest([this.fetchingAvailabilities$, this._store.selectedFilterOption$]).pipe(
filter(([fetching]) => !fetching),
withLatestFrom(
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryDigAvailabilities$,
this.deliveryB2bAvailabilities$
),
map(([[_, option], takeAway, pickUp, delivery, deliveryDig, deliveryB2b]) => {
let availability;
switch (option) {
case 'take-away':
availability = takeAway;
break;
case 'pick-up':
availability = pickUp;
break;
case 'delivery':
if (deliveryDig || delivery) {
availability = deliveryDig || delivery;
} else {
availability = deliveryB2b;
option = 'b2b-delivery';
availability.p;
}
break;
default:
return this.item.availability?.price ?? this.item.unitPrice;
}
return this._availabilityService.getPriceForAvailability(option, this.item.availability, availability) ?? this.item.unitPrice;
})
);
selectDisabled$ = this._store.selectedFilterOption$.pipe(map((selectedFilterOption) => !selectedFilterOption));
selectVisible$ = combineLatest([this._store.canAdd$, this._store.selectedShoppingCartItems$]).pipe(
withLatestFrom(
this._store.selectedFilterOption$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$,
this._store.fetchingAvailabilities$
),
map(([[canAdd, items], option, delivery, deliveryDig, deliveryB2b, fetching]) => {
if (!option || fetching) {
return false;
}
// Select immer sichtbar bei ausgewählten Items
if (items?.find((item) => item.product?.catalogProductNumber === this.item.product?.catalogProductNumber)) {
return true;
}
// Select nur anzeigen, wenn ein anderes ausgewähltes Item die gleiche Verfügbarkeit hat (B2B Versand z.B.)
if (items?.length > 0 && option === 'delivery' && canAdd[this.item.product.catalogProductNumber]?.status < 2) {
if (items.every((item) => delivery[item.product?.catalogProductNumber]) && delivery[this.item.product?.catalogProductNumber]) {
return true;
}
if (
items.every((item) => deliveryDig[item.product?.catalogProductNumber]) &&
deliveryDig[this.item.product?.catalogProductNumber]
) {
return true;
}
if (
items.every((item) => deliveryB2b[item.product?.catalogProductNumber]) &&
deliveryB2b[this.item.product?.catalogProductNumber]
) {
return true;
}
return false;
}
return canAdd && canAdd[this.item.product.catalogProductNumber]?.status < 2;
})
);
canAdd$ = this._store.canAdd$.pipe(
filter((canAdd) => !!this.item && !!canAdd),
map((canAdd) => !!canAdd[this.item.product.catalogProductNumber]?.message)
);
quantityRange$ = combineLatest([this._store.selectedFilterOption$, this.takeAwayAvailabilities$]).pipe(
map(([option, availability]) => (option === 'take-away' ? (availability as AvailabilityDTO)?.inStock : 999))
);
constructor(private _store: PurchasingOptionsListModalStore, private _availabilityService: DomainAvailabilityService) {}
selected(value: boolean) {
this._store.selectShoppingCartItem([this.item], value);
}
changeQuantity(quantity: number) {
if (quantity === 0) {
this._store.removeShoppingCartItem(this.item);
} else {
this._store.updateItemQuantity({ itemId: this.item.id, quantity });
this._store.loadAvailabilities({ items: [{ ...this.item, quantity }] });
}
}
}

View File

@@ -1,49 +0,0 @@
<div class="options">
<page-take-away-option-list></page-take-away-option-list>
<page-pick-up-option-list></page-pick-up-option-list>
<page-delivery-option-list></page-delivery-option-list>
</div>
<div class="items" *ngIf="shoppingCartItems$ | async; let shoppingCartItems">
<div class="item-actions">
<ng-container>
<button
*ngIf="!(allShoppingCartItemsSelected$ | async); else unselectAll"
class="cta-select-all"
[disabled]="selectAllCtaDisabled$ | async"
(click)="selectAll(shoppingCartItems, true)"
>
Alle auswählen
</button>
<ng-template #unselectAll>
<button class="cta-select-all" [disabled]="selectAllCtaDisabled$ | async" (click)="selectAll(shoppingCartItems, false)">
Alle abwählen
</button>
</ng-template>
</ng-container>
<br />
{{ (selectedShoppingCartItems$ | async)?.length || 0 }} von {{ shoppingCartItems?.length || 0 }} Artikeln
</div>
<div class="item-list scroll-bar" *ngIf="shoppingCartItems?.length > 0; else emptyMessage">
<hr />
<ng-container *ngFor="let item of shoppingCartItems">
<page-purchasing-options-list-item [item]="item"></page-purchasing-options-list-item>
<hr />
</ng-container>
</div>
<ng-template #emptyMessage>
<div class="empty-message">Keine Artikel für die ausgewählte Kaufoption verfügbar</div>
</ng-template>
</div>
<div class="actions">
<button class="cta-apply" [disabled]="applyCtaDisabled$ | async" (click)="apply()">
<ui-spinner [show]="addItemsLoader$ | async">
Übernehmen
</ui-spinner>
</button>
</div>

View File

@@ -1,49 +0,0 @@
:host {
@apply block box-border;
}
.options {
@apply flex flex-row box-border text-center justify-center mt-4;
}
.items {
min-height: 440px;
.item-actions {
@apply text-right;
.cta-select-all {
@apply text-brand bg-transparent text-base font-bold outline-none border-none px-4 py-4 -mr-4;
&:disabled {
@apply text-inactive-branch;
}
}
}
.item-list {
@apply overflow-y-scroll overflow-x-hidden -ml-4;
max-height: calc(100vh - 580px);
width: calc(100% + 2rem);
page-purchasing-options-list-item {
@apply px-4;
}
}
.empty-message {
@apply text-inactive-branch my-8 text-center font-bold;
}
}
.actions {
@apply flex justify-center mt-8;
.cta-apply {
@apply text-white border-2 border-solid border-brand bg-brand font-bold text-lg px-4 py-2 rounded-full;
&:disabled {
@apply bg-inactive-branch border-inactive-branch;
}
}
}

View File

@@ -1,266 +0,0 @@
import { Component, ChangeDetectionStrategy, OnInit } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { ShoppingCartItemDTO, UpdateShoppingCartItemDTO } from '@swagger/checkout';
import { UiErrorModalComponent, UiModalRef, UiModalService } from '@ui/modal';
import { BehaviorSubject, combineLatest, Subject } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, takeUntil, withLatestFrom } from 'rxjs/operators';
import { PurchasingOptionsListModalData } from './purchasing-options-list-modal.data';
import { PurchasingOptionsListModalStore } from './purchasing-options-list-modal.store';
@Component({
selector: 'page-purchasing-options-list-modal',
templateUrl: 'purchasing-options-list-modal.component.html',
styleUrls: ['purchasing-options-list-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [PurchasingOptionsListModalStore],
})
export class PurchasingOptionsListModalComponent implements OnInit {
private _onDestroy$ = new Subject();
addItemsLoader$ = new BehaviorSubject<boolean>(false);
shoppingCartItems$ = combineLatest([
this._store.fetchingAvailabilities$,
this._store.selectedFilterOption$,
this._store.shoppingCartItems$,
]).pipe(
withLatestFrom(
this._store.takeAwayAvailabilities$,
this._store.pickUpAvailabilities$,
this._store.deliveryAvailabilities$,
this._store.deliveryDigAvailabilities$,
this._store.deliveryB2bAvailabilities$
),
map(
([
[_, selectedFilterOption, shoppingCartItems],
takeAwayAvailability,
pickUpAvailability,
deliveryAvailability,
deliveryDigAvailability,
deliveryB2bAvailability,
]) => {
if (!!takeAwayAvailability && !!pickUpAvailability && !!deliveryAvailability) {
switch (selectedFilterOption) {
case 'take-away':
return shoppingCartItems.filter((item) => !!takeAwayAvailability[item.product?.catalogProductNumber]);
case 'pick-up':
return shoppingCartItems.filter((item) => !!pickUpAvailability[item.product?.catalogProductNumber]);
case 'delivery':
return shoppingCartItems.filter(
(item) =>
!!deliveryAvailability[item.product?.catalogProductNumber] ||
!!deliveryDigAvailability[item.product?.catalogProductNumber] ||
!!deliveryB2bAvailability[item.product?.catalogProductNumber]
);
}
}
return shoppingCartItems;
}
),
map((shoppingCartItems) => shoppingCartItems?.sort((a, b) => a.product?.name.localeCompare(b.product?.name))),
shareReplay()
);
selectedShoppingCartItems$ = this._store.selectedShoppingCartItems$;
allShoppingCartItemsSelected$ = combineLatest([this.shoppingCartItems$, this.selectedShoppingCartItems$]).pipe(
map(
([shoppingCartItems, selectedShoppingCartItems]) =>
shoppingCartItems.every((item) => selectedShoppingCartItems.find((i) => item.id === i.id)) && shoppingCartItems?.length > 0
)
);
canAddItems$ = this._store.canAdd$.pipe(
map((canAdd) => {
for (const key in canAdd) {
if (Object.prototype.hasOwnProperty.call(canAdd, key)) {
if (!!canAdd[key]?.message) {
return false;
}
}
}
return true;
}),
shareReplay()
);
selectAllCtaDisabled$ = combineLatest([this._store.selectedFilterOption$, this.canAddItems$]).pipe(
withLatestFrom(this.shoppingCartItems$),
map(([[selectedFilterOption, canAddItems], items]) => !selectedFilterOption || items?.length === 0 || !canAddItems)
);
applyCtaDisabled$ = combineLatest([this.addItemsLoader$, this._store.selectedFilterOption$, this._store.selectedShoppingCartItems$]).pipe(
withLatestFrom(this.shoppingCartItems$),
map(
([[addItemsLoader, selectedFilterOption, selectedShoppingCartItems], shoppingCartItems]) =>
addItemsLoader || !selectedFilterOption || shoppingCartItems?.length === 0 || selectedShoppingCartItems?.length === 0
)
);
constructor(
private _modalRef: UiModalRef<any, PurchasingOptionsListModalData>,
private _modal: UiModalService,
private _store: PurchasingOptionsListModalStore,
private _availability: DomainAvailabilityService,
private _checkout: DomainCheckoutService
) {
this._store.shoppingCartItems = _modalRef.data.shoppingCartItems;
this._store.customerFeatures = _modalRef.data.customerFeatures;
this._store.processId = _modalRef.data.processId;
}
ngOnInit() {
this._store.loadBranches();
// Beim Wechsel der ausgewählten Filteroption oder der Branches die Auswahl leeren
combineLatest([this._store.selectedFilterOption$, this._store.selectedTakeAwayBranch$, this._store.selectedPickUpBranch$])
.pipe(takeUntil(this._onDestroy$))
.subscribe(() => this._store.clearSelectedShoppingCartItems());
this._store.selectedFilterOption$
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.shoppingCartItems$))
.subscribe(([option, items]) => this.checkCanAdd(option, items));
this._store.fetchingAvailabilities$
.pipe(
takeUntil(this._onDestroy$),
debounceTime(250),
filter((fetching) => !fetching),
withLatestFrom(this.shoppingCartItems$, this._store.selectedFilterOption$)
)
.subscribe(([_, items, option]) => this.checkCanAdd(option, items));
this.canAddItems$
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this.shoppingCartItems$, this._store.selectedFilterOption$))
.subscribe(([showSelectAll, items, option]) => {
if (items?.length > 0 && this._store.lastSelectedFilterOption$.value !== option) {
this.selectAll(items, showSelectAll && !!option);
}
// Nach dem Übernehmen von Items wird eine neue CanAdd Abfrage ausgeführt, in diesem Fall soll aber nicht alles ausgewählt werden
this._store.lastSelectedFilterOption$.next(option);
});
}
checkCanAdd(selectedFilterOption: string, items: ShoppingCartItemDTO[]) {
if (!!selectedFilterOption && items?.length > 0) {
this._store.checkCanAddItems(items);
} else {
this._store.patchState({ canAdd: {} });
}
}
async selectAll(items: ShoppingCartItemDTO[], value: boolean) {
this._store.selectShoppingCartItem(items, value);
}
async apply() {
this.addItemsLoader$.next(true);
try {
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
const items = await this._store.selectedShoppingCartItems$.pipe(first()).toPromise();
const takeAwayAvailabilities = await this._store.takeAwayAvailabilities$.pipe(first()).toPromise();
const pickupAvailabilities = await this._store.pickUpAvailabilities$.pipe(first()).toPromise();
const deliveryAvailabilities = await this._store.deliveryAvailabilities$.pipe(first()).toPromise();
const deliveryB2bAvailabilities = await this._store.deliveryB2bAvailabilities$.pipe(first()).toPromise();
const deliveryDigAvailabilities = await this._store.deliveryDigAvailabilities$.pipe(first()).toPromise();
const selectedTakeAwayBranch = await this._store.selectedTakeAwayBranch$.pipe(first()).toPromise();
const selectedPickUpBranch = await this._store.selectedPickUpBranch$.pipe(first()).toPromise();
let option = this._store.selectedFilterOption;
for (const item of items) {
let availability;
switch (this._store.selectedFilterOption) {
case 'take-away':
availability = takeAwayAvailabilities[item.product.catalogProductNumber];
break;
case 'pick-up':
availability = pickupAvailabilities[item.product.catalogProductNumber];
break;
case 'delivery':
if (
deliveryDigAvailabilities[item.product.catalogProductNumber] &&
deliveryB2bAvailabilities[item.product.catalogProductNumber] &&
deliveryAvailabilities[item.product.catalogProductNumber]
) {
availability = deliveryAvailabilities[item.product.catalogProductNumber];
} else if (deliveryDigAvailabilities[item.product.catalogProductNumber]) {
availability = deliveryDigAvailabilities[item.product.catalogProductNumber];
} else if (deliveryB2bAvailabilities[item.product.catalogProductNumber]) {
availability = deliveryB2bAvailabilities[item.product.catalogProductNumber];
option = 'b2b-delivery';
}
break;
}
const price = this._availability.getPriceForAvailability(option, item.availability, availability);
// Negative Preise und nicht vorhandene Availability ignorieren
if (price?.value?.value < 0 || !availability) {
continue;
}
const updateItem: UpdateShoppingCartItemDTO = {
quantity: item.quantity,
availability: {
...availability,
price: price ? price : item.unitPrice,
},
promotion: item?.promotion?.points ? { points: item.promotion.points } : undefined,
};
switch (this._store.selectedFilterOption) {
case 'take-away':
updateItem.destination = {
data: { target: 1, targetBranch: { id: selectedTakeAwayBranch.id } },
};
break;
case 'pick-up':
updateItem.destination = {
data: { target: 1, targetBranch: { id: selectedPickUpBranch.id } },
};
break;
case 'delivery':
case 'dig-delivery':
case 'b2b-delivery':
updateItem.destination = {
data: { target: 2, logistician: availability?.logistician },
};
break;
}
await this._checkout
.updateItemInShoppingCart({
processId: this._modalRef.data.processId,
shoppingCartItemId: item.id,
update: {
...updateItem,
},
})
.toPromise();
}
const remainingItems = shoppingCartItems.filter((i) => !items.find((j) => i.id === j.id));
this._store.shoppingCartItems = [...remainingItems];
this._store.clearSelectedShoppingCartItems();
if (remainingItems?.length === 0) {
this._modalRef.close();
}
} catch (error) {
console.error(error);
this._modal.open({ content: UiErrorModalComponent, data: error, title: 'Fehler beim Hinzufügen zum Warenkorb' });
} finally {
this.addItemsLoader$.next(false);
}
const shoppingCartItems = await this.shoppingCartItems$.pipe(first()).toPromise();
if (shoppingCartItems?.length > 0) {
this._store.checkCanAddItems(shoppingCartItems);
}
}
}

View File

@@ -1,7 +0,0 @@
import { ShoppingCartItemDTO } from '@swagger/checkout';
export interface PurchasingOptionsListModalData {
processId: number;
shoppingCartItems?: ShoppingCartItemDTO[];
customerFeatures: { [key: string]: string };
}

View File

@@ -1,41 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PurchasingOptionsListModalComponent } from './purchasing-options-list-modal.component';
import { UiIconModule } from '@ui/icon';
import { ProductImageModule } from '@cdn/product-image';
import { UiCommonModule } from '@ui/common';
import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { PickUpOptionListComponent } from './pick-up-option/pick-up-option-list.component';
import { TakeAwayOptionListComponent } from './take-away-option/take-away-option-list.component';
import { DeliveryOptionListComponent } from './delivery-option/delivery-option-list.component';
import { PurchasingOptionsListItemComponent } from './purchasing-options-list-item/purchasing-options-list-item.component';
import { FormsModule } from '@angular/forms';
import { UiBranchDropdownModule } from '@ui/branch-dropdown';
import { UiTooltipModule } from '@ui/tooltip';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
UiCommonModule,
UiIconModule,
UiSelectBulletModule,
UiQuantityDropdownModule,
ProductImageModule,
UiBranchDropdownModule,
UiTooltipModule,
UiSpinnerModule,
],
exports: [PurchasingOptionsListModalComponent],
declarations: [
PurchasingOptionsListModalComponent,
PurchasingOptionsListItemComponent,
PickUpOptionListComponent,
TakeAwayOptionListComponent,
DeliveryOptionListComponent,
],
})
export class PurchasingOptionsListModalModule {}

View File

@@ -1,598 +0,0 @@
import { Injectable } from '@angular/core';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { AvailabilityDTO, BranchDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { map, mergeMap, switchMap, withLatestFrom } from 'rxjs/operators';
import { DomainAvailabilityService } from '@domain/availability';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { DomainCheckoutService } from '@domain/checkout';
import { ApplicationService } from '@core/application';
interface PurchasingOptionsListModalState {
processId: number;
shoppingCartItems: ShoppingCartItemDTO[];
selectedFilterOption: string;
takeAwayAvailabilities: { [key: string]: AvailabilityDTO | true };
pickUpAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryB2bAvailabilities: { [key: string]: AvailabilityDTO | true };
deliveryDigAvailabilities: { [key: string]: AvailabilityDTO | true };
customerFeatures: { [key: string]: string };
canAdd: { [key: string]: { message: string; status: number } };
selectedShoppingCartItems: ShoppingCartItemDTO[];
branches: BranchDTO[];
currentBranch: BranchDTO;
selectedTakeAwayBranch: BranchDTO;
selectedPickUpBranch: BranchDTO;
}
@Injectable()
export class PurchasingOptionsListModalStore extends ComponentStore<PurchasingOptionsListModalState> {
lastSelectedFilterOption$ = new BehaviorSubject<string>(undefined);
branches$ = this.select((s) => s.branches);
currentBranch$ = this.select((s) => s.currentBranch);
takeAwayAvailabilities$ = this.select((s) => s.takeAwayAvailabilities);
pickUpAvailabilities$ = this.select((s) => s.pickUpAvailabilities);
deliveryAvailabilities$ = this.select((s) => s.deliveryAvailabilities);
deliveryB2bAvailabilities$ = this.select((s) => s.deliveryB2bAvailabilities);
canAdd$ = this.select((s) => s.canAdd);
deliveryDigAvailabilities$ = this.select((s) => s.deliveryDigAvailabilities);
shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
shoppingCartItems = shoppingCartItems.sort((a, b) => a.product?.name.localeCompare(b.product.name));
this.patchState({ shoppingCartItems });
}
processId$ = this.select((s) => s.processId);
set processId(processId: number) {
this.patchState({ processId });
}
customerFeatures$ = this.select((s) => s.customerFeatures);
set customerFeatures(customerFeatures: { [key: string]: string }) {
this.patchState({ customerFeatures });
}
selectedFilterOption$ = this.select((s) => s.selectedFilterOption);
set selectedFilterOption(selectedFilterOption: string) {
this.patchState({ selectedFilterOption });
}
get selectedFilterOption() {
return this.get((s) => s.selectedFilterOption);
}
selectedShoppingCartItems$ = this.select((s) => s.selectedShoppingCartItems);
get selectedShoppingCartItems() {
return this.get((s) => s.selectedShoppingCartItems);
}
selectedTakeAwayBranch$ = this.select((s) => s.selectedTakeAwayBranch);
set selectedTakeAwayBranch(selectedTakeAwayBranch: BranchDTO) {
this.patchState({ selectedTakeAwayBranch });
}
selectedPickUpBranch$ = this.select((s) => s.selectedPickUpBranch);
set selectedPickUpBranch(selectedPickUpBranch: BranchDTO) {
this.patchState({ selectedPickUpBranch });
}
fetchingAvailabilities$ = combineLatest([this.takeAwayAvailabilities$, this.pickUpAvailabilities$, this.deliveryAvailabilities$]).pipe(
map(([takeAway, pickUp, delivery]) => {
const fetchingCheck = (obj) => {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const element = obj[key];
if (typeof element === 'boolean') {
return true;
}
}
}
return false;
};
return !takeAway || fetchingCheck(takeAway) || !pickUp || fetchingCheck(pickUp) || !delivery || fetchingCheck(delivery);
})
);
constructor(
private _availabilityService: DomainAvailabilityService,
private _checkoutService: DomainCheckoutService,
private _application: ApplicationService
) {
super({
processId: undefined,
shoppingCartItems: [],
selectedFilterOption: undefined,
pickUpAvailabilities: undefined,
deliveryAvailabilities: undefined,
takeAwayAvailabilities: undefined,
deliveryB2bAvailabilities: undefined,
deliveryDigAvailabilities: undefined,
selectedShoppingCartItems: [],
branches: [],
currentBranch: undefined,
selectedTakeAwayBranch: undefined,
selectedPickUpBranch: undefined,
customerFeatures: undefined,
canAdd: undefined,
});
}
loadAvailabilities(options: { items?: ShoppingCartItemDTO[] }) {
const shoppingCartItems = options.items ?? this.get((s) => s.shoppingCartItems);
for (const item of shoppingCartItems) {
this.loadTakeAwayAvailability({ item });
this.loadPickUpAvailability({ item });
this.loadDeliveryAvailability({ item });
this.loadDeliveryB2bAvailability({ item });
this.loadDeliveryDigAvailability({ item });
}
}
readonly setAvailabilityFetching = this.updater((state, { name, id, fetching }: { name: string; id: string; fetching?: boolean }) => {
const availability = { ...state[name] };
if (fetching) {
availability[id] = fetching;
} else {
delete availability[id];
}
return {
...state,
[name]: {
...availability,
},
};
});
readonly setAvailability = this.updater((state, { name, availability }: { name: string; availability: any }) => {
const av = { ...state[name] };
if (this._availabilityService.isAvailable({ availability })) {
av[availability.itemId] = availability;
}
return {
...state,
[name]: av,
};
});
loadPickUpAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
withLatestFrom(this.selectedPickUpBranch$),
mergeMap(([options, branch]) => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getPickUpAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
branch,
quantity: options.item.quantity,
})
.pipe(
map((av) => {
if (av?.length > 0) {
if (av[1].availableFor) {
if ((av[1].availableFor & 2) === 2) {
return av[0];
} else {
return undefined;
}
} else {
return av[0];
}
}
}),
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'pickUpAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'pickUpAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'pickUpAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryB2bAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getB2bDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryB2bAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryB2bAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryB2bAvailabilities', availability: {} });
}
)
);
})
)
);
loadDeliveryDigAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
mergeMap((options) => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getDigDeliveryAvailability({
item: {
itemId: +options.item.product.catalogProductNumber,
ean: options.item.product.ean,
price: options.item.availability.price,
},
quantity: options.item.quantity,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'deliveryDigAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'deliveryDigAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'deliveryDigAvailabilities', availability: {} });
}
)
);
})
)
);
loadTakeAwayAvailability = this.effect((options$: Observable<{ item?: ShoppingCartItemDTO }>) =>
options$.pipe(
withLatestFrom(this.selectedTakeAwayBranch$),
mergeMap(([options, branch]) => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: true,
});
return this._availabilityService
.getTakeAwayAvailabilityByBranch({
itemId: +options.item.product.catalogProductNumber,
price: options.item.availability.price,
quantity: options.item.quantity,
branch,
})
.pipe(
tapResponse(
(availability) => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
});
this.setAvailability({
name: 'takeAwayAvailabilities',
availability: { ...availability, itemId: options.item.product.catalogProductNumber },
});
},
() => {
this.setAvailabilityFetching({
name: 'takeAwayAvailabilities',
id: options.item.product.catalogProductNumber,
fetching: false,
});
this.setAvailability({ name: 'takeAwayAvailabilities', availability: {} });
}
)
);
})
)
);
getCurrentBranch() {
return combineLatest([this._application.getSelectedBranch$(), this._availabilityService.getDefaultBranch()]).pipe(
map(([selectedBranch, defaultBranch]) => selectedBranch || defaultBranch)
);
}
loadBranches = this.effect(($) =>
$.pipe(
switchMap(() =>
this._availabilityService.getBranches().pipe(
map((branches) =>
branches.filter(
(branch) => branch.status === 1 && branch.branchType === 1 && branch.isOnline === true && branch.isShippingEnabled === true
)
),
withLatestFrom(this.getCurrentBranch()),
tapResponse(
([branches, currentBranch]) => {
this.patchState({
branches,
selectedTakeAwayBranch: currentBranch,
selectedPickUpBranch: currentBranch,
currentBranch,
});
this.loadAvailabilities({});
},
() =>
this.patchState({
branches: [],
selectedTakeAwayBranch: undefined,
selectedPickUpBranch: undefined,
currentBranch: undefined,
})
)
)
)
)
);
checkCanAddItems = this.effect((items$: Observable<ShoppingCartItemDTO[]>) =>
items$.pipe(
withLatestFrom(
this.processId$,
this.selectedFilterOption$,
this.takeAwayAvailabilities$,
this.pickUpAvailabilities$,
this.deliveryAvailabilities$,
this.deliveryB2bAvailabilities$,
this.deliveryDigAvailabilities$
),
mergeMap(([items, processId, selectedOption, takeAway, pickUp, delivery, deliveryB2b, deliveryDig]) => {
let orderType: string;
const payload = items.map((item) => {
switch (selectedOption) {
case 'take-away':
orderType = 'Rücklage';
return {
availabilities: [this.getOlaAvailability(takeAway[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
case 'pick-up':
orderType = 'Abholung';
return {
availabilities: [this.getOlaAvailability(pickUp[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
case 'delivery':
orderType = 'Versand';
if (
deliveryDig[item.product.catalogProductNumber] &&
deliveryB2b[item.product.catalogProductNumber] &&
delivery[item.product.catalogProductNumber]
) {
return {
availabilities: [this.getOlaAvailability(delivery[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
} else if (deliveryDig[item.product.catalogProductNumber]) {
return {
availabilities: [this.getOlaAvailability(deliveryDig[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
} else if (deliveryB2b[item.product.catalogProductNumber]) {
return {
availabilities: [this.getOlaAvailability(deliveryB2b[item.product.catalogProductNumber], item)],
id: item.product.catalogProductNumber,
};
}
break;
}
});
return this._checkoutService.canAddItems({ processId, payload, orderType }).pipe(
tapResponse(
(result: any) => {
const canAdd = {};
result?.forEach((r) => {
canAdd[r.id] = { message: r.message, status: r.status };
});
this.patchState({ canAdd });
},
(error: Error) => {
const canAdd = {};
items?.forEach((i) => {
canAdd[i.product?.catalogProductNumber] = { message: error?.message };
});
this.patchState({ canAdd });
}
)
);
})
)
);
getOlaAvailability(availability: AvailabilityDTO, item: ShoppingCartItemDTO) {
return {
qty: item.quantity,
ean: item.product.ean,
itemId: item.product.catalogProductNumber,
format: item.product.format,
at: availability?.estimatedShippingDate,
isPrebooked: availability?.isPrebooked,
status: availability?.availabilityType,
logisticianId: availability?.logistician?.id,
price: availability?.price,
ssc: availability?.ssc,
sscText: availability?.sscText,
supplierId: availability?.supplier?.id,
};
}
readonly updateItemQuantity = this.updater((state, value: { itemId: number; quantity: number }) => {
const itemToUpdate = state.shoppingCartItems.find((item) => item.id === value.itemId);
const otherItems = state.shoppingCartItems.filter((item) => item.id !== value.itemId);
const updatedItem = { ...itemToUpdate, quantity: value.quantity };
const shoppingCartItems = [...otherItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name));
// Ausgewählte Items auch aktualisieren
let selectedShoppingCartItems = state.selectedShoppingCartItems;
if (state.selectedShoppingCartItems.find((item) => item.id === value.itemId)) {
const selectedItems = state.selectedShoppingCartItems.filter((item) => item.id !== value.itemId);
selectedShoppingCartItems = [...selectedItems, updatedItem].sort((a, b) => a.product?.name.localeCompare(b.product.name));
}
return {
...state,
shoppingCartItems,
selectedShoppingCartItems,
};
});
async removeShoppingCartItem(item: ShoppingCartItemDTO) {
const items = this.get((s) => s.shoppingCartItems);
const processId = this.get((s) => s.processId);
await this._checkoutService
.updateItemInShoppingCart({
processId,
shoppingCartItemId: item.id,
update: {
quantity: 0,
availability: null,
},
})
.toPromise();
this.selectShoppingCartItem([item], false);
const shoppingCartItems = items.filter((i) => i.id !== item.id);
this.patchState({ shoppingCartItems });
}
selectShoppingCartItem(shoppingCartItems: ShoppingCartItemDTO[], selected: boolean) {
if (selected) {
this.patchState({
selectedShoppingCartItems: [
...this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)),
...shoppingCartItems,
],
});
} else {
this.patchState({
selectedShoppingCartItems: this.selectedShoppingCartItems.filter((item) => !shoppingCartItems.find((i) => item.id === i.id)),
});
}
}
clearSelectedShoppingCartItems() {
this.patchState({ selectedShoppingCartItems: [] });
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './take-away-option-list.component';
// end:ng42.barrel

View File

@@ -1,18 +0,0 @@
<div class="option-icon">
<ui-icon size="50px" icon="shopping_bag"></ui-icon>
</div>
<button
class="option-chip"
[disabled]="optionChipDisabled$ | async"
(click)="optionChange('take-away')"
[class.selected]="(selectedOption$ | async) === 'take-away'"
>
Rücklage
</button>
<p>Möchten Sie die Artikel<br />zurücklegen lassen oder<br />sofort mitnehmen?</p>
<ui-branch-dropdown
[branches]="branches$ | async"
[selected]="selectedBranch$ | async"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>

View File

@@ -1,48 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BranchDTO } from '@swagger/checkout';
import { combineLatest } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { PurchasingOptionsListModalStore } from '../purchasing-options-list-modal.store';
@Component({
selector: 'page-take-away-option-list',
templateUrl: 'take-away-option-list.component.html',
styleUrls: ['../list-options.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TakeAwayOptionListComponent {
branches$ = this._store.branches$;
selectedBranch$ = this._store.selectedTakeAwayBranch$.pipe(
map((branch) => {
// Determins if branch is targetBranch
if (branch?.branchType === 1) {
return branch.name;
}
})
);
selectedOption$ = this._store.selectedFilterOption$;
optionChipDisabled$ = combineLatest([this._store.fetchingAvailabilities$, this.selectedBranch$]).pipe(
map(([fetching, selectedBranch]) => {
return fetching || !selectedBranch;
})
);
constructor(private _store: PurchasingOptionsListModalStore) {}
optionChange(option: string) {
if (this._store.selectedFilterOption === option) {
this._store.selectedFilterOption = undefined;
} else {
this._store.selectedFilterOption = option;
}
}
async selectBranch(branch: BranchDTO) {
this._store.lastSelectedFilterOption$.next(undefined);
this._store.selectedTakeAwayBranch = branch;
const shoppingCartItems = await this._store.shoppingCartItems$.pipe(first()).toPromise();
shoppingCartItems.forEach((item) => this._store.loadTakeAwayAvailability({ item }));
}
}

View File

@@ -1,23 +0,0 @@
<ng-container *ngIf="item$ | async; let item">
<ng-container *ngIf="availability$ | async; let availability">
<div class="option-icon">
<ui-icon size="80px" icon="truck_b2b"></ui-icon>
</div>
<h4>B2B Versand</h4>
<p>
Als B2B Kunde können wir Ihnen den Artikel auch liefern.
</p>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<div class="grow"></div>
<span class="delivery">Versandkostenfrei</span>
<span class="date"
>Versanddatum <strong>{{ availability?.estimatedShippingDate | date: 'shortDate' }}</strong></span
>
<div>
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
Auswählen
</button>
</div>
</ng-container>
</ng-container>

View File

@@ -1,9 +0,0 @@
.option-icon {
margin-top: -12px;
width: 70px;
}
h4 {
@apply font-bold;
margin-top: -2px;
}

View File

@@ -1,27 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@Component({
selector: 'page-b2b-delivery-option',
templateUrl: 'b2b-delivery-option.component.html',
styleUrls: ['../option.scss', 'b2b-delivery-option.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class B2BDeliveryOptionComponent {
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['b2b-delivery']));
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('b2b-delivery', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this._purchasingOptionsModalStore.setOption('b2b-delivery');
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './b2b-delivery-option.component';
// end:ng42.barrel

View File

@@ -1,41 +0,0 @@
<ng-container *ngIf="item$ | async; let item">
<ng-container *ngIf="availability$ | async; let availability">
<div class="option-icon">
<ui-icon size="50px" icon="truck"></ui-icon>
</div>
<h4>Versand</h4>
<p>
Möchten Sie den Artikel geliefert bekommen?
</p>
<div class="price-wrapper">
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<ng-container *ngIf="showTooltip$ | async">
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
i
</button>
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
Günstigerer Preis aus Hugendubel Katalog wird übernommen
</ui-tooltip>
</ng-container>
</div>
<div class="grow"></div>
<span class="delivery">Versandkostenfrei</span>
<span *ngIf="availability?.estimatedDelivery; else estimatedShippingDateTmpl" class="date">
Zustellung zwischen <br />
<strong
>{{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}</strong
>
</span>
<ng-template #estimatedShippingDateTmpl>
<span class="date">
Versanddatum <strong>{{ availability?.estimatedShippingDate | date }}</strong>
</span>
</ng-template>
<div>
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
Auswählen
</button>
</div>
</ng-container>
</ng-container>

View File

@@ -1,15 +0,0 @@
.price-wrapper {
@apply mt-2;
}
.info-tooltip-button {
@apply border-font-customer border-solid border-2 bg-white rounded-full text-base font-bold;
border-style: outset;
width: 31px;
height: 31px;
margin-left: 10px;
}
h4 {
@apply font-bold;
}

View File

@@ -1,35 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@Component({
selector: 'page-delivery-option',
templateUrl: 'delivery-option.component.html',
styleUrls: ['../option.scss', 'delivery-option.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DeliveryOptionComponent {
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['delivery']));
readonly showTooltip$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => {
const shippingPrice = availability?.price?.value?.value;
const catalogPrice = item?.catalogAvailability?.price?.value?.value;
return catalogPrice < shippingPrice;
})
);
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('delivery', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this._purchasingOptionsModalStore.setOption('delivery');
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './delivery-option.component';
// end:ng42.barrel

View File

@@ -1,40 +0,0 @@
<ng-container *ngIf="item$ | async; let item">
<ng-container *ngIf="availability$ | async; let availability">
<div class="option-icon">
<ui-icon size="50px" icon="truck"></ui-icon>
</div>
<h4>DIG Versand</h4>
<p>Möchten Sie den Artikel geliefert bekommen?</p>
<div class="price-wrapper">
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<ng-container *ngIf="showTooltip$ | async">
<button [uiOverlayTrigger]="tooltipContent" #tooltip="uiOverlayTrigger" class="info-tooltip-button" type="button">
i
</button>
<ui-tooltip #tooltipContent yPosition="above" xPosition="after" [yOffset]="-16">
Günstigerer Preis aus Hugendubel Katalog wird übernommen
</ui-tooltip>
</ng-container>
</div>
<div class="grow"></div>
<span class="delivery">Versandkostenfrei</span>
<span *ngIf="availability?.estimatedDelivery; else estimatedShippingDateTmpl" class="date">
Zustellung zwischen <br />
<strong
>{{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}</strong
>
</span>
<ng-template #estimatedShippingDateTmpl>
<span class="date">
Versanddatum <strong>{{ availability?.estimatedShippingDate | date }}</strong>
</span>
</ng-template>
<div>
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
Auswählen
</button>
</div>
</ng-container>
</ng-container>

View File

@@ -1,15 +0,0 @@
.price-wrapper {
@apply mt-2;
}
.info-tooltip-button {
@apply border-font-customer bg-white rounded-full text-base font-bold;
border-style: outset;
width: 31px;
height: 31px;
margin-left: 10px;
}
h4 {
@apply font-bold;
}

View File

@@ -1,35 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@Component({
selector: 'page-dig-delivery-option',
templateUrl: 'dig-delivery-option.component.html',
styleUrls: ['../option.scss', 'dig-delivery-option.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DigDeliveryOptionComponent {
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['dig-delivery']));
readonly showTooltip$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => {
const shippingPrice = availability?.price?.value?.value;
const catalogPrice = item?.catalogAvailability?.price?.value?.value;
return catalogPrice < shippingPrice;
})
);
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('dig-delivery', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this._purchasingOptionsModalStore.setOption('dig-delivery');
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './dig-delivery-option.component';
// end:ng42.barrel

View File

@@ -1,6 +0,0 @@
// start:ng42.barrel
export * from './options';
export * from './purchasing-options-modal.component';
export * from './purchasing-options-modal.data';
export * from './purchasing-options-modal.module';
// end:ng42.barrel

View File

@@ -1,41 +0,0 @@
:host {
@apply flex flex-col box-border text-center;
width: 202px;
}
.option-icon {
@apply text-ucla-blue mx-auto;
width: 40px;
}
h4 {
@apply text-2xl mt-4 mb-0;
}
p {
@apply my-2;
}
.price {
@apply font-bold my-2;
}
.delivery {
@apply text-regular mb-px-5;
}
.date {
@apply text-cta-l whitespace-nowrap;
}
.grow {
@apply flex-grow;
}
.select-option {
@apply mt-4 mb-4 border-2 border-solid border-brand text-brand text-cta-l font-bold bg-white rounded-full py-3 px-6;
}
.select-option:disabled {
@apply bg-disabled-branch border-disabled-branch text-white;
}

View File

@@ -1,7 +0,0 @@
// start:ng42.barrel
export * from './b2b-delivery-option';
export * from './delivery-option';
export * from './dig-delivery-option';
export * from './pick-up-option';
export * from './take-away-option';
// end:ng42.barrel

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './pick-up-option.component';
// end:ng42.barrel

View File

@@ -1,32 +0,0 @@
<ng-container *ngIf="item$ | async; let item">
<ng-container *ngIf="availability$ | async; let availability">
<div class="option-icon">
<ui-icon size="50px" icon="box_out"></ui-icon>
</div>
<h4>Abholung</h4>
<p>
Möchten Sie den Artikel in einer unserer Filialen abholen?
</p>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<ui-branch-dropdown
class="min-h-[38px]"
[branches]="branches$ | async"
[selected]="selected$ | async"
(selectBranch)="selectBranch($event)"
></ui-branch-dropdown>
<span class="date"
>Abholung ab <strong>{{ (availability$ | async)?.estimatedShippingDate | date: 'shortDate' }}</strong></span
>
<div class="grow"></div>
<div>
<button
[disabled]="availability.price?.value?.value < 0 || !(selected$ | async)"
type="button"
class="select-option"
(click)="select()"
>
Auswählen
</button>
</div>
</ng-container>
</ng-container>

View File

@@ -1,3 +0,0 @@
h4 {
@apply font-bold;
}

View File

@@ -1,42 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { BranchDTO } from '@swagger/checkout';
import { combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@Component({
selector: 'page-pick-up-option',
templateUrl: 'pick-up-option.component.html',
styleUrls: ['../option.scss', 'pick-up-option.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PickUpOptionComponent {
branches$: Observable<BranchDTO[]> = this._purchasingOptionsModalStore.selectAvailableBranches;
selected$: Observable<string> = this._purchasingOptionsModalStore.selectBranch.pipe(
map((branch) => {
// Determins if branch is targetBranch
if (branch?.branchType === 1) {
return branch.name;
}
})
);
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['pick-up']));
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('pick-up', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this._purchasingOptionsModalStore.setOption('pick-up');
}
selectBranch(branch: BranchDTO) {
this._purchasingOptionsModalStore.setBranch(branch);
}
}

View File

@@ -1,11 +0,0 @@
<form *ngIf="control" [formGroup]="control">
<ui-form-control label="MwSt" variant="default" *ngIf="!hideVat">
<ui-select formControlName="vat">
<ui-select-option *ngFor="let vat of vats$ | async" [label]="vat.name + '%'" [value]="vat.vatType"> </ui-select-option>
</ui-select>
</ui-form-control>
<ui-form-control class="price" label="Preis" variant="default">
<input uiInput formControlName="price" [max]="maxValue" maxLength="6" />
</ui-form-control>
</form>

View File

@@ -1,11 +0,0 @@
form {
@apply grid grid-flow-col items-center justify-end gap-4 mb-2;
}
ui-form-control {
@apply w-32;
}
::ng-deep page-purchasing-options-modal-price-input ui-form-control .input-wrapper input {
@apply w-20;
}

View File

@@ -1,55 +0,0 @@
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
import { DomainOmsService } from '@domain/oms';
import { VATType } from '@swagger/checkout';
import { VATDTO } from '@swagger/oms';
import { Observable, Subscription } from 'rxjs';
import { shareReplay } from 'rxjs/operators';
@Component({
selector: 'page-purchasing-options-modal-price-input',
templateUrl: 'purchasing-options-modal-price-input.component.html',
styleUrls: ['purchasing-options-modal-price-input.component.scss'],
providers: [UntypedFormBuilder],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class PurchasingOptionsModalPriceInputComponent implements OnInit {
control: UntypedFormGroup;
vats$: Observable<VATDTO[]> = this._omsService.getVATs().pipe(shareReplay());
@Output()
priceChanged = new EventEmitter<number>();
@Output()
vatChanged = new EventEmitter<VATType>();
private _subscriptions = new Subscription();
@Input()
hideVat = false;
@Input()
maxValue = 99999;
constructor(private _omsService: DomainOmsService, private _fb: UntypedFormBuilder) {}
ngOnInit() {
this.initForm();
}
initForm() {
const fb = this._fb;
this.control = fb.group({
price: fb.control(undefined, [Validators.required, Validators.pattern(/^\d+([\,]\d{1,2})?$/), Validators.max(this.maxValue)]),
vat: fb.control(0, [Validators.required]),
});
this._subscriptions.add(
this.control.get('price').valueChanges.subscribe((price) => this.priceChanged.emit(Number(String(price).replace(',', '.'))))
);
this._subscriptions.add(this.control.get('vat').valueChanges.subscribe(this.vatChanged));
this.control.markAllAsTouched();
}
}

View File

@@ -1,16 +0,0 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { UiFormControlModule } from '@ui/form-control';
import { UiInputModule } from '@ui/input';
import { UiSelectModule } from '@ui/select';
import { PurchasingOptionsModalPriceInputComponent } from './purchasing-options-modal-price-input.component';
@NgModule({
imports: [CommonModule, UiFormControlModule, UiInputModule, UiSelectModule, FormsModule, ReactiveFormsModule],
exports: [PurchasingOptionsModalPriceInputComponent],
declarations: [PurchasingOptionsModalPriceInputComponent],
providers: [],
})
export class PurchasingOptionsModalPriceInputModule {}

View File

@@ -1,140 +0,0 @@
<ng-container *ngIf="(hasOption$ | async) === false">
<h3 class="modal-title">Wie möchten Sie den Artikel erhalten?</h3>
<div class="options-wrapper">
<ng-container *ngFor="let option of availableOptions$ | async" [ngSwitch]="option">
<page-take-away-option *ngSwitchCase="'take-away'"></page-take-away-option>
<page-pick-up-option *ngSwitchCase="'pick-up'"></page-pick-up-option>
<page-delivery-option *ngSwitchCase="'delivery'"></page-delivery-option>
<page-dig-delivery-option *ngSwitchCase="'dig-delivery'"></page-dig-delivery-option>
<page-b2b-delivery-option *ngSwitchCase="'b2b-delivery'"></page-b2b-delivery-option>
</ng-container>
<ng-container *ngIf="(availableOptions$ | async).length === 0">
<p class="hint">Derzeit nicht bestellbar</p>
</ng-container>
</div>
</ng-container>
<ng-container *ngIf="hasOption$ | async">
<h3 class="modal-title">Artikel dem Warenkorb hinzufügen</h3>
<div class="option-product-summary" *ngIf="item$ | async; let item">
<div class="header-row">
<h5 class="option-name" *ngIf="option$ | async; let option">
<ng-container *ngIf="option | purchaseOptionIcon; let icon">
<ui-icon [size]="icon === 'truck_b2b' ? '40px' : '23px'" [icon]="icon"></ui-icon>
</ng-container>
{{ option | purchaseOptionName }}
</h5>
<span *ngIf="(option$ | async) !== 'download'">
in der Filiale:
<span class="option-branch">{{ (branch$ | async)?.name }}</span>
</span>
</div>
<hr />
<div class="product-row">
<img class="thumbnail" [src]="(item?.imageId !== undefined ? item?.imageId : item?.product?.ean) | productImage: 80:100:true" />
<div class="details">
<h6 class="title">{{ item?.product?.contributors }} - {{ item?.product?.name }}</h6>
<strong class="can-add-error" *ngIf="canAddError$ | async; let canAddError">{{ canAddError }}</strong>
<div class="grow"></div>
<div class="format" *ngIf="item?.product?.format && item?.product?.formatDetail">
<img
*ngIf="item?.product?.format !== '--'"
src="assets/images/Icon_{{ item?.product?.format }}.svg"
[alt]="item?.product?.formatDetail"
/>
{{ item?.product?.formatDetail }}
</div>
<div class="price">
{{ price$ | async | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
</div>
<div class="date" *ngIf="option$ | async; let option">
<ng-container *ngIf="option === 'pick-up'">
Abholung ab {{ (getAvailability(option) | async)?.estimatedShippingDate | date: 'shortDate' }}
</ng-container>
<ng-container *ngIf="showDeliveryInfo$ | async">
<ng-container *ngIf="getAvailability(option) | async; let availability">
<ng-container *ngIf="availability?.estimatedDelivery; else estimatedShippingDate">
Zustellung zwischen {{ (availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
{{ (availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
</ng-container>
<ng-template #estimatedShippingDate> Versanddatum {{ availability?.estimatedShippingDate | date: 'shortDate' }} </ng-template>
</ng-container>
</ng-container>
</div>
</div>
<div class="quantity">
<div class="row">
<ui-quantity-dropdown
#quantityControl
[showSpinner]="purchasingOptionsModalStore.selectFetchingAvailability | async"
[ngModel]="quantity$ | async"
[range]="quantityRange$ | async"
(ngModelChange)="changeQuantity($event)"
>
</ui-quantity-dropdown>
<button *ngIf="!quantityControl?.customInput" (click)="backToSetOptions()" class="cta-modify">Ändern</button>
</div>
<div class="quantity-error" *ngIf="quantityError$ | async; let message">{{ message }}</div>
</div>
</div>
<div class="custom-price" *ngIf="showCustomPrice$ | async">
<page-purchasing-options-modal-price-input
[hideVat]="isGiftCard(item.type)"
[maxValue]="isGiftCard(item.type) ? 200 : 99999"
(priceChanged)="changeCustomPrice($event)"
(vatChanged)="changeCustomVat($event)"
>
</page-purchasing-options-modal-price-input>
</div>
<hr />
<div class="summary-row" *ngIf="quantity$ | async; let quantity">
<div class="reading-points">
{{ quantity }} Artikel
<ng-container *ngIf="promoPoints$ | async; let promoPoints"> | {{ promoPoints }} Lesepunkte </ng-container>
</div>
<div class="subtotal">
Zwischensumme
{{ (price$ | async) * quantity | currency: item?.catalogAvailability?.price?.value?.currency || 'EUR':'code' }}
<div class="shipping-cost" *ngIf="showDeliveryInfo$ | async">
ohne Versandkosten
</div>
</div>
</div>
</div>
<div class="actions" *ngIf="option$ | async; let option">
<button
*ngIf="canContinueShopping$ | async"
class="cta-continue-shopping"
[disabled]="(fetching$ | async) || (canContinueShopping$ | async) === false || (customPriceInvalid$ | async) === true"
(click)="continue('continue-shopping')"
>
Weiter einkaufen
</button>
<button *ngIf="canUpgrade$ | async" class="cta-upgrade-customer" (click)="continue('add-customer-data')">
Kundendaten erfassen
</button>
<button
*ngIf="showTakeAwayButton$ | async"
[disabled]="(fetching$ | async) || (canAdd$ | async) === false || (customPriceInvalid$ | async) === true"
class="cta-continue"
(click)="continue()"
>
Reservieren
</button>
<button
*ngIf="showDefaultContinueButton$ | async"
class="cta-continue"
(click)="continue()"
[disabled]="(fetching$ | async) || (canAdd$ | async) === false || (customPriceInvalid$ | async) === true"
>
Fortfahren
</button>
<button *ngIf="showContinueWithoutAdding$ | async" class="cta-continue" (click)="continue()">
Ohne Artikel Fortfahren
</button>
</div>
</ng-container>

View File

@@ -1,124 +0,0 @@
:host {
@apply block box-border;
}
.modal-title {
@apply text-center mt-2 text-xl font-bold;
}
.cta-modify {
@apply self-end bg-transparent text-brand font-bold text-lg outline-none border-none ml-4;
}
.options-wrapper {
@apply flex flex-row justify-evenly items-stretch mt-2;
}
.option-product-summary {
@apply flex flex-col box-border;
hr {
@apply my-4;
}
}
.option-name {
@apply flex flex-row items-center font-bold text-card-sub mb-2 mt-1;
ui-icon {
@apply mr-2 text-ucla-blue;
}
}
.option-branch {
@apply font-bold;
}
.product-row {
@apply flex flex-row items-center;
}
.summary-row {
@apply flex flex-row justify-between font-bold;
}
.reading-points {
@apply text-ucla-blue;
}
.subtotal {
@apply text-lg;
}
.shipping-cost {
@apply text-sm text-right font-normal;
}
img.thumbnail {
height: 100px;
}
.grow {
@apply flex-grow;
}
.details {
@apply ml-4 flex flex-col font-bold self-stretch flex-grow;
.title {
@apply text-base m-0;
}
}
.format {
@apply flex flex-row items-center whitespace-nowrap;
img {
@apply mr-2;
}
}
.quantity {
@apply self-end flex flex-col justify-end;
.row {
@apply flex flex-row justify-end;
}
}
.actions {
@apply flex flex-row justify-end items-center mt-8;
}
.cta-continue-shopping {
@apply text-brand border-2 border-solid border-brand bg-white font-bold text-lg px-4 py-2 rounded-full;
::ng-deep.spin {
@apply text-brand;
}
&:disabled {
@apply text-inactive-branch border-inactive-branch cursor-not-allowed;
}
}
.cta-continue,
.cta-upgrade-customer {
@apply text-white bg-brand font-bold text-lg px-4 py-2 rounded-full border-none ml-4 no-underline;
&:disabled {
@apply bg-inactive-branch cursor-not-allowed;
}
}
.can-add-error {
@apply text-xl text-dark-goldenrod mt-2;
}
.quantity-error {
@apply text-dark-goldenrod font-bold text-sm mt-2;
}
.hint {
@apply text-dark-goldenrod font-bold text-xl;
}

View File

@@ -1,396 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { AddToShoppingCartDTO, AvailabilityDTO, ItemType, VATType } from '@swagger/checkout';
import { UiModalRef } from '@ui/modal';
import { shareReplay, debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { combineLatest, Observable } from 'rxjs';
import { PurchasingOptionsModalData } from './purchasing-options-modal.data';
import { PurchasingOptionsModalStore } from './purchasing-options-modal.store';
import { DomainCatalogService } from '@domain/catalog';
import { BreadcrumbService } from '@core/breadcrumb';
import {
encodeFormData,
mapCustomerDtoToCustomerCreateFormData,
} from 'apps/page/customer/src/lib/create-customer/customer-create-form-data';
import { isNumber } from '@utils/common';
@Component({
selector: 'page-purchasing-options-modal',
templateUrl: 'purchasing-options-modal.component.html',
styleUrls: ['purchasing-options-modal.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [PurchasingOptionsModalStore],
})
export class PurchasingOptionsModalComponent {
readonly item$ = this.purchasingOptionsModalStore.selectItem;
readonly availableOptions$ = this.purchasingOptionsModalStore.selectAvailableOptions.pipe(shareReplay());
readonly option$ = this.purchasingOptionsModalStore.selectOption;
readonly hasOption$ = this.purchasingOptionsModalStore.selectHasOption;
readonly quantity$ = this.purchasingOptionsModalStore.selectQuantity;
readonly canAdd$ = this.purchasingOptionsModalStore.selectCanAdd;
readonly canAddError$ = this.purchasingOptionsModalStore.selectCanAddError;
readonly canUpgrade$ = this.purchasingOptionsModalStore.selectCanUpgrade;
readonly availability$ = this.purchasingOptionsModalStore.selectAvailability;
readonly branch$ = this.purchasingOptionsModalStore.selectBranch;
readonly canContinueShopping$ = this.purchasingOptionsModalStore.selectCanContinueShopping;
readonly canContinueShoppingIsLoading$ = this.purchasingOptionsModalStore.selectCanContinueShoppingIsLoading;
readonly quantityError$ = this.purchasingOptionsModalStore.selectQuantityError;
readonly showCustomPrice$ = this.purchasingOptionsModalStore.selectAvailabilities.pipe(
withLatestFrom(this.option$),
map(([availabilities, option]) => !availabilities[option]?.price?.value?.value)
);
readonly customPriceInvalid$ = combineLatest([
this.item$,
this.showCustomPrice$,
this.purchasingOptionsModalStore.selectCustomPrice,
this.purchasingOptionsModalStore.selectCustomVat,
]).pipe(
map(([item, showCustomPrice, customPrice, customVat]) => {
if (!showCustomPrice) {
return false;
}
if ((item.type as any) === 66560) {
return !isNumber(customPrice) || customPrice < 1 || customPrice > 200;
}
return !customPrice || !customVat;
})
);
readonly showTakeAwayButton$ = combineLatest([
this.option$,
this.purchasingOptionsModalStore.selectFetchingAvailability,
this.purchasingOptionsModalStore.selectCheckingCanAdd,
this.canAdd$,
this.canUpgrade$,
]).pipe(
map(([option, fetchingAvailability, checkingCanAdd, canAdd, canUpgrade]) => {
if (option !== 'take-away') {
return false;
}
if (!fetchingAvailability && !checkingCanAdd && !canAdd) {
return false;
}
return !canUpgrade;
})
);
readonly showDefaultContinueButton$ = combineLatest([
this.option$,
this.purchasingOptionsModalStore.selectFetchingAvailability,
this.purchasingOptionsModalStore.selectCheckingCanAdd,
this.canAdd$,
this.canUpgrade$,
]).pipe(
map(([option, fetchingAvailability, checkingCanAdd, canAdd, canUpgrade]) => {
if (option === 'take-away') {
return false;
}
if (!fetchingAvailability && !checkingCanAdd && !canAdd) {
return false;
}
return !canUpgrade;
})
);
readonly showContinueWithoutAdding$ = combineLatest([this.showTakeAwayButton$, this.showDefaultContinueButton$, this.canUpgrade$]).pipe(
map(([showTakeAway, showDefault, canUpgrade]) => !canUpgrade && !(showTakeAway || showDefault))
);
readonly showDeliveryInfo$ = this.option$.pipe(map((option) => ['delivery', 'b2b-delivery', 'dig-delivery'].indexOf(option) > -1));
readonly fetching$ = combineLatest([
this.purchasingOptionsModalStore.selectFetchingAvailability,
this.purchasingOptionsModalStore.selectCheckingCanAdd,
]).pipe(map(([fetching, checking]) => fetching || checking));
customerFeatures$ = this.application.activatedProcessId$.pipe(
switchMap((processId) => this.checkoutService.getCustomerFeatures({ processId }))
);
readonly customer$ = this.application.activatedProcessId$.pipe(switchMap((processId) => this.checkoutService.getCustomer({ processId })));
price$ = combineLatest([
this.purchasingOptionsModalStore.selectAvailabilities,
this.option$,
this.purchasingOptionsModalStore.selectCustomPrice,
]).pipe(
map(([availabilities, option, customPrice]) => {
if (option && !!availabilities[option]) {
if (availabilities[option]?.price?.value?.value) {
return availabilities[option]?.price?.value?.value;
}
return availabilities[option]?.price?.value?.value ?? customPrice;
} else {
const key = Object.keys(availabilities).find((key) => !!availabilities[key]?.price?.value?.value);
return availabilities[key]?.price?.value?.value ?? customPrice;
}
})
);
vat$ = combineLatest([
this.purchasingOptionsModalStore.selectAvailabilities,
this.option$,
this.purchasingOptionsModalStore.selectCustomVat,
]).pipe(
map(([availabilities, option, customVat]) => {
if (option && !!availabilities[option]) {
if (availabilities[option]?.price?.vat?.vatType) {
return availabilities[option]?.price?.vat?.vatType;
}
return availabilities[option]?.price?.vat?.vatType ?? customVat;
} else {
const key = Object.keys(availabilities).find((key) => !!availabilities[key]?.price?.vat?.vatType);
return availabilities[key]?.price?.vat?.vatType ?? customVat;
}
})
);
readonly promoPoints$ = combineLatest([this.item$, this.quantity$, this.price$]).pipe(
debounceTime(250),
switchMap(([item, quantity, price]) =>
this.domainCatalogService
.getPromotionPoints({
items: [
{
id: item.id,
quantity: quantity,
price: price,
},
],
})
.pipe(map((res) => res.result[item.id]))
)
);
quantityRange$ = combineLatest([this.option$, this.availability$]).pipe(
map(([option, availability]) => (option === 'take-away' && availability?.inStock ? availability.inStock : 999))
);
activeSpinner: string;
constructor(
public modalRef: UiModalRef<any, PurchasingOptionsModalData>,
public purchasingOptionsModalStore: PurchasingOptionsModalStore,
private application: ApplicationService,
private router: Router,
private checkoutService: DomainCheckoutService,
private domainCatalogService: DomainCatalogService,
private breadcrumb: BreadcrumbService
) {
this.purchasingOptionsModalStore.setShoppingCartItem(this.modalRef.data.shoppingCartItem);
this.purchasingOptionsModalStore.setItem(this.modalRef.data.item);
this.purchasingOptionsModalStore.setProcessId(this.modalRef.data.processId || this.application.activatedProcessId);
this.purchasingOptionsModalStore.setAvailabilities(this.modalRef.data.availabilities || {});
this.purchasingOptionsModalStore.setQuantity(this.modalRef?.data?.shoppingCartItem?.quantity || 1);
this.purchasingOptionsModalStore.setOption(this.modalRef.data.option);
this.purchasingOptionsModalStore.setAvailableOptions(this.modalRef.data.availableOptions);
if (
this.modalRef.data.availableOptions?.some((option) => option === 'pick-up' || option === 'take-away') ||
['take-away', 'pick-up'].includes(this.modalRef.data.option)
) {
this.purchasingOptionsModalStore.loadBranches(this.modalRef?.data?.branchId);
}
}
changeCustomVat(vat: VATType) {
this.purchasingOptionsModalStore.setCustomVat(vat);
}
changeCustomPrice(price: number) {
this.purchasingOptionsModalStore.setCustomPrice(price);
}
backToSetOptions() {
this.purchasingOptionsModalStore.setOption(undefined);
}
getAvailability(option: string): Observable<AvailabilityDTO> {
return this.purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava[option]));
}
async changeQuantity(quantity: number = 1) {
this.purchasingOptionsModalStore.setQuantity(quantity);
if (quantity === 0) {
this.modalRef.close();
}
}
async continue(navigate: 'continue' | 'continue-shopping' | 'add-customer-data' = 'continue') {
this.activeSpinner = navigate ? 'continue-shopping' : 'continue';
try {
const processId = await this.purchasingOptionsModalStore.selectProcessId.pipe(first()).toPromise();
const buyer = await this.checkoutService.getBuyer({ processId }).pipe(first()).toPromise();
const item = await this.item$.pipe(first()).toPromise();
const quantity = await this.quantity$.pipe(first()).toPromise();
const availability = await this.availability$.pipe(first()).toPromise();
const option = await this.option$.pipe(first()).toPromise();
const branch = await this.branch$.pipe(first()).toPromise();
const shoppingCartItem = await this.purchasingOptionsModalStore.selectShoppingCartItem.pipe(first()).toPromise();
const canAdd = await this.canAdd$.pipe(first()).toPromise();
const customPrice = await this.purchasingOptionsModalStore.selectCustomPrice.pipe(first()).toPromise();
const customVat = (await this.purchasingOptionsModalStore.selectCustomVat.pipe(first()).toPromise()) ?? 0;
const customer = await this.checkoutService.getCustomer({ processId }).pipe(first()).toPromise();
if (canAdd || navigate === 'add-customer-data') {
const newItem: AddToShoppingCartDTO = {
quantity,
availability,
product: {
catalogProductNumber: '',
...item.product,
},
promotion: { points: item.promoPoints },
itemType: item.type,
};
newItem.product.catalogProductNumber = String(item.id);
if (!!customPrice && !!customVat) {
newItem.availability.price = {
value: {
value: customPrice,
currency: 'EUR',
},
vat: {
vatType: customVat,
},
};
} else {
const price = await this.price$.pipe(first()).toPromise();
const vat = await this.vat$.pipe(first()).toPromise();
newItem.availability.price = {
value: {
value: price,
currency: 'EUR',
},
vat: {
vatType: vat,
},
};
}
switch (option) {
case 'take-away':
case 'pick-up':
newItem.destination = {
data: { target: 1, targetBranch: { id: branch.id } },
};
break;
case 'delivery':
case 'dig-delivery':
case 'b2b-delivery':
newItem.destination = {
data: { target: 2, logistician: availability.logistician },
};
break;
case 'download':
newItem.destination = {
data: { target: 16, logistician: availability.logistician },
};
break;
}
if (shoppingCartItem) {
await this.checkoutService
.updateItemInShoppingCart({
processId,
shoppingCartItemId: shoppingCartItem?.id,
update: {
availability: newItem.availability,
quantity: newItem.quantity,
destination: newItem.destination,
},
})
.toPromise();
} else {
await this.checkoutService
.addItemToShoppingCart({
processId,
items: [newItem],
})
.toPromise();
}
}
this.modalRef.close();
if (shoppingCartItem) {
return;
}
if (navigate === 'continue-shopping') {
const crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'filter', 'results'])
.pipe(first())
.toPromise();
if (!!crumbs && crumbs.length > 0) {
const queryParams = crumbs[0].params;
this.router.navigate(['/kunde', this.application.activatedProcessId, 'product', 'search', 'results'], { queryParams });
} else {
// Route back to search if no result page was loaded (f.e. When searching Article and landing directly on details page)
this.router.navigate(['/kunde', this.application.activatedProcessId, 'product', 'search']);
}
} else if (navigate === 'continue') {
// Set filter for navigation to customer search if customer is not set
let filter: { [key: string]: string };
if (!buyer) {
filter = await this.customerFeatures$
.pipe(
first(),
switchMap((customerFeatures) => {
return this.checkoutService.canSetCustomer({ processId, customerFeatures });
}),
map((res) => res.filter)
)
.toPromise();
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'search'], {
queryParams: { filter_customertype: filter.customertype },
});
} else {
this.router.navigate(['/kunde', this.application.activatedProcessId, 'cart', 'review']);
}
} else if (navigate === 'add-customer-data') {
if (customer?.attributes.some((attr) => attr.data.key === 'p4mUser')) {
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'create', 'webshop-p4m'], {
queryParams: { formData: encodeFormData(mapCustomerDtoToCustomerCreateFormData(customer)) },
});
} else {
this.router.navigate(['/kunde', this.application.activatedProcessId, 'customer', 'create', 'webshop'], {
queryParams: { formData: encodeFormData(mapCustomerDtoToCustomerCreateFormData(customer)) },
});
}
}
} catch (error) {
console.log('PurchasingOptionsModalComponent.continue', error);
}
this.activeSpinner = undefined;
}
isGiftCard(itemType: ItemType): boolean {
return (itemType as any) === 66560;
}
}

View File

@@ -1,13 +0,0 @@
import { ItemDTO } from '@swagger/cat';
import { AvailabilityDTO, ShoppingCartItemDTO } from '@swagger/checkout';
import { PurchasingOptions } from './purchasing-options-modal.store';
export interface PurchasingOptionsModalData {
item: ItemDTO;
availableOptions: PurchasingOptions[];
processId?: number;
option?: PurchasingOptions;
shoppingCartItem?: ShoppingCartItemDTO;
availabilities?: { [key: string]: AvailabilityDTO };
branchId?: number;
}

View File

@@ -1,55 +0,0 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { OverlayModule } from '@angular/cdk/overlay';
import { UiIconModule } from '@ui/icon';
import {
B2BDeliveryOptionComponent,
TakeAwayOptionComponent,
PickUpOptionComponent,
DeliveryOptionComponent,
DigDeliveryOptionComponent,
} from './options';
import { PurchasingOptionsModalComponent } from './purchasing-options-modal.component';
import { PageCheckoutPipeModule } from '../../pipes/page-checkout-pipe.module';
import { ProductImageModule } from 'apps/cdn/product-image/src/public-api';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { KeyNavigationModule } from '../../shared/key-navigation/key-navigation.module';
import { RouterModule } from '@angular/router';
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
import { PurchasingOptionsModalPriceInputModule } from './price-input/purchasing-options-modal-price-input.module';
import { UiTooltipModule } from '@ui/tooltip';
import { UiCommonModule } from '@ui/common';
import { UiBranchDropdownModule } from '@ui/branch-dropdown';
@NgModule({
imports: [
CommonModule,
UiCommonModule,
FormsModule,
UiIconModule,
OverlayModule,
PageCheckoutPipeModule,
ProductImageModule,
UiQuantityDropdownModule,
UiSpinnerModule,
KeyNavigationModule,
RouterModule,
PurchasingOptionsModalPriceInputModule,
UiTooltipModule,
UiBranchDropdownModule,
],
exports: [PurchasingOptionsModalComponent],
declarations: [
PurchasingOptionsModalComponent,
B2BDeliveryOptionComponent,
TakeAwayOptionComponent,
PickUpOptionComponent,
DeliveryOptionComponent,
DigDeliveryOptionComponent,
],
})
export class PurchasingOptionsModalModule {}

View File

@@ -1,498 +0,0 @@
import { Injectable } from '@angular/core';
import { ApplicationService } from '@core/application';
import { DomainAvailabilityService } from '@domain/availability';
import { DomainCheckoutService } from '@domain/checkout';
import { CrmCustomerService } from '@domain/crm';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ItemDTO } from '@swagger/cat';
import { AvailabilityDTO, BranchDTO, OLAAvailabilityDTO, ShoppingCartItemDTO, VATType } from '@swagger/checkout';
import { isBoolean, isNullOrUndefined, isString } from '@utils/common';
import { NEVER, Observable } from 'rxjs';
import { delay, filter, map, switchMap, tap, withLatestFrom } from 'rxjs/operators';
export type PurchasingOptions = 'take-away' | 'pick-up' | 'delivery' | 'dig-delivery' | 'b2b-delivery' | 'download';
interface PurchasingOptionsModalState {
item?: ItemDTO;
shoppingCartItem?: ShoppingCartItemDTO;
option?: PurchasingOptions;
defaultBranch?: BranchDTO;
branch?: BranchDTO;
processId?: number;
fetchingAvailability: boolean;
availableOptions: PurchasingOptions[];
availableBranches: BranchDTO[];
quantity: number;
maxQuantityError: boolean;
checkingCanAdd: boolean;
canAdd: boolean;
canAddError?: string;
canUpgrade: boolean;
availabilities: { [key: string]: AvailabilityDTO };
customPrice?: number;
customVat?: VATType;
}
@Injectable()
export class PurchasingOptionsModalStore extends ComponentStore<PurchasingOptionsModalState> {
readonly selectItem = this.select((s) => s.item);
readonly selectShoppingCartItem = this.select((s) => s.shoppingCartItem);
readonly selectOption = this.select((s) => s.option);
readonly selectOrderType = this.select(this.selectOption, (option) => {
switch (option) {
case 'take-away':
return 'Rücklage';
case 'pick-up':
return 'Abholung';
case 'delivery':
return 'Versand';
case 'b2b-delivery':
return 'B2B-Versand';
case 'dig-delivery':
return 'DIG-Versand';
case 'download':
return 'Download';
}
});
readonly selectHasOption = this.select((s) => !!s.option);
readonly selectBranch = this.select((s) => {
return s.branch || s.defaultBranch;
});
readonly selectQuantity = this.select((s) => s.quantity);
readonly selectCustomPrice = this.select((s) => s.customPrice);
readonly selectCustomVat = this.select((s) => s.customVat);
readonly selectAvailabilities = this.select((s) => s.availabilities);
readonly selectAvailability = this.select((s) => s.availabilities[s.option]);
readonly selectAvailabilityIsValid = this.select(this.selectAvailability, (availability) =>
this.availabilityService.isAvailable({ availability })
);
readonly selectAvailableOptions = this.select((s) => s.availableOptions);
readonly selectAvailableBranches = this.select((s) => s.availableBranches);
readonly selectProcessId = this.select((s) => s.processId);
readonly selectCheckingCanAdd = this.select((s) => s.checkingCanAdd);
readonly selectCanAdd = this.select(
this.selectAvailability,
this.select((s) => s.canAdd),
(availability, canAdd) => canAdd && this.availabilityService.isAvailable({ availability })
);
readonly selectCanAddError = this.select((s) => s.canAddError);
readonly selectFetchingAvailability = this.select((s) => s.fetchingAvailability);
readonly selectMaxQuantityError = this.select((s) => s.maxQuantityError);
readonly selectQuantityError = this.select(
this.selectQuantity,
this.selectOption,
this.selectAvailability,
this.selectFetchingAvailability,
this.selectMaxQuantityError,
(quantity, option, availability, fetching, maxQuantityError) => {
if (!fetching) {
if (maxQuantityError) {
return `Achtung, Maximal 999 Exemplare bestellbar.`;
}
if (availability?.inStock < quantity) {
if (option === 'pick-up') {
return `${availability?.inStock} Exemplare sofort lieferbar.`;
}
if (option === 'take-away') {
return `${availability?.inStock} Exemplare sofort lieferbar.`;
}
}
if (!this.availabilityService.isAvailable({ availability })) {
return availability?.sscText;
}
}
return undefined;
}
);
readonly selectOlaAvailability = this.select(
(s): OLAAvailabilityDTO =>
this.availabilityService.mapToOlaAvailability({
availability: s.availabilities[s.option],
item: s.item,
quantity: s.quantity,
})
);
readonly selectCanUpgrade = this.select((s) => s.canUpgrade);
readonly selectCanContinueShopping = this.select(
this.selectFetchingAvailability,
this.selectCanAdd,
this.selectOption,
this.selectQuantityError,
this.selectQuantity,
this.selectCanUpgrade,
(fetching, canAdd, option, quantityError, quantity, canUpgrade) => {
let hasError = !!quantityError;
if (option === 'pick-up' && quantity <= 999) {
hasError = false;
}
return !fetching && (canAdd || canUpgrade);
}
).pipe(delay(1));
readonly selectCanContinueShoppingIsLoading = this.select(this.selectFetchingAvailability, this.selectCanAdd, (fetching, canAdd) => {
return fetching;
});
constructor(
private checkoutService: DomainCheckoutService,
private availabilityService: DomainAvailabilityService,
private customerService: CrmCustomerService,
private applicationService: ApplicationService
) {
super({
availableBranches: [],
availableOptions: [],
quantity: 1,
canAdd: false,
canUpgrade: false,
availabilities: {},
checkingCanAdd: false,
fetchingAvailability: false,
maxQuantityError: false,
});
this.loadDefaultBranch();
}
readonly setItem = this.updater((state, item: ItemDTO) => {
this.loadAvailability();
return {
...state,
item,
availability: undefined,
canAdd: false,
canAddError: undefined,
};
});
readonly setShoppingCartItem = this.updater((state, shoppingCartItem: ShoppingCartItemDTO) => {
this.loadAvailability();
return {
...state,
shoppingCartItem,
};
});
readonly setOption = this.updater((state, option: PurchasingOptions) => {
this.loadAvailability();
return {
...state,
option,
availability: undefined,
canAdd: false,
canAddError: undefined,
};
});
readonly setBranch = this.updater((state, branch: BranchDTO) => {
this.loadAvailability();
return {
...state,
branch,
availability: undefined,
canAdd: false,
canAddError: undefined,
};
});
readonly setBranchId = this.updater((state, branchId: number) => {
const branch = state.availableBranches.find((branch) => branch.id === branchId);
this.loadAvailability();
return {
...state,
branchId,
branch,
};
});
readonly setAvailability = this.updater(
(state, { availability, option, item }: { availability: AvailabilityDTO; option: PurchasingOptions; item: ItemDTO }) => {
this.checkCanAdd();
let updatedAvailability = availability;
if ((option && option === 'delivery') || option === 'dig-delivery') {
const catalogPrice = item?.catalogAvailability?.price?.value?.value;
const availabilityPrice = availability?.price?.value?.value;
const updatedPrice = catalogPrice <= availabilityPrice ? catalogPrice : availabilityPrice;
updatedAvailability = {
...availability,
price: {
...availability.price,
value: {
...availability.price.value,
value: updatedPrice,
},
},
};
}
return {
...state,
availabilities: {
...state.availabilities,
[option]: updatedAvailability,
},
};
}
);
readonly setQuantity = this.updater((state, quantity: number = 1) => {
let qty = quantity;
if (quantity > 999) {
qty = 999;
this.patchState({ maxQuantityError: true });
} else {
this.patchState({ maxQuantityError: false });
}
this.loadAvailability();
return {
...state,
quantity: qty,
canAdd: false,
canAddError: undefined,
};
});
readonly setCustomPrice = this.updater((state, customPrice: number) => {
return {
...state,
customPrice,
};
});
readonly setCustomVat = this.updater((state, customVat: VATType) => {
return {
...state,
customVat,
};
});
readonly setAvailableOptions = this.updater((state, availableOptions: PurchasingOptions[]) => {
let option = state.option;
if (availableOptions?.length === 1 && availableOptions[0] === 'download') {
option = availableOptions[0];
}
return {
...state,
availableOptions,
option,
};
});
readonly setAvailableBranches = this.updater((state, availableBranches: BranchDTO[]) => {
const branch = state.branch || state.defaultBranch;
return {
...state,
availableBranches,
branch,
};
});
readonly setProcessId = this.updater((state, processId: number) => ({
...state,
processId,
}));
readonly setCanAdd = this.updater((state, canAddItem: true | string) => {
let canAdd = isBoolean(canAddItem) ? Boolean(canAddItem) : false;
if (!canAdd) {
this.checkCanUpgrade();
}
return { ...state, canAdd, canAddError: isString(canAddItem) ? String(canAddItem) : undefined };
});
readonly setAvailabilities = this.updater((state, availabilities: { [key: string]: AvailabilityDTO }) => {
this.checkCanAdd();
return {
...state,
availabilities: {
...state.availabilities,
...availabilities,
},
};
});
loadBranches = this.effect((branchId$: Observable<number>) =>
branchId$.pipe(
switchMap((branchId) =>
this.checkoutService.getBranches().pipe(
tapResponse(
(branches: BranchDTO[]) => {
this.setAvailableBranches(branches);
this.setBranchId(branchId);
},
() => this.setAvailableBranches([])
)
)
)
)
);
loadAvailability = this.effect(($) =>
$.pipe(
delay(10),
withLatestFrom(this.selectItem, this.selectQuantity, this.selectOption, this.selectBranch),
switchMap(([_, item, quantity, option, branch]) => {
let availability$: Observable<AvailabilityDTO> = NEVER;
if (!isNullOrUndefined(item) && quantity > 0 && isString(option)) {
this.patchState({ fetchingAvailability: true });
switch (option) {
case 'take-away':
availability$ = this.availabilityService.getTakeAwayAvailabilityByBranch({
itemId: item.id,
price: item.catalogAvailability.price,
quantity,
branch,
});
break;
case 'pick-up':
if (!isNullOrUndefined(branch)) {
availability$ = this.availabilityService
.getPickUpAvailability({
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
quantity,
branch,
})
.pipe(
map((av) => {
if (av?.length > 0) {
if (av[1].availableFor) {
if ((av[1].availableFor & 2) === 2) {
return av[0];
} else {
undefined;
}
} else {
return av[0];
}
}
})
);
}
break;
case 'delivery':
availability$ = this.availabilityService.getDeliveryAvailability({
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
quantity,
});
break;
case 'dig-delivery':
availability$ = this.availabilityService.getDigDeliveryAvailability({
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
quantity,
});
break;
case 'b2b-delivery':
if (!isNullOrUndefined(branch)) {
availability$ = this.availabilityService.getB2bDeliveryAvailability({
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
quantity,
});
}
break;
case 'download':
availability$ = this.availabilityService.getDownloadAvailability({
item: { itemId: item.id, ean: item.product.ean, price: item.catalogAvailability.price },
});
break;
}
}
return availability$.pipe(
tapResponse(
(availability) => {
this.setAvailability({ option, availability, item });
this.patchState({ fetchingAvailability: false });
},
() => {
this.setAvailability(null);
this.patchState({ fetchingAvailability: false });
}
)
);
})
)
);
checkCanAdd = this.effect(($) =>
$.pipe(
delay(10),
withLatestFrom(this.selectOlaAvailability, this.selectProcessId, this.selectOrderType),
switchMap(([_, availability, processId, orderType]) => {
this.patchState({ checkingCanAdd: true });
return this.checkoutService.canAddItems({ processId, payload: [{ availabilities: [availability] }], orderType }).pipe(
tapResponse(
(response: any) => {
this.setCanAdd(response?.find((_) => true)?.status === 0 ? true : response?.find((_) => true)?.message);
},
(error: Error) => this.setCanAdd(error?.message)
),
tap((_) => this.patchState({ checkingCanAdd: false }))
);
})
)
);
checkCanUpgrade = this.effect(($) =>
$.pipe(
withLatestFrom(this.applicationService.activatedProcessId$),
switchMap(([_, processId]) => this.checkoutService.getBuyer({ processId })),
map((buyer) => buyer?.source),
filter((customerId) => !isNaN(customerId)),
switchMap((customerId) =>
this.customerService.canUpgrade(customerId).pipe(
tapResponse(
(response) => {
let canUpgrade = response.options?.values?.some((u) => u.value === 'webshop');
this.patchState({ canUpgrade });
},
(error) => {
this.patchState({ canUpgrade: false });
}
)
)
)
)
);
readonly loadDefaultBranch = this.effect(($) =>
$.pipe(
switchMap((_) =>
this.availabilityService.getDefaultBranch().pipe(
tapResponse(
(defaultBranch) => this.patchState({ defaultBranch }),
(err) => {}
)
)
)
)
);
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './take-away-option.component';
// end:ng42.barrel

View File

@@ -1,18 +0,0 @@
<ng-container *ngIf="item$ | async; let item">
<ng-container *ngIf="availability$ | async; let availability">
<div class="option-icon">
<ui-icon size="50px" icon="shopping_bag"></ui-icon>
</div>
<h4>Rücklage / Filialentnahme</h4>
<p>
Möchten Sie den Artikel zurücklegen lassen oder sofort mitnehmen?
</p>
<span class="price" *ngIf="price$ | async; let price">{{ price?.value?.value | currency: price?.value?.currency:'code' }}</span>
<div class="grow"></div>
<div>
<button [disabled]="availability.price?.value?.value < 0" type="button" class="select-option" (click)="select()">
Auswählen
</button>
</div>
</ng-container>
</ng-container>

View File

@@ -1,27 +0,0 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { DomainAvailabilityService } from '@domain/availability';
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';
import { PurchasingOptionsModalStore } from '../purchasing-options-modal.store';
@Component({
selector: 'page-take-away-option',
templateUrl: 'take-away-option.component.html',
styleUrls: ['../option.scss', 'take-away-option.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TakeAwayOptionComponent {
readonly item$ = this._purchasingOptionsModalStore.selectItem;
readonly availability$ = this._purchasingOptionsModalStore.selectAvailabilities.pipe(map((ava) => ava['take-away']));
readonly price$ = combineLatest([this.availability$, this.item$]).pipe(
map(([availability, item]) => this._availabilityService.getPriceForAvailability('take-away', item.catalogAvailability, availability))
);
constructor(private _purchasingOptionsModalStore: PurchasingOptionsModalStore, private _availabilityService: DomainAvailabilityService) {}
select() {
this._purchasingOptionsModalStore.setOption('take-away');
}
}

View File

@@ -1,10 +0,0 @@
import { NgModule } from '@angular/core';
import { PurchasingOptionsListModalModule } from './modals/purchasing-options-list-modal';
import { PurchasingOptionsModalModule } from './modals/purchasing-options-modal';
@NgModule({
imports: [PurchasingOptionsModalModule, PurchasingOptionsListModalModule],
exports: [PurchasingOptionsModalModule, PurchasingOptionsListModalModule],
})
export class PageCheckoutModalsModule {}

View File

@@ -4,14 +4,12 @@ import { ShellBreadcrumbModule } from '@shell/breadcrumb';
import { CheckoutDummyModule } from './checkout-dummy/checkout-dummy.module';
import { CheckoutReviewModule } from './checkout-review/checkout-review.module';
import { CheckoutSummaryModule } from './checkout-summary/checkout-summary.module';
import { PageCheckoutModalsModule } from './page-checkout-modals.module';
import { PageCheckoutRoutingModule } from './page-checkout-routing.module';
import { PageCheckoutComponent } from './page-checkout.component';
@NgModule({
imports: [
CommonModule,
PageCheckoutModalsModule,
CheckoutSummaryModule,
PageCheckoutRoutingModule,
CheckoutReviewModule,
@@ -19,6 +17,6 @@ import { PageCheckoutComponent } from './page-checkout.component';
ShellBreadcrumbModule,
],
declarations: [PageCheckoutComponent],
exports: [PageCheckoutModalsModule],
exports: [],
})
export class PageCheckoutModule {}

View File

@@ -70,9 +70,9 @@ export class PackageDetailsComponent {
this.onNextAction(action);
} else {
try {
await this.changePackageStatus(action.value);
const changes = await this.changePackageStatus(action.value);
this.updatedToaster();
this.navigateToList((+action.value as ArrivalStatus) || 0);
this.navigateToList(changes);
} catch (error) {
this._uiModal.error('Fehler beim Speichern der Daten.', error);
}
@@ -80,7 +80,19 @@ export class PackageDetailsComponent {
this.calculateListHeight();
}
async navigateToList(arrivalStatus: ArrivalStatus) {
async navigateToList({
previousArrivalStatus,
newArrivalStatus,
}: {
previousArrivalStatus: ArrivalStatus;
newArrivalStatus: ArrivalStatus;
}) {
// Navigiere nur zurück, wenn der Status nicht 4 oder 16 ist (Fehlt)
if ([4, 16].includes(previousArrivalStatus)) {
this._router.navigate([`/filiale/package-inspection/packages`]);
return;
}
const breadcrumb = await this._getListPageBreadcrumb();
if (breadcrumb) {
@@ -89,7 +101,7 @@ export class PackageDetailsComponent {
queryParams: {
...breadcrumb.params,
updated_packageId: packageId,
updated_arrivalStatus: arrivalStatus,
updated_arrivalStatus: newArrivalStatus,
},
});
} else {
@@ -107,9 +119,19 @@ export class PackageDetailsComponent {
this.childActionsSubject.next(action.children || undefined);
}
async changePackageStatus(status: string): Promise<void> {
async changePackageStatus(
status: string
): Promise<{
previousArrivalStatus: ArrivalStatus;
newArrivalStatus: ArrivalStatus;
}> {
const packageDetails = await this.packageDetails$.pipe(first()).toPromise();
await this._packageInspectionService.changePackageStatus(packageDetails.package, status).toPromise();
return {
previousArrivalStatus: packageDetails.package.arrivalStatus,
newArrivalStatus: (+status as ArrivalStatus) ?? 0,
};
}
async createBreadcrumbIfNotExists(details: PackageDetailResponseDTO): Promise<void> {

View File

@@ -79,7 +79,7 @@ export class PackageResultComponentStore extends ComponentStore<PackageResultCom
withLatestFrom(this.filter$),
switchMap(([options, filter]) =>
this._packageInspectionService
.queryPackagesResponse({ ...filter.getQueryToken(), take: options.take || 20 })
.queryPackagesResponse({ ...filter.getQueryToken(), take: options.take || 50 })
.pipe(takeUntil(this._cancleFetchPackages$), tapResponse(this.onFetchPackagesResponse, this.onFetchError))
)
)
@@ -98,7 +98,7 @@ export class PackageResultComponentStore extends ComponentStore<PackageResultCom
tap(() => this.setFetching(true)),
switchMap(([_, filter, packages]) =>
this._packageInspectionService
.queryPackagesResponse({ ...filter.getQueryToken(), skip: packages.length, take: 20 })
.queryPackagesResponse({ ...filter.getQueryToken(), skip: packages.length, take: 50 })
.pipe(tapResponse(this.onPaginatePackagesResponse, this.onFetchError))
)
)

View File

@@ -55,7 +55,7 @@ export class CreateRemissionComponent implements OnInit {
const returnDTO = await this.createReturn(supplier.id, returnGroup);
const receipt = await this.createReceipt({ returnDTO, receiptNumber });
if (receipt) {
await this.navigateToList(returnDTO.id);
await this.navigateToFinishShippingDocument(returnDTO.id, receipt.id);
} else {
return undefined;
}
@@ -79,8 +79,8 @@ export class CreateRemissionComponent implements OnInit {
return await this._domainRemissionService.createReceipt(returnDTO, receiptNumber);
}
async navigateToList(returnId: number) {
await this._router.navigate(['/filiale', 'remission', returnId, 'list'], {
async navigateToFinishShippingDocument(returnId: number, receiptId: number) {
await this._router.navigate(['/filiale', 'remission', returnId, 'finish-shipping-document', receiptId], {
queryParams: { ...this._activatedRoute.snapshot.queryParams, supplier: this.supplierId, source: this.source },
});
}

View File

@@ -1,6 +1,6 @@
<div class="grid grid-flow-row text-center py-8 bg-white">
<h1 class="font-bold text-2xl">Wannennummer scannen</h1>
<p class="text-xl mt-2">Scannen Sie die Wannennummer<br />um den Warenbegleitschein<br />abzuschließen.</p>
<p class="text-xl mt-2">Scannen Sie die Wannennummer<br />um den Warenbegleitschein<br />der Wanne zuordnen zu können.</p>
<ui-searchbox
placeholder="Wannennummer scannen"

View File

@@ -2,8 +2,7 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from
import { ActivatedRoute, Router } from '@angular/router';
import { BreadcrumbService } from '@core/breadcrumb';
import { Config } from '@core/config';
import { DomainRemissionService } from '@domain/remission';
import { DialogModel, UiDialogModalComponent, UiErrorModalComponent, UiModalService } from '@ui/modal';
import { DialogModel, UiDialogModalComponent, UiModalService } from '@ui/modal';
import { UiSearchboxNextComponent } from '@ui/searchbox';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@@ -24,10 +23,21 @@ export class FinishShippingDocumentComponent implements OnInit, OnDestroy {
hint$ = new Subject<string>();
get supplierId(): number {
return Number(this._activatedRoute?.snapshot?.queryParams?.supplier);
}
get source(): string {
return this._activatedRoute?.snapshot?.queryParams?.source;
}
get returnId(): number {
return Number(this._activatedRoute?.snapshot?.params?.returnId);
}
constructor(
private _activatedRoute: ActivatedRoute,
private _modal: UiModalService,
private _remissionService: DomainRemissionService,
private _router: Router,
private _breadcrumb: BreadcrumbService,
private _config: Config
@@ -67,86 +77,19 @@ export class FinishShippingDocumentComponent implements OnInit, OnDestroy {
modal.afterClosed$.pipe(takeUntil(this._onDestroy$)).subscribe(async (result) => {
if (result?.data === 'correct') {
if (await this.completeReceipt(query)) {
await this.completeReturn();
}
await this.navigateToFinishRemission();
await this.navigateToRemissionList(query);
} else if (result?.data === 'rescan') {
this.searchboxComponent.clear();
}
});
}
async completeReceipt(packageCode: string) {
const returnId = +this._activatedRoute.snapshot.params?.returnId;
const receiptId = +this._activatedRoute.snapshot.params?.receiptId;
if (receiptId) {
try {
await this._remissionService.completeReceipt(returnId, receiptId, packageCode);
return true;
} catch (err) {
this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Abschließen des Warenbegleitscheins',
data: err,
});
}
} else {
this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Abschließen des Warenbegleitscheins',
data: new Error('Keine gültige ID'),
});
}
return false;
}
async completeReturn() {
const returnId = +this._activatedRoute.snapshot.params?.returnId;
if (returnId) {
try {
await this._remissionService.completeReturn(returnId);
} catch (err) {
this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Abschließen der Remission',
data: err,
});
}
} else {
this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Abschließen der Remission',
data: new Error('Keine gültige ID'),
});
}
}
async navigateToFinishRemission() {
await this._router.navigate(['../../', 'finish-remission'], {
relativeTo: this._activatedRoute,
queryParams: { ...this.cleanupQueryParams(this._activatedRoute.snapshot.queryParams) },
async navigateToRemissionList(packageNumber: string) {
await this._router.navigate(['/filiale', 'remission', this.returnId, packageNumber, 'list'], {
queryParams: { ...this._activatedRoute.snapshot.queryParams, supplier: this.supplierId, source: this.source },
});
}
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };
delete clean['scroll_position'];
for (const key in clean) {
if (Object.prototype.hasOwnProperty.call(clean, key)) {
if (clean[key] == undefined) {
delete clean[key];
}
}
}
return clean;
}
addBreadcrumbIfNotExists() {
const returnId = +this._activatedRoute.snapshot.params?.returnId;
const receiptId = +this._activatedRoute.snapshot.params?.receiptId;

View File

@@ -205,6 +205,7 @@ export class RemissionListComponent implements OnInit, OnDestroy {
async updateBreadcrumb(queryParams: Record<string, string> = {}) {
const scroll_position = this.scrollContainer.measureScrollOffset('top');
const returnId = this._activatedRoute?.snapshot?.params?.returnId;
const packageNumber = this._activatedRoute?.snapshot?.params?.packageNumber;
const crumbs = await this._breadcrumb
.getBreadcrumbsByKeyAndTags$(this._remissionListStore.processId, ['remission'])
@@ -215,7 +216,7 @@ export class RemissionListComponent implements OnInit, OnDestroy {
for (const crumb of crumbs) {
this._breadcrumb.patchBreadcrumb(crumb.id, {
params,
path: `/filiale/remission/${returnId ? returnId + '/' : ''}list`,
path: `/filiale/remission/${returnId ? returnId + '/' : ''}${packageNumber ? packageNumber + '/' : ''}list`,
});
}
}

View File

@@ -24,11 +24,11 @@ const routes: Routes = [
component: RemissionListComponent,
},
{
path: ':returnId/list',
path: ':returnId/:packageNumber/list',
component: RemissionListComponent,
},
{
path: ':returnId/shipping-document',
path: ':returnId/:packageNumber/shipping-document',
component: ShippingDocumentDetailsComponent,
},
{

View File

@@ -27,7 +27,7 @@
<div class="actions">
<ng-container *ngIf="remissionStarted$ | async">
<a
[routerLink]="['/filiale', 'remission', returnId$ | async, 'list']"
[routerLink]="['/filiale', 'remission', returnId$ | async, packageNumber, 'list']"
[queryParams]="queryParams$ | async"
class="flex items-center bg-white font-bold text-lg px-6 py-3 rounded-full shadow-cta whitespace-nowrap"
>
@@ -35,9 +35,8 @@
Warenbegleitschein befüllen
</a>
<button
[routerLink]="['..', 'finish-shipping-document', firstReceiptId$ | async]"
[queryParams]="queryParams$ | async"
[disabled]="finishShippingDocumentDisabled$ | async"
(click)="complete()"
class="bg-brand text-white font-bold text-lg px-6 py-3 rounded-full shadow-cta whitespace-nowrap"
>
Wanne abschließen

View File

@@ -6,6 +6,7 @@ import { Config } from '@core/config';
import { DomainRemissionService } from '@domain/remission';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { ReturnDTO } from '@swagger/remi';
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { NEVER, Observable } from 'rxjs';
import { catchError, filter, first, map, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { ShortReceiptNumberPipe } from '../../pipes/short-receipt-number.pipe';
@@ -34,8 +35,6 @@ export class ShippingDocumentDetailsComponent extends ComponentStore<ShippingDoc
hasItems$: Observable<boolean>;
firstReceiptId$: Observable<number>;
finishShippingDocumentDisabled$: Observable<boolean>;
get remissionStarted() {
@@ -50,10 +49,18 @@ export class ShippingDocumentDetailsComponent extends ComponentStore<ShippingDoc
return this.get((s) => s.return);
}
get firstReceipt() {
return this.return.receipts?.find((_) => true)?.data;
}
get return$() {
return this.select((s) => s.return).pipe(shareReplay());
}
get packageNumber() {
return this._activatedRoute?.snapshot?.params?.packageNumber;
}
scrollPosition$ = this._breadcrumb.getBreadcrumbsByKeyAndTags$(this.processId, ['remission']).pipe(
map((crumbs) => {
const crumbParams = crumbs?.find((_) => true)?.params;
@@ -81,7 +88,8 @@ export class ShippingDocumentDetailsComponent extends ComponentStore<ShippingDoc
private _config: Config,
private _shortReceiptNumberPipe: ShortReceiptNumberPipe,
private _router: Router,
private _cache: CacheService
private _cache: CacheService,
private _modal: UiModalService
) {
super({ return: undefined });
}
@@ -94,8 +102,6 @@ export class ShippingDocumentDetailsComponent extends ComponentStore<ShippingDoc
map((returnDto) => returnDto?.receipts?.find((_) => true)?.data?.items?.length === 0)
);
this.firstReceiptId$ = this.return$.pipe(map((returnDto) => returnDto?.receipts?.find((_) => true)?.data?.id));
this.notCompleted$ = this.return$.pipe(map((returnDto) => !returnDto?.completed && !this.remissionStarted));
this.returnId$ = this.return$.pipe(map((returnDto) => returnDto?.id));
@@ -151,6 +157,79 @@ export class ShippingDocumentDetailsComponent extends ComponentStore<ShippingDoc
this.reloadReturn();
}
async complete() {
if (await this.completeReceipt(this.packageNumber)) {
await this.completeReturn();
}
}
async completeReceipt(packageCode: string) {
const returnId = this.return?.id;
const receiptId = this.firstReceipt?.id;
if (receiptId) {
try {
await this._remissionService.completeReceipt(returnId, receiptId, packageCode);
return true;
} catch (err) {
this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Abschließen des Warenbegleitscheins',
data: err,
});
}
} else {
this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Abschließen des Warenbegleitscheins',
data: new Error('Keine gültige ID'),
});
}
return false;
}
async completeReturn() {
const returnId = this.return?.id;
if (returnId) {
try {
await this._remissionService.completeReturn(returnId);
await this._router.navigate(['/filiale', 'remission', returnId, 'finish-remission'], {
relativeTo: this._activatedRoute,
queryParams: { ...this.cleanupQueryParams(this._activatedRoute.snapshot.queryParams) },
});
} catch (err) {
this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Abschließen der Remission',
data: err,
});
}
} else {
this._modal.open({
content: UiErrorModalComponent,
title: 'Fehler beim Abschließen der Remission',
data: new Error('Keine gültige ID'),
});
}
}
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };
delete clean['scroll_position'];
for (const key in clean) {
if (Object.prototype.hasOwnProperty.call(clean, key)) {
if (clean[key] == undefined) {
delete clean[key];
}
}
}
return clean;
}
reloadReturn = this.effect(($) =>
$.pipe(
tap((_) => {

View File

@@ -20,7 +20,7 @@
::ng-deep shared-branch-selector ui-autocomplete .ui-autocomplete-output-wrapper {
@apply overflow-hidden overflow-y-auto max-w-content rounded-b-md;
max-height: 500px;
max-height: 350px;
width: 100%;
left: unset;
box-shadow: 0px 14px 14px rgba(206, 212, 219, 0.2);

View File

@@ -20,7 +20,7 @@ import { UiAutocompleteComponent, UiAutocompleteModule } from '@ui/autocomplete'
import { UiCommonModule } from '@ui/common';
import { UiIconModule } from '@ui/icon';
import { isNaN } from 'lodash';
import { combineLatest, Subject } from 'rxjs';
import { asapScheduler, combineLatest, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { BranchSelectorStore } from './branch-selector.store';
@@ -79,6 +79,13 @@ export class BranchSelectorComponent implements OnInit, OnDestroy, AfterViewInit
onChange = (value: BranchDTO) => {};
onTouched = () => {};
@ViewChild('branchInput')
branchInput: ElementRef<HTMLInputElement>;
get isOpen() {
return this.autocompleteComponent?.open ?? false;
}
constructor(public store: BranchSelectorStore, private _elementRef: ElementRef) {}
writeValue(obj: any): void {
@@ -188,6 +195,13 @@ export class BranchSelectorComponent implements OnInit, OnDestroy, AfterViewInit
this.complete.next('');
}
focus() {
asapScheduler.schedule(() => {
this.branchInput?.nativeElement?.focus();
this.openComplete();
});
}
@HostListener('focusout', ['$event'])
closeAutocomplete(event?: FocusEvent) {
// Soll bei Klick auf den Branch-Selector und auf die Scrollbar das Autocomplete nicht schließen

View File

@@ -81,14 +81,16 @@
<div class="value">{{ orderItem?.clientChannel | environmentChannel }}</div>
</div>
<div class="detail justify-space-between" [ngSwitch]="orderItem.processingStatus" data-detail-id="Geaendert">
<ng-container *ngSwitchCase="16">
<ng-container *ngTemplateOutlet="vslLieferdatum"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="8192">
<ng-container *ngTemplateOutlet="vslLieferdatum"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="128">
<ng-container *ngTemplateOutlet="abholfrist"></ng-container>
<ng-container *ifRole="'Store'">
<ng-container *ngSwitchCase="16">
<ng-container *ngTemplateOutlet="vslLieferdatum"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="8192">
<ng-container *ngTemplateOutlet="vslLieferdatum"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="128">
<ng-container *ngTemplateOutlet="abholfrist"></ng-container>
</ng-container>
</ng-container>
<ng-container *ngSwitchDefault>
<div class="label">Geändert</div>
@@ -96,26 +98,104 @@
</ng-container>
</div>
<div class="detail" data-detail-id="Benachrichtigung">
<div *ifRole="'Store'" class="detail" data-detail-id="Benachrichtigung">
<div class="label">Benachrichtigung</div>
<div class="value">{{ (notificationsChannel | notificationsChannel) || '-' }}</div>
</div>
<div
class="detail justify-space-between"
data-detail-id="Wunschdatum"
*ngIf="orderItem.processingStatus === 16 || orderItem.processingStatus === 8192"
>
<ng-container *ngTemplateOutlet="preferredPickUpDate"></ng-container>
</div>
<ng-container *ifRole="'CallCenter'">
<div *ngIf="!!digOrderNumber" class="detail" data-detail-id="Dig-Bestellnummer">
<div class="label">Dig-Bestell Nr.</div>
<div class="value">{{ digOrderNumber }}</div>
</div>
</ng-container>
<ng-container *ifRole="'Store'">
<div
class="detail justify-space-between"
data-detail-id="Wunschdatum"
*ngIf="orderItem.processingStatus === 16 || orderItem.processingStatus === 8192"
>
<ng-container *ngTemplateOutlet="preferredPickUpDate"></ng-container>
</div>
</ng-container>
</div>
</div>
<div class="goods-in-out-header-actions" *ngIf="showMultiselect$ | async">
<div class="goods-in-out-header-select" *ngIf="showMultiselect$ | async">
<button class="cta-select-all" (click)="selectAll()">Alle auswählen</button>
<br />
{{ selectedOrderItemCount$ | async }} von {{ orderItemCount$ | async }} Titeln
</div>
<div *ifRole="'CallCenter'" class="goods-in-out-header-additional-information">
<div *ngIf="showFeature" class="goods-in-out-header-additional-feature">
<ng-container [ngSwitch]="order.features.orderType">
<ng-container *ngSwitchCase="'Versand'">
<ui-icon size="22px" icon="truck"></ui-icon>
<p>Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'DIG-Versand'">
<ui-icon size="22px" icon="truck"></ui-icon>
<p>Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'B2B-Versand'">
<ui-icon size="22px" icon="truck_b2b"></ui-icon>
<p>B2B-Versand</p>
</ng-container>
<ng-container *ngSwitchCase="'Abholung'">
<ui-icon size="22px" icon="box_out"></ui-icon>
<p>Abholung</p>
</ng-container>
<ng-container *ngSwitchCase="'Rücklage'">
<ui-icon size="22px" icon="shopping_bag"></ui-icon>
<p>Rücklage</p>
</ng-container>
<ng-container *ngSwitchCase="'Download'">
<ui-icon size="22px" icon="download"></ui-icon>
<p>Download</p>
</ng-container>
</ng-container>
</div>
<div class="goods-in-out-header-additional-addresses" *ngIf="showAddresses">
<button (click)="openAddresses = !openAddresses" class="goods-in-out-header-addresses-button">
Lieferadresse / Rechnungsadresse {{ openAddresses ? 'ausblenden' : 'anzeigen' }}
</button>
<div class="goods-in-out-header-addresses-popover" *ngIf="openAddresses">
<button (click)="openAddresses = !openAddresses" class="close"><ui-icon icon="close" size="17px"></ui-icon></button>
<div class="goods-in-out-header-addresses-popover-data">
<div *ngIf="order.shipping" class="goods-in-out-header-addresses-popover-delivery">
<p>Lieferadresse</p>
<div class="goods-in-out-header-addresses-popover-delivery-data">
<ng-container *ngIf="order.shipping?.data?.organisation">
<p>{{ order.shipping?.data?.organisation?.name }}</p>
<p>{{ order.shipping?.data?.organisation?.department }}</p>
</ng-container>
<p>{{ order.shipping?.data?.firstName }} {{ order.shipping?.data?.lastName }}</p>
<p>{{ order.shipping?.data?.address?.street }} {{ order.shipping?.data?.address?.streetNumber }}</p>
<p>{{ order.shipping?.data?.address?.zipCode }} {{ order.shipping?.data?.address?.city }}</p>
<p>{{ order.shipping?.data?.address?.info }}</p>
</div>
</div>
<div *ngIf="order.billing" class="goods-in-out-header-addresses-popover-billing">
<p>Rechnungsadresse</p>
<div class="goods-in-out-header-addresses-popover-billing-data">
<ng-container *ngIf="order.billing?.data?.organisation">
<p>{{ order.billing?.data?.organisation?.name }}</p>
<p>{{ order.billing?.data?.organisation?.department }}</p>
</ng-container>
<p>{{ order.billing?.data?.firstName }} {{ order.billing?.data?.lastName }}</p>
<p>{{ order.billing?.data?.address?.street }} {{ order.billing?.data?.address?.streetNumber }}</p>
<p>{{ order.billing?.data?.address?.zipCode }} {{ order.billing?.data?.address?.city }}</p>
<p>{{ order.billing?.data?.address?.info }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ng-template #featureLoading>

View File

@@ -93,8 +93,65 @@
}
}
.goods-in-out-header-actions {
@apply text-right;
.goods-in-out-header-select {
@apply flex flex-col items-end;
}
.goods-in-out-header-additional-information {
@apply flex flex-row items-end relative pt-4;
.goods-in-out-header-additional-feature {
@apply flex flex-row items-center mr-3;
ui-icon {
@apply mr-[9px];
}
p {
@apply font-bold text-lg;
}
}
.goods-in-out-header-additional-addresses {
.goods-in-out-header-addresses-button {
@apply bg-white text-[#0556B4];
}
.goods-in-out-header-addresses-popover {
@apply absolute inset-x-0 -mx-4 top-12 bottom-0 z-popover;
.close {
@apply bg-white absolute right-0 p-6;
}
.goods-in-out-header-addresses-popover-data {
@apply flex flex-col bg-white p-6 z-popover min-h-[200px];
box-shadow: 0px 6px 24px rgba(206, 212, 219, 0.8);
.goods-in-out-header-addresses-popover-delivery {
@apply grid mb-6;
grid-template-columns: 153px auto;
.goods-in-out-header-addresses-popover-delivery-data {
p {
@apply font-bold;
}
}
}
.goods-in-out-header-addresses-popover-billing {
@apply grid;
grid-template-columns: 153px auto;
.goods-in-out-header-addresses-popover-billing-data {
p {
@apply font-bold;
}
}
}
}
}
}
}
.cta-select-all {

View File

@@ -82,6 +82,20 @@ export class SharedGoodsInOutOrderDetailsHeaderComponent implements OnChanges {
map(([statusActions, crudaUpdate]) => statusActions?.length > 0 && crudaUpdate)
);
openAddresses: boolean = false;
get digOrderNumber(): string {
return this.order?.linkedRecords?.find((_) => true)?.number;
}
get showAddresses(): boolean {
return (this.order?.orderType === 2 || this.order?.orderType === 4) && (!!this.order?.shipping || !!this.order?.billing);
}
get showFeature(): boolean {
return !!this.order?.features && !!this.order?.features?.orderType;
}
constructor(
@Host() private host: SharedGoodsInOutOrderDetailsComponent,
private customerService: CrmCustomerService,

View File

@@ -74,11 +74,14 @@
<div class="detail">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
<button class="cta-more" *ngIf="(more$ | async) === false" (click)="setMore(true)">
Mehr <ui-icon size="15px" icon="arrow"></ui-icon>
</button>
<ng-container *ifRole="'Store'">
<button class="cta-more" *ngIf="(showMore$ | async) === false" (click)="setMore(true)">
Mehr <ui-icon size="15px" icon="arrow"></ui-icon>
</button>
</ng-container>
</div>
<ng-container *ngIf="more$ | async">
<ng-container *ngIf="showMore$ | async">
<div class="detail">
<div class="label">Zielfiliale</div>
<div class="value">{{ orderItem.targetBranch }}</div>
@@ -95,15 +98,38 @@
orderItemFeature(orderItem) === 'B2B-Versand' ||
orderItemFeature(orderItem) === 'DIG-Versand'
"
>Lieferung ab</ng-container
>{{ orderItem?.estimatedDelivery ? 'Lieferung zwischen' : 'Lieferung ab' }}</ng-container
>
<ng-container *ngIf="orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage'">
Abholung ab
</ng-container>
</div>
<div class="value">
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
</div>
<ng-container *ngIf="orderItem?.estimatedDelivery || orderItem?.estimatedShippingDate">
<ng-container *ifRole="'CallCenter'">
<div class="value bg-[#D8DFE5] rounded-card w-max px-2">
<ng-container *ngIf="orderItem?.estimatedDelivery; else estimatedShippingDate">
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
</ng-container>
</div>
</ng-container>
<ng-container *ifRole="'Store'">
<div class="value">
<ng-container *ngIf="orderItem?.estimatedDelivery; else estimatedShippingDate">
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
</ng-container>
</div>
</ng-container>
</ng-container>
<ng-template #estimatedShippingDate>
<ng-container *ngIf="orderItem?.estimatedShippingDate">
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
</ng-container>
</ng-template>
</div>
<div class="detail">
<div class="label">Vormerker</div>
@@ -146,9 +172,11 @@
</div>
</ng-container>
<button class="cta-more" *ngIf="more$ | async" (click)="setMore(false)">
<ui-icon class="less" size="15px" icon="arrow"></ui-icon> Weniger
</button>
<ng-container *ifRole="'Store'">
<button class="cta-more" *ngIf="showMore$ | async" (click)="setMore(false)">
<ui-icon class="less" size="15px" icon="arrow"></ui-icon> Weniger
</button>
</ng-container>
</ng-container>
<div class="history-wrapper">
<button [disabled]="fetchHistory$ | async" class="cta-history" (click)="openHistory()">

View File

@@ -11,6 +11,7 @@ import {
ViewChild,
} from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { AuthService } from '@core/auth';
import { DomainOmsService, DomainReceiptService } from '@domain/oms';
import { HistoryModalComponent, HistoryData } from '@modal/history';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
@@ -140,6 +141,8 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<Sh
more$ = this.select((s) => s.more);
showMore$ = this.more$.pipe(map((more) => more || this._auth.hasRole('CallCenter')));
fetchHistory$ = new BehaviorSubject<boolean>(false);
private _onDestroy$ = new Subject();
@@ -148,7 +151,8 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<Sh
private _domainReceiptService: DomainReceiptService,
private _omsService: DomainOmsService,
private _modal: UiModalService,
private _cdr: ChangeDetectorRef
private _cdr: ChangeDetectorRef,
private _auth: AuthService
) {
super({
more: false,
@@ -275,7 +279,7 @@ export class SharedGoodsInOutOrderDetailsItemComponent extends ComponentStore<Sh
orderItemFeature(orderItemListItem: OrderItemListItemDTO) {
const orderItems = this.order?.items;
return orderItems.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features?.orderType;
return orderItems?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features?.orderType;
}
triggerResize() {

View File

@@ -19,6 +19,7 @@ import { UiSelectBulletModule } from '@ui/select-bullet';
import { UiSpinnerModule } from 'apps/ui/spinner/src/lib/ui-spinner.module';
import { UiTooltipModule } from '@ui/tooltip';
import { TextFieldModule } from '@angular/cdk/text-field';
import { AuthModule } from '@core/auth';
@NgModule({
imports: [
@@ -37,6 +38,7 @@ import { TextFieldModule } from '@angular/cdk/text-field';
UiSpinnerModule,
UiTooltipModule,
TextFieldModule,
AuthModule,
],
exports: [
SharedGoodsInOutOrderDetailsComponent,

Some files were not shown because too many files have changed in this diff Show More