mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merge branch 'release/4.3'
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import type { Preview } from '@storybook/angular';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
|
||||
registerLocaleData(localeDe);
|
||||
|
||||
const preview: Preview = {
|
||||
tags: ['autodocs'],
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { inject, isDevMode, NgModule } from '@angular/core';
|
||||
import { Location } from '@angular/common';
|
||||
import { RouterModule, Routes, Router } from '@angular/router';
|
||||
import { isDevMode, NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
import {
|
||||
CanActivateCartGuard,
|
||||
CanActivateCartWithProcessIdGuard,
|
||||
@@ -11,7 +10,6 @@ import {
|
||||
CanActivateGoodsInGuard,
|
||||
CanActivateProductGuard,
|
||||
CanActivateProductWithProcessIdGuard,
|
||||
CanActivateRemissionGuard,
|
||||
CanActivateTaskCalendarGuard,
|
||||
IsAuthenticatedGuard,
|
||||
} from './guards';
|
||||
@@ -31,12 +29,7 @@ import {
|
||||
ActivateProcessIdWithConfigKeyGuard,
|
||||
} from './guards/activate-process-id.guard';
|
||||
import { MatomoRouteData } from 'ngx-matomo-client';
|
||||
import {
|
||||
tabResolverFn,
|
||||
TabService,
|
||||
TabNavigationService,
|
||||
processResolverFn,
|
||||
} from '@isa/core/tabs';
|
||||
import { tabResolverFn, processResolverFn } from '@isa/core/tabs';
|
||||
import { provideScrollPositionRestoration } from '@isa/utils/scroll-position';
|
||||
|
||||
const routes: Routes = [
|
||||
@@ -193,9 +186,31 @@ const routes: Routes = [
|
||||
children: [
|
||||
{
|
||||
path: 'reward',
|
||||
loadChildren: () =>
|
||||
import('@isa/checkout/feature/reward-catalog').then((m) => m.routes),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('@isa/checkout/feature/reward-catalog').then(
|
||||
(m) => m.routes,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'cart',
|
||||
loadChildren: () =>
|
||||
import('@isa/checkout/feature/reward-shopping-cart').then(
|
||||
(m) => m.routes,
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'order-confirmation',
|
||||
loadChildren: () =>
|
||||
import('@isa/checkout/feature/reward-order-confirmation').then(
|
||||
(m) => m.routes,
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: 'return',
|
||||
loadChildren: () =>
|
||||
@@ -242,9 +257,4 @@ if (isDevMode()) {
|
||||
exports: [RouterModule],
|
||||
providers: [provideScrollPositionRestoration()],
|
||||
})
|
||||
export class AppRoutingModule {
|
||||
constructor() {
|
||||
// Loading TabNavigationService to ensure tab state is synced with tab location
|
||||
inject(TabNavigationService);
|
||||
}
|
||||
}
|
||||
export class AppRoutingModule {}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
NgModule,
|
||||
inject,
|
||||
provideAppInitializer,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
@@ -78,8 +79,15 @@ import {
|
||||
ConsoleLogSink,
|
||||
logger as loggerFactory,
|
||||
} from '@isa/core/logging';
|
||||
import { IDBStorageProvider, UserStorageProvider } from '@isa/core/storage';
|
||||
import {
|
||||
IDBStorageProvider,
|
||||
provideUserSubFactory,
|
||||
UserStorageProvider,
|
||||
} from '@isa/core/storage';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { OAuthService } from 'angular-oauth2-oidc';
|
||||
import z from 'zod';
|
||||
import { TabNavigationService } from '@isa/core/tabs';
|
||||
|
||||
registerLocaleData(localeDe, localeDeExtra);
|
||||
registerLocaleData(localeDe, 'de', localeDeExtra);
|
||||
@@ -124,11 +132,12 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
const auth = injector.get(AuthService);
|
||||
try {
|
||||
await auth.init();
|
||||
} catch (error) {
|
||||
} catch {
|
||||
statusElement.innerHTML = 'Authentifizierung wird durchgeführt...';
|
||||
logger.info('Performing login');
|
||||
const strategy = injector.get(LoginStrategy);
|
||||
await strategy.login();
|
||||
return;
|
||||
}
|
||||
|
||||
statusElement.innerHTML = 'Native Container wird initialisiert...';
|
||||
@@ -169,6 +178,8 @@ export function _appInitializerFactory(config: Config, injector: Injector) {
|
||||
});
|
||||
|
||||
logger.info('Application initialization completed');
|
||||
// Inject tab navigation service to initialize it
|
||||
injector.get(TabNavigationService).init();
|
||||
} catch (error) {
|
||||
logger.error('Application initialization failed', error as Error, () => ({
|
||||
message: (error as Error).message,
|
||||
@@ -217,6 +228,31 @@ export function _notificationsHubOptionsFactory(
|
||||
return options;
|
||||
}
|
||||
|
||||
const USER_SUB_FACTORY = () => {
|
||||
const _logger = loggerFactory(() => ({
|
||||
context: 'USER_SUB',
|
||||
}));
|
||||
const auth = inject(OAuthService);
|
||||
|
||||
const claims = auth.getIdentityClaims();
|
||||
|
||||
if (!claims || typeof claims !== 'object' || !('sub' in claims)) {
|
||||
const err = new Error('No valid identity claims found. User is anonymous.');
|
||||
_logger.error(err.message);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const validation = z.string().safeParse(claims['sub']);
|
||||
|
||||
if (!validation.success) {
|
||||
const err = new Error('Invalid "sub" claim in identity claims.');
|
||||
_logger.error(err.message, { claims });
|
||||
throw err;
|
||||
}
|
||||
|
||||
return signal(validation.data);
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
declarations: [AppComponent, MainComponent],
|
||||
bootstrap: [AppComponent],
|
||||
@@ -290,6 +326,7 @@ export function _notificationsHubOptionsFactory(
|
||||
provide: DEFAULT_CURRENCY_CODE,
|
||||
useValue: 'EUR',
|
||||
},
|
||||
provideUserSubFactory(USER_SUB_FACTORY),
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -1,205 +1,206 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from "@angular/router";
|
||||
import { ApplicationProcess, ApplicationService } from "@core/application";
|
||||
import { DomainCheckoutService } from "@domain/checkout";
|
||||
import { logger } from "@isa/core/logging";
|
||||
import { CustomerSearchNavigation } from "@shared/services/navigation";
|
||||
import { first } from "rxjs/operators";
|
||||
|
||||
@Injectable({ providedIn: "root" })
|
||||
export class CanActivateCustomerGuard {
|
||||
#logger = logger(() => ({
|
||||
context: "CanActivateCustomerGuard",
|
||||
tags: ["guard", "customer", "navigation"],
|
||||
}));
|
||||
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _router: Router,
|
||||
private readonly _navigation: CustomerSearchNavigation,
|
||||
) {}
|
||||
|
||||
async canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
{ url }: RouterStateSnapshot,
|
||||
) {
|
||||
if (url.startsWith("/kunde/customer/search/")) {
|
||||
const processId = Date.now(); // Generate a new process ID
|
||||
// Extract parts before and after the pattern
|
||||
const parts = url.split("/kunde/customer/");
|
||||
if (parts.length === 2) {
|
||||
const prefix = parts[0] + "/kunde/";
|
||||
const suffix = "customer/" + parts[1];
|
||||
|
||||
// Construct the new URL with process ID inserted
|
||||
const newUrl = `${prefix}${processId}/${suffix}`;
|
||||
|
||||
this.#logger.info("Redirecting to URL with process ID", () => ({
|
||||
originalUrl: url,
|
||||
newUrl,
|
||||
processId,
|
||||
}));
|
||||
|
||||
// Navigate to the new URL and prevent original navigation
|
||||
this._router.navigateByUrl(newUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const processes = await this._applicationService
|
||||
.getProcesses$("customer")
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const lastActivatedProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$("customer", "cart")
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedCartCheckoutProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$("customer", "cart-checkout")
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedGoodsOutProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$("customer", "goods-out")
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const activatedProcessId = await this._applicationService
|
||||
.getActivatedProcessId$()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
|
||||
if (
|
||||
!!lastActivatedCartCheckoutProcessId &&
|
||||
lastActivatedCartCheckoutProcessId === activatedProcessId
|
||||
) {
|
||||
await this.fromCartCheckoutProcess(
|
||||
processes,
|
||||
lastActivatedCartCheckoutProcessId,
|
||||
);
|
||||
return false;
|
||||
} else if (
|
||||
!!lastActivatedGoodsOutProcessId &&
|
||||
lastActivatedGoodsOutProcessId === activatedProcessId
|
||||
) {
|
||||
await this.fromGoodsOutProcess(processes, lastActivatedGoodsOutProcessId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lastActivatedProcessId) {
|
||||
await this.fromCartProcess(processes);
|
||||
return false;
|
||||
} else {
|
||||
await this.navigateToDefaultRoute(lastActivatedProcessId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async navigateToDefaultRoute(processId: number) {
|
||||
const route = this._navigation.defaultRoute({ processId });
|
||||
|
||||
await this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Kundensuche
|
||||
async fromCartProcess(processes: ApplicationProcess[]) {
|
||||
const newProcessId = Date.now();
|
||||
await this._applicationService.createProcess({
|
||||
id: newProcessId,
|
||||
type: "cart",
|
||||
section: "customer",
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`,
|
||||
});
|
||||
|
||||
await this.navigateToDefaultRoute(newProcessId);
|
||||
}
|
||||
|
||||
// Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
|
||||
async fromCartCheckoutProcess(
|
||||
processes: ApplicationProcess[],
|
||||
processId: number,
|
||||
) {
|
||||
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
|
||||
this._checkoutService.removeProcess({ processId });
|
||||
|
||||
// Ändere type cart-checkout zu cart
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: "cart",
|
||||
section: "customer",
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`,
|
||||
data: {},
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this.navigateToDefaultRoute(processId);
|
||||
}
|
||||
|
||||
// Bei offener Warenausgabe und Klick auf Footer Kundensuche
|
||||
async fromGoodsOutProcess(
|
||||
processes: ApplicationProcess[],
|
||||
processId: number,
|
||||
) {
|
||||
const buyer = await this._checkoutService
|
||||
.getBuyer({ processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const customerFeatures = await this._checkoutService
|
||||
.getCustomerFeatures({ processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const name = buyer
|
||||
? customerFeatures?.b2b
|
||||
? buyer.organisation?.name
|
||||
? buyer.organisation?.name
|
||||
: buyer.lastName
|
||||
: buyer.lastName
|
||||
: `Vorgang ${this.processNumber(processes.filter((process) => process.type === "cart"))}`;
|
||||
|
||||
// Ändere type goods-out zu cart
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: "cart",
|
||||
section: "customer",
|
||||
name,
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this.navigateToDefaultRoute(processId);
|
||||
}
|
||||
|
||||
processNumber(processes: ApplicationProcess[]) {
|
||||
const processNumbers = processes?.map((process) =>
|
||||
Number(process?.name?.replace(/\D/g, "")),
|
||||
);
|
||||
return !!processNumbers && processNumbers.length > 0
|
||||
? this.findMissingNumber(processNumbers)
|
||||
: 1;
|
||||
}
|
||||
|
||||
findMissingNumber(processNumbers: number[]) {
|
||||
for (
|
||||
let missingNumber = 1;
|
||||
missingNumber < Math.max(...processNumbers);
|
||||
missingNumber++
|
||||
) {
|
||||
if (!processNumbers.find((number) => number === missingNumber)) {
|
||||
return missingNumber;
|
||||
}
|
||||
}
|
||||
return Math.max(...processNumbers) + 1;
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { first } from 'rxjs/operators';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CanActivateCustomerGuard {
|
||||
#logger = logger(() => ({
|
||||
module: 'isa-app',
|
||||
importMetaUrl: import.meta.url,
|
||||
class: 'CanActivateCustomerGuard',
|
||||
}));
|
||||
|
||||
constructor(
|
||||
private readonly _applicationService: ApplicationService,
|
||||
private readonly _checkoutService: DomainCheckoutService,
|
||||
private readonly _router: Router,
|
||||
private readonly _navigation: CustomerSearchNavigation,
|
||||
) {}
|
||||
|
||||
async canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
{ url }: RouterStateSnapshot,
|
||||
) {
|
||||
if (url.startsWith('/kunde/customer/search/')) {
|
||||
const processId = Date.now(); // Generate a new process ID
|
||||
// Extract parts before and after the pattern
|
||||
const parts = url.split('/kunde/customer/');
|
||||
if (parts.length === 2) {
|
||||
const prefix = parts[0] + '/kunde/';
|
||||
const suffix = 'customer/' + parts[1];
|
||||
|
||||
// Construct the new URL with process ID inserted
|
||||
const newUrl = `${prefix}${processId}/${suffix}`;
|
||||
|
||||
this.#logger.info('Redirecting to URL with process ID', () => ({
|
||||
originalUrl: url,
|
||||
newUrl,
|
||||
processId,
|
||||
}));
|
||||
|
||||
// Navigate to the new URL and prevent original navigation
|
||||
this._router.navigateByUrl(newUrl);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const processes = await this._applicationService
|
||||
.getProcesses$('customer')
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const lastActivatedProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$('customer', 'cart')
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedCartCheckoutProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$('customer', 'cart-checkout')
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const lastActivatedGoodsOutProcessId = (
|
||||
await this._applicationService
|
||||
.getLastActivatedProcessWithSectionAndType$('customer', 'goods-out')
|
||||
.pipe(first())
|
||||
.toPromise()
|
||||
)?.id;
|
||||
|
||||
const activatedProcessId = await this._applicationService
|
||||
.getActivatedProcessId$()
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
// Darf nur reinkommen wenn der aktuell aktive Tab ein Bestellabschluss Tab ist
|
||||
if (
|
||||
!!lastActivatedCartCheckoutProcessId &&
|
||||
lastActivatedCartCheckoutProcessId === activatedProcessId
|
||||
) {
|
||||
await this.fromCartCheckoutProcess(
|
||||
processes,
|
||||
lastActivatedCartCheckoutProcessId,
|
||||
);
|
||||
return false;
|
||||
} else if (
|
||||
!!lastActivatedGoodsOutProcessId &&
|
||||
lastActivatedGoodsOutProcessId === activatedProcessId
|
||||
) {
|
||||
await this.fromGoodsOutProcess(processes, lastActivatedGoodsOutProcessId);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lastActivatedProcessId) {
|
||||
await this.fromCartProcess(processes);
|
||||
return false;
|
||||
} else {
|
||||
await this.navigateToDefaultRoute(lastActivatedProcessId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async navigateToDefaultRoute(processId: number) {
|
||||
const route = this._navigation.defaultRoute({ processId });
|
||||
|
||||
await this._router.navigate(route.path, { queryParams: route.queryParams });
|
||||
}
|
||||
|
||||
// Bei offener Artikelsuche/Kundensuche und Klick auf Footer Kundensuche
|
||||
async fromCartProcess(processes: ApplicationProcess[]) {
|
||||
const newProcessId = Date.now();
|
||||
await this._applicationService.createProcess({
|
||||
id: newProcessId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
||||
});
|
||||
|
||||
await this.navigateToDefaultRoute(newProcessId);
|
||||
}
|
||||
|
||||
// Bei offener Bestellbestätigung und Klick auf Footer Kundensuche
|
||||
async fromCartCheckoutProcess(
|
||||
processes: ApplicationProcess[],
|
||||
processId: number,
|
||||
) {
|
||||
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
|
||||
this._checkoutService.removeProcess({ processId });
|
||||
|
||||
// Ändere type cart-checkout zu cart
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
|
||||
data: {},
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this.navigateToDefaultRoute(processId);
|
||||
}
|
||||
|
||||
// Bei offener Warenausgabe und Klick auf Footer Kundensuche
|
||||
async fromGoodsOutProcess(
|
||||
processes: ApplicationProcess[],
|
||||
processId: number,
|
||||
) {
|
||||
const buyer = await this._checkoutService
|
||||
.getBuyer({ processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const customerFeatures = await this._checkoutService
|
||||
.getCustomerFeatures({ processId })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const name = buyer
|
||||
? customerFeatures?.b2b
|
||||
? buyer.organisation?.name
|
||||
? buyer.organisation?.name
|
||||
: buyer.lastName
|
||||
: buyer.lastName
|
||||
: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`;
|
||||
|
||||
// Ändere type goods-out zu cart
|
||||
this._applicationService.patchProcess(processId, {
|
||||
id: processId,
|
||||
type: 'cart',
|
||||
section: 'customer',
|
||||
name,
|
||||
});
|
||||
|
||||
// Navigation
|
||||
await this.navigateToDefaultRoute(processId);
|
||||
}
|
||||
|
||||
processNumber(processes: ApplicationProcess[]) {
|
||||
const processNumbers = processes?.map((process) =>
|
||||
Number(process?.name?.replace(/\D/g, '')),
|
||||
);
|
||||
return !!processNumbers && processNumbers.length > 0
|
||||
? this.findMissingNumber(processNumbers)
|
||||
: 1;
|
||||
}
|
||||
|
||||
findMissingNumber(processNumbers: number[]) {
|
||||
for (
|
||||
let missingNumber = 1;
|
||||
missingNumber < Math.max(...processNumbers);
|
||||
missingNumber++
|
||||
) {
|
||||
if (!processNumbers.find((number) => number === missingNumber)) {
|
||||
return missingNumber;
|
||||
}
|
||||
}
|
||||
return Math.max(...processNumbers) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,8 @@ import {
|
||||
HttpErrorResponse,
|
||||
} from '@angular/common/http';
|
||||
import { from, NEVER, Observable, throwError } from 'rxjs';
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { catchError, filter, mergeMap, takeUntil } from 'rxjs/operators';
|
||||
import { AuthService, LoginStrategy } from '@core/auth';
|
||||
import { LogLevel } from '@core/logger';
|
||||
import { injectOnline$ } from '../services/network-status.service';
|
||||
import { logger } from '@isa/core/logging';
|
||||
|
||||
|
||||
@@ -142,7 +142,6 @@ export class ApplicationServiceAdapter extends ApplicationService {
|
||||
|
||||
patchProcessData(processId: number, data: Record<string, unknown>): void {
|
||||
const currentProcess = this.#tabService.entityMap()[processId];
|
||||
|
||||
const currentData: TabMetadata =
|
||||
(currentProcess?.metadata?.['process_data'] as TabMetadata) ?? {};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { coerceArray } from '@angular/cdk/coercion';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Config } from '@core/config';
|
||||
import { isNullOrUndefined } from '@utils/common';
|
||||
import { AuthConfig, OAuthService } from 'angular-oauth2-oidc';
|
||||
@@ -144,6 +144,7 @@ export class AuthService {
|
||||
if (isNullOrUndefined(token)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const base64Url = token.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
|
||||
@@ -3,31 +3,49 @@ import { ActionHandler } from './action-handler.interface';
|
||||
import { CommandService } from './command.service';
|
||||
import { FEATURE_ACTION_HANDLERS, ROOT_ACTION_HANDLERS } from './tokens';
|
||||
|
||||
export function provideActionHandlers(actionHandlers: Type<ActionHandler>[]): Provider[] {
|
||||
export function provideActionHandlers(
|
||||
actionHandlers: Type<ActionHandler>[],
|
||||
): Provider[] {
|
||||
return [
|
||||
CommandService,
|
||||
actionHandlers.map((handler) => ({ provide: FEATURE_ACTION_HANDLERS, useClass: handler, multi: true })),
|
||||
actionHandlers.map((handler) => ({
|
||||
provide: FEATURE_ACTION_HANDLERS,
|
||||
useClass: handler,
|
||||
multi: true,
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
@NgModule({})
|
||||
export class CoreCommandModule {
|
||||
static forRoot(actionHandlers: Type<ActionHandler>[]): ModuleWithProviders<CoreCommandModule> {
|
||||
static forRoot(
|
||||
actionHandlers: Type<ActionHandler>[],
|
||||
): ModuleWithProviders<CoreCommandModule> {
|
||||
return {
|
||||
ngModule: CoreCommandModule,
|
||||
providers: [
|
||||
CommandService,
|
||||
actionHandlers.map((handler) => ({ provide: ROOT_ACTION_HANDLERS, useClass: handler, multi: true })),
|
||||
actionHandlers.map((handler) => ({
|
||||
provide: ROOT_ACTION_HANDLERS,
|
||||
useClass: handler,
|
||||
multi: true,
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
static forChild(actionHandlers: Type<ActionHandler>[]): ModuleWithProviders<CoreCommandModule> {
|
||||
static forChild(
|
||||
actionHandlers: Type<ActionHandler>[],
|
||||
): ModuleWithProviders<CoreCommandModule> {
|
||||
return {
|
||||
ngModule: CoreCommandModule,
|
||||
providers: [
|
||||
CommandService,
|
||||
actionHandlers.map((handler) => ({ provide: FEATURE_ACTION_HANDLERS, useClass: handler, multi: true })),
|
||||
actionHandlers.map((handler) => ({
|
||||
provide: FEATURE_ACTION_HANDLERS,
|
||||
useClass: handler,
|
||||
multi: true,
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,10 +15,13 @@ export class CommandService {
|
||||
for (const action of actions) {
|
||||
const handler = this.getActionHandler(action);
|
||||
if (!handler) {
|
||||
console.error('CommandService.handleCommand', 'Action Handler does not exist', { action });
|
||||
console.error(
|
||||
'CommandService.handleCommand',
|
||||
'Action Handler does not exist',
|
||||
{ action },
|
||||
);
|
||||
throw new Error('Action Handler does not exist');
|
||||
}
|
||||
|
||||
data = await handler.handler(data, this);
|
||||
}
|
||||
return data;
|
||||
@@ -29,10 +32,18 @@ export class CommandService {
|
||||
}
|
||||
|
||||
getActionHandler(action: string): ActionHandler | undefined {
|
||||
const featureActionHandlers: ActionHandler[] = this.injector.get(FEATURE_ACTION_HANDLERS, []);
|
||||
const rootActionHandlers: ActionHandler[] = this.injector.get(ROOT_ACTION_HANDLERS, []);
|
||||
const featureActionHandlers: ActionHandler[] = this.injector.get(
|
||||
FEATURE_ACTION_HANDLERS,
|
||||
[],
|
||||
);
|
||||
const rootActionHandlers: ActionHandler[] = this.injector.get(
|
||||
ROOT_ACTION_HANDLERS,
|
||||
[],
|
||||
);
|
||||
|
||||
let handler = [...featureActionHandlers, ...rootActionHandlers].find((handler) => handler.action === action);
|
||||
let handler = [...featureActionHandlers, ...rootActionHandlers].find(
|
||||
(handler) => handler.action === action,
|
||||
);
|
||||
|
||||
if (this._parent && !handler) {
|
||||
handler = this._parent.getActionHandler(action);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,11 @@ export const setShoppingCart = createAction(
|
||||
props<{ processId: number; shoppingCart: ShoppingCartDTO }>(),
|
||||
);
|
||||
|
||||
export const setShoppingCartByShoppingCartId = createAction(
|
||||
`${prefix} Set Shopping Cart By Shopping Cart Id`,
|
||||
props<{ shoppingCartId: number; shoppingCart: ShoppingCartDTO }>(),
|
||||
);
|
||||
|
||||
export const setCheckout = createAction(
|
||||
`${prefix} Set Checkout`,
|
||||
props<{ processId: number; checkout: CheckoutDTO }>(),
|
||||
|
||||
@@ -1,207 +1,311 @@
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import { initialCheckoutState, storeCheckoutAdapter } from './domain-checkout.state';
|
||||
|
||||
import * as DomainCheckoutActions from './domain-checkout.actions';
|
||||
import { Dictionary } from '@ngrx/entity';
|
||||
import { CheckoutEntity } from './defs/checkout.entity';
|
||||
import { isNullOrUndefined } from '@utils/common';
|
||||
|
||||
const _domainCheckoutReducer = createReducer(
|
||||
initialCheckoutState,
|
||||
on(DomainCheckoutActions.setShoppingCart, (s, { processId, shoppingCart }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
|
||||
const addedShoppingCartItems =
|
||||
shoppingCart?.items
|
||||
?.filter((item) => !entity.shoppingCart?.items?.find((i) => i.id === item.id))
|
||||
?.map((item) => item.data) ?? [];
|
||||
|
||||
entity.shoppingCart = shoppingCart;
|
||||
|
||||
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp ? { ...entity.itemAvailabilityTimestamp } : {};
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (let shoppingCartItem of addedShoppingCartItems) {
|
||||
if (shoppingCartItem.features?.orderType) {
|
||||
entity.itemAvailabilityTimestamp[`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`] = now;
|
||||
}
|
||||
}
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.checkout = checkout;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setBuyerCommunicationDetails, (s, { processId, email, mobile }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
const communicationDetails = { ...entity.buyer.communicationDetails };
|
||||
communicationDetails.email = email || communicationDetails.email;
|
||||
communicationDetails.mobile = mobile || communicationDetails.mobile;
|
||||
entity.buyer = {
|
||||
...entity.buyer,
|
||||
communicationDetails,
|
||||
};
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setNotificationChannels, (s, { processId, notificationChannels }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
return storeCheckoutAdapter.setOne({ ...entity, notificationChannels }, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCheckoutDestination, (s, { processId, destination }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.checkout = {
|
||||
...entity.checkout,
|
||||
destinations: entity.checkout.destinations.map((dest) => {
|
||||
if (dest.id === destination.id) {
|
||||
return { ...dest, ...destination };
|
||||
}
|
||||
return { ...dest };
|
||||
}),
|
||||
};
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setShippingAddress, (s, { processId, shippingAddress }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.shippingAddress = shippingAddress;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setBuyer, (s, { processId, buyer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.buyer = buyer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setPayer, (s, { processId, payer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.payer = payer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setSpecialComment, (s, { processId, agentComment }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.specialComment = agentComment;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.removeCheckoutWithProcessId, (s, { processId }) => {
|
||||
return storeCheckoutAdapter.removeOne(processId, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({ ...s, orders: [...s.orders, ...orders] })),
|
||||
on(DomainCheckoutActions.updateOrderItem, (s, { item }) => {
|
||||
const orders = [...s.orders];
|
||||
|
||||
const orderToUpdate = orders?.find((order) => order.items?.find((i) => i.id === item?.id));
|
||||
const orderToUpdateIndex = orders?.indexOf(orderToUpdate);
|
||||
|
||||
const orderItemToUpdate = orderToUpdate?.items?.find((i) => i.id === item?.id);
|
||||
const orderItemToUpdateIndex = orderToUpdate?.items?.indexOf(orderItemToUpdate);
|
||||
|
||||
const items = [...orderToUpdate?.items];
|
||||
items[orderItemToUpdateIndex] = item;
|
||||
|
||||
orders[orderToUpdateIndex] = {
|
||||
...orderToUpdate,
|
||||
items: [...items],
|
||||
};
|
||||
|
||||
return { ...s, orders: [...orders] };
|
||||
}),
|
||||
on(DomainCheckoutActions.removeAllOrders, (s) => ({
|
||||
...s,
|
||||
orders: [],
|
||||
})),
|
||||
on(DomainCheckoutActions.setOlaError, (s, { processId, olaErrorIds }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.olaErrorIds = olaErrorIds;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCustomer, (s, { processId, customer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
entity.customer = customer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory,
|
||||
(s, { processId, shoppingCartItemId, availability }) => {
|
||||
const entity = getOrCreateCheckoutEntity({ processId, entities: s.entities });
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
|
||||
? { ...entity?.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
|
||||
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
|
||||
const entity = getCheckoutEntityByShoppingCartId({ shoppingCartId, entities: s.entities });
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
|
||||
? { ...entity?.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find((i) => i.id === shoppingCartItemId)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] = Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export function domainCheckoutReducer(state, action) {
|
||||
return _domainCheckoutReducer(state, action);
|
||||
}
|
||||
|
||||
function getOrCreateCheckoutEntity({
|
||||
entities,
|
||||
processId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
processId: number;
|
||||
}): CheckoutEntity {
|
||||
let entity = entities[processId];
|
||||
|
||||
if (isNullOrUndefined(entity)) {
|
||||
return {
|
||||
processId,
|
||||
checkout: undefined,
|
||||
shoppingCart: undefined,
|
||||
shippingAddress: undefined,
|
||||
orders: [],
|
||||
payer: undefined,
|
||||
buyer: undefined,
|
||||
specialComment: '',
|
||||
notificationChannels: 0,
|
||||
olaErrorIds: [],
|
||||
customer: undefined,
|
||||
// availabilityHistory: [],
|
||||
itemAvailabilityTimestamp: {},
|
||||
};
|
||||
}
|
||||
|
||||
return { ...entity };
|
||||
}
|
||||
|
||||
function getCheckoutEntityByShoppingCartId({
|
||||
entities,
|
||||
shoppingCartId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
shoppingCartId: number;
|
||||
}): CheckoutEntity {
|
||||
return Object.values(entities).find((entity) => entity.shoppingCart?.id === shoppingCartId);
|
||||
}
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import {
|
||||
initialCheckoutState,
|
||||
storeCheckoutAdapter,
|
||||
} from './domain-checkout.state';
|
||||
|
||||
import * as DomainCheckoutActions from './domain-checkout.actions';
|
||||
import { Dictionary } from '@ngrx/entity';
|
||||
import { CheckoutEntity } from './defs/checkout.entity';
|
||||
import { isNullOrUndefined } from '@utils/common';
|
||||
|
||||
const _domainCheckoutReducer = createReducer(
|
||||
initialCheckoutState,
|
||||
on(
|
||||
DomainCheckoutActions.setShoppingCart,
|
||||
(s, { processId, shoppingCart }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
|
||||
const addedShoppingCartItems =
|
||||
shoppingCart?.items
|
||||
?.filter(
|
||||
(item) =>
|
||||
!entity.shoppingCart?.items?.find((i) => i.id === item.id),
|
||||
)
|
||||
?.map((item) => item.data) ?? [];
|
||||
|
||||
entity.shoppingCart = shoppingCart;
|
||||
|
||||
entity.itemAvailabilityTimestamp = entity.itemAvailabilityTimestamp
|
||||
? { ...entity.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
for (let shoppingCartItem of addedShoppingCartItems) {
|
||||
if (shoppingCartItem.features?.orderType) {
|
||||
entity.itemAvailabilityTimestamp[
|
||||
`${shoppingCartItem.id}_${shoppingCartItem.features.orderType}`
|
||||
] = now;
|
||||
}
|
||||
}
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.setShoppingCartByShoppingCartId,
|
||||
(s, { shoppingCartId, shoppingCart }) => {
|
||||
let entity = getCheckoutEntityByShoppingCartId({
|
||||
shoppingCartId,
|
||||
entities: s.entities,
|
||||
});
|
||||
|
||||
if (!entity) {
|
||||
// No entity found for this shoppingCartId, cannot update
|
||||
return s;
|
||||
}
|
||||
|
||||
entity = { ...entity, shoppingCart };
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(DomainCheckoutActions.setCheckout, (s, { processId, checkout }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.checkout = checkout;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.setBuyerCommunicationDetails,
|
||||
(s, { processId, email, mobile }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
const communicationDetails = { ...entity.buyer.communicationDetails };
|
||||
communicationDetails.email = email || communicationDetails.email;
|
||||
communicationDetails.mobile = mobile || communicationDetails.mobile;
|
||||
entity.buyer = {
|
||||
...entity.buyer,
|
||||
communicationDetails,
|
||||
};
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.setNotificationChannels,
|
||||
(s, { processId, notificationChannels }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
return storeCheckoutAdapter.setOne(
|
||||
{ ...entity, notificationChannels },
|
||||
s,
|
||||
);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.setCheckoutDestination,
|
||||
(s, { processId, destination }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.checkout = {
|
||||
...entity.checkout,
|
||||
destinations: entity.checkout.destinations.map((dest) => {
|
||||
if (dest.id === destination.id) {
|
||||
return { ...dest, ...destination };
|
||||
}
|
||||
return { ...dest };
|
||||
}),
|
||||
};
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.setShippingAddress,
|
||||
(s, { processId, shippingAddress }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.shippingAddress = shippingAddress;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(DomainCheckoutActions.setBuyer, (s, { processId, buyer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.buyer = buyer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setPayer, (s, { processId, payer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.payer = payer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.setSpecialComment,
|
||||
(s, { processId, agentComment }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.specialComment = agentComment;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(DomainCheckoutActions.removeCheckoutWithProcessId, (s, { processId }) => {
|
||||
return storeCheckoutAdapter.removeOne(processId, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setOrders, (s, { orders }) => ({
|
||||
...s,
|
||||
orders: [...s.orders, ...orders],
|
||||
})),
|
||||
on(DomainCheckoutActions.updateOrderItem, (s, { item }) => {
|
||||
const orders = [...s.orders];
|
||||
|
||||
const orderToUpdate = orders?.find((order) =>
|
||||
order.items?.find((i) => i.id === item?.id),
|
||||
);
|
||||
const orderToUpdateIndex = orders?.indexOf(orderToUpdate);
|
||||
|
||||
const orderItemToUpdate = orderToUpdate?.items?.find(
|
||||
(i) => i.id === item?.id,
|
||||
);
|
||||
const orderItemToUpdateIndex =
|
||||
orderToUpdate?.items?.indexOf(orderItemToUpdate);
|
||||
|
||||
const items = [...(orderToUpdate?.items ?? [])];
|
||||
items[orderItemToUpdateIndex] = item;
|
||||
|
||||
orders[orderToUpdateIndex] = {
|
||||
...orderToUpdate,
|
||||
items: [...items],
|
||||
};
|
||||
|
||||
return { ...s, orders: [...orders] };
|
||||
}),
|
||||
on(DomainCheckoutActions.removeAllOrders, (s) => ({
|
||||
...s,
|
||||
orders: [],
|
||||
})),
|
||||
on(DomainCheckoutActions.setOlaError, (s, { processId, olaErrorIds }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.olaErrorIds = olaErrorIds;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(DomainCheckoutActions.setCustomer, (s, { processId, customer }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
entity.customer = customer;
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
}),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistory,
|
||||
(s, { processId, shoppingCartItemId, availability }) => {
|
||||
const entity = getOrCreateCheckoutEntity({
|
||||
processId,
|
||||
entities: s.entities,
|
||||
});
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
|
||||
? { ...entity?.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find(
|
||||
(i) => i.id === shoppingCartItemId,
|
||||
)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] =
|
||||
Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
on(
|
||||
DomainCheckoutActions.addShoppingCartItemAvailabilityToHistoryByShoppingCartId,
|
||||
(s, { shoppingCartId, shoppingCartItemId, availability }) => {
|
||||
const entity = getCheckoutEntityByShoppingCartId({
|
||||
shoppingCartId,
|
||||
entities: s.entities,
|
||||
});
|
||||
|
||||
const itemAvailabilityTimestamp = entity?.itemAvailabilityTimestamp
|
||||
? { ...entity?.itemAvailabilityTimestamp }
|
||||
: {};
|
||||
|
||||
const item = entity?.shoppingCart?.items?.find(
|
||||
(i) => i.id === shoppingCartItemId,
|
||||
)?.data;
|
||||
|
||||
if (!item?.features?.orderType) return s;
|
||||
|
||||
itemAvailabilityTimestamp[`${item.id}_${item?.features?.orderType}`] =
|
||||
Date.now();
|
||||
|
||||
entity.itemAvailabilityTimestamp = itemAvailabilityTimestamp;
|
||||
|
||||
return storeCheckoutAdapter.setOne(entity, s);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
export function domainCheckoutReducer(state, action) {
|
||||
return _domainCheckoutReducer(state, action);
|
||||
}
|
||||
|
||||
function getOrCreateCheckoutEntity({
|
||||
entities,
|
||||
processId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
processId: number;
|
||||
}): CheckoutEntity {
|
||||
let entity = entities[processId];
|
||||
|
||||
if (isNullOrUndefined(entity)) {
|
||||
return {
|
||||
processId,
|
||||
checkout: undefined,
|
||||
shoppingCart: undefined,
|
||||
shippingAddress: undefined,
|
||||
orders: [],
|
||||
payer: undefined,
|
||||
buyer: undefined,
|
||||
specialComment: '',
|
||||
notificationChannels: 0,
|
||||
olaErrorIds: [],
|
||||
customer: undefined,
|
||||
// availabilityHistory: [],
|
||||
itemAvailabilityTimestamp: {},
|
||||
};
|
||||
}
|
||||
|
||||
return { ...entity };
|
||||
}
|
||||
|
||||
function getCheckoutEntityByShoppingCartId({
|
||||
entities,
|
||||
shoppingCartId,
|
||||
}: {
|
||||
entities: Dictionary<CheckoutEntity>;
|
||||
shoppingCartId: number;
|
||||
}): CheckoutEntity {
|
||||
return Object.values(entities).find(
|
||||
(entity) => entity.shoppingCart?.id === shoppingCartId,
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,9 +20,15 @@ export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsCont
|
||||
|
||||
async printShippingNoteHelper(printer: string, receipts: ReceiptDTO[]) {
|
||||
try {
|
||||
for (const group of groupBy(receipts, (receipt) => receipt?.buyer?.buyerNumber)) {
|
||||
for (const group of groupBy(
|
||||
receipts,
|
||||
(receipt) => receipt?.buyer?.buyerNumber,
|
||||
)) {
|
||||
await this.domainPrinterService
|
||||
.printShippingNote({ printer, receipts: group?.items?.map((r) => r?.id) })
|
||||
.printShippingNote({
|
||||
printer,
|
||||
receipts: group?.items?.map((r) => r?.id),
|
||||
})
|
||||
.toPromise();
|
||||
}
|
||||
return {
|
||||
@@ -38,7 +44,9 @@ export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsCont
|
||||
}
|
||||
|
||||
async handler(data: OrderItemsContext): Promise<OrderItemsContext> {
|
||||
const printerList = await this.domainPrinterService.getAvailableLabelPrinters().toPromise();
|
||||
const printerList = await this.domainPrinterService
|
||||
.getAvailableLabelPrinters()
|
||||
.toPromise();
|
||||
const receipts = data?.receipts?.filter((r) => r?.receiptType & 1);
|
||||
let printer: Printer;
|
||||
|
||||
@@ -53,7 +61,8 @@ export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsCont
|
||||
data: {
|
||||
printImmediately: !this._environmentSerivce.matchTablet(),
|
||||
printerType: 'Label',
|
||||
print: async (printer) => await this.printShippingNoteHelper(printer, receipts),
|
||||
print: async (printer) =>
|
||||
await this.printShippingNoteHelper(printer, receipts),
|
||||
} as PrintModalData,
|
||||
})
|
||||
.afterClosed$.toPromise();
|
||||
|
||||
@@ -12,7 +12,13 @@ import {
|
||||
ShoppingCartItemDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { catchError, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
catchError,
|
||||
mergeMap,
|
||||
switchMap,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import {
|
||||
BranchService,
|
||||
DisplayOrderDTO,
|
||||
@@ -40,7 +46,10 @@ export interface KulturpassOrderModalState {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderModalState> implements OnStoreInit {
|
||||
export class KulturpassOrderModalStore
|
||||
extends ComponentStore<KulturpassOrderModalState>
|
||||
implements OnStoreInit
|
||||
{
|
||||
private _checkoutService = inject(DomainCheckoutService);
|
||||
private _branchService = inject(BranchService);
|
||||
private _authService = inject(AuthService);
|
||||
@@ -87,23 +96,33 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
|
||||
readonly order$ = this.select((state) => state.order);
|
||||
|
||||
readonly updateCheckout = this.updater((state, checkout: CheckoutDTO) => ({ ...state, checkout }));
|
||||
readonly updateCheckout = this.updater((state, checkout: CheckoutDTO) => ({
|
||||
...state,
|
||||
checkout,
|
||||
}));
|
||||
|
||||
readonly updateOrder = this.updater((state, order: OrderDTO) => ({ ...state, order }));
|
||||
readonly updateOrder = this.updater((state, order: OrderDTO) => ({
|
||||
...state,
|
||||
order,
|
||||
}));
|
||||
|
||||
readonly fetchShoppingCart$ = this.select((state) => state.fetchShoppingCart);
|
||||
|
||||
readonly updateFetchShoppingCart = this.updater((state, fetchShoppingCart: boolean) => ({
|
||||
...state,
|
||||
fetchShoppingCart,
|
||||
}));
|
||||
readonly updateFetchShoppingCart = this.updater(
|
||||
(state, fetchShoppingCart: boolean) => ({
|
||||
...state,
|
||||
fetchShoppingCart,
|
||||
}),
|
||||
);
|
||||
|
||||
readonly ordering$ = this.select((state) => state.ordering);
|
||||
|
||||
loadBranch = this.effect(($) =>
|
||||
$.pipe(
|
||||
switchMap(() =>
|
||||
this._branchService.BranchGetBranches({}).pipe(tapResponse(this.handleBranchResponse, this.handleBranchError)),
|
||||
this._branchService
|
||||
.BranchGetBranches({})
|
||||
.pipe(tapResponse(this.handleBranchResponse, this.handleBranchError)),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -111,31 +130,45 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
handleBranchResponse = (res: ResponseArgsOfIEnumerableOfBranchDTO) => {
|
||||
const branchNumber = this._authService.getClaimByKey('branch_no');
|
||||
|
||||
this.patchState({ branch: res.result.find((b) => b.branchNumber === branchNumber) });
|
||||
this.patchState({
|
||||
branch: res.result.find((b) => b.branchNumber === branchNumber),
|
||||
});
|
||||
};
|
||||
|
||||
handleBranchError = (err) => {
|
||||
this._modal.error('Fehler beim Laden der Filiale', err);
|
||||
};
|
||||
|
||||
createShoppingCart = this.effect((orderItemListItem$: Observable<OrderItemListItemDTO>) =>
|
||||
orderItemListItem$.pipe(
|
||||
tap((orderItemListItem) => {
|
||||
this.patchState({ orderItemListItem });
|
||||
this.updateFetchShoppingCart(true);
|
||||
}),
|
||||
switchMap((orderItemListItem) =>
|
||||
this._checkoutService
|
||||
.getShoppingCart({ processId: this.processId })
|
||||
.pipe(tapResponse(this.handleCreateShoppingCartResponse, this.handleCreateShoppingCartError)),
|
||||
createShoppingCart = this.effect(
|
||||
(orderItemListItem$: Observable<OrderItemListItemDTO>) =>
|
||||
orderItemListItem$.pipe(
|
||||
tap((orderItemListItem) => {
|
||||
this.patchState({ orderItemListItem });
|
||||
this.updateFetchShoppingCart(true);
|
||||
}),
|
||||
switchMap((orderItemListItem) =>
|
||||
this._checkoutService
|
||||
.getShoppingCart({ processId: this.processId })
|
||||
.pipe(
|
||||
tapResponse(
|
||||
this.handleCreateShoppingCartResponse,
|
||||
this.handleCreateShoppingCartError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
handleCreateShoppingCartResponse = (res: ShoppingCartDTO) => {
|
||||
this.patchState({ shoppingCart: res });
|
||||
this._checkoutService.setBuyer({ processId: this.processId, buyer: this.order.buyer });
|
||||
this._checkoutService.setPayer({ processId: this.processId, payer: this.order.billing?.data });
|
||||
this._checkoutService.setBuyer({
|
||||
processId: this.processId,
|
||||
buyer: this.order.buyer,
|
||||
});
|
||||
this._checkoutService.setPayer({
|
||||
processId: this.processId,
|
||||
payer: this.order.billing?.data,
|
||||
});
|
||||
|
||||
this.updateFetchShoppingCart(false);
|
||||
};
|
||||
@@ -154,7 +187,9 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
processId: this.processId,
|
||||
items: [add],
|
||||
})
|
||||
.pipe(tapResponse(this.handleAddItemResponse, this.handleAddItemError)),
|
||||
.pipe(
|
||||
tapResponse(this.handleAddItemResponse, this.handleAddItemError),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -174,7 +209,12 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
shoppingCartItemId: change.id,
|
||||
update: { quantity: change.quantity },
|
||||
})
|
||||
.pipe(tapResponse(this.handleQuantityChangeResponse, this.handleQuantityChangeError)),
|
||||
.pipe(
|
||||
tapResponse(
|
||||
this.handleQuantityChangeResponse,
|
||||
this.handleQuantityChangeError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -206,7 +246,10 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
this.onOrderSuccess(res.result.item1[0], res.result.item2);
|
||||
};
|
||||
|
||||
onOrderSuccess = (displayOrder: DisplayOrderDTO, action: KeyValueDTOOfStringAndString[]) => {};
|
||||
onOrderSuccess = (
|
||||
displayOrder: DisplayOrderDTO,
|
||||
action: KeyValueDTOOfStringAndString[],
|
||||
) => {};
|
||||
|
||||
handleOrderError = (err: any) => {
|
||||
this._modal.error('Fehler beim Bestellen', err);
|
||||
@@ -215,8 +258,9 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
|
||||
itemQuantityByCatalogProductNumber(catalogProductNumber: string) {
|
||||
return (
|
||||
this.shoppingCart?.items?.find((i) => getCatalogProductNumber(i?.data) === catalogProductNumber)?.data
|
||||
?.quantity ?? 0
|
||||
this.shoppingCart?.items?.find(
|
||||
(i) => getCatalogProductNumber(i?.data) === catalogProductNumber,
|
||||
)?.data?.quantity ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
@@ -227,7 +271,11 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
.canAddItemsKulturpass([item?.product])
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(results) => this.handleCanAddItemResponse({ item, result: results?.find((_) => true) }),
|
||||
(results) =>
|
||||
this.handleCanAddItemResponse({
|
||||
item,
|
||||
result: results?.find((_) => true),
|
||||
}),
|
||||
this.handleCanAddItemError,
|
||||
),
|
||||
),
|
||||
@@ -235,14 +283,23 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
),
|
||||
);
|
||||
|
||||
handleCanAddItemResponse = ({ item, result }: { item: ItemDTO; result: KulturPassResult }) => {
|
||||
handleCanAddItemResponse = ({
|
||||
item,
|
||||
result,
|
||||
}: {
|
||||
item: ItemDTO;
|
||||
result: KulturPassResult;
|
||||
}) => {
|
||||
if (result?.canAdd) {
|
||||
this.addItemToShoppingCart(item);
|
||||
} else {
|
||||
this._modal.open({
|
||||
content: UiMessageModalComponent,
|
||||
title: 'Artikel nicht förderfähig',
|
||||
data: { message: result?.message, closeAction: 'ohne Artikel fortfahren' },
|
||||
data: {
|
||||
message: result?.message,
|
||||
closeAction: 'ohne Artikel fortfahren',
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -254,14 +311,18 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
addItemToShoppingCart = this.effect((item$: Observable<ItemDTO>) =>
|
||||
item$.pipe(
|
||||
mergeMap((item) => {
|
||||
const takeAwayAvailability$ = this._availabilityService.getTakeAwayAvailability({
|
||||
item: {
|
||||
ean: item.product.ean,
|
||||
itemId: item.id,
|
||||
price: item.catalogAvailability.price,
|
||||
},
|
||||
quantity: this.itemQuantityByCatalogProductNumber(getCatalogProductNumber(item)) + 1,
|
||||
});
|
||||
const takeAwayAvailability$ =
|
||||
this._availabilityService.getTakeAwayAvailability({
|
||||
item: {
|
||||
ean: item.product.ean,
|
||||
itemId: item.id,
|
||||
price: item.catalogAvailability.price,
|
||||
},
|
||||
quantity:
|
||||
this.itemQuantityByCatalogProductNumber(
|
||||
getCatalogProductNumber(item),
|
||||
) + 1,
|
||||
});
|
||||
|
||||
const deliveryAvailability$ = this._availabilityService
|
||||
.getDeliveryAvailability({
|
||||
@@ -270,7 +331,10 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
itemId: item.id,
|
||||
price: item.catalogAvailability.price,
|
||||
},
|
||||
quantity: this.itemQuantityByCatalogProductNumber(getCatalogProductNumber(item)) + 1,
|
||||
quantity:
|
||||
this.itemQuantityByCatalogProductNumber(
|
||||
getCatalogProductNumber(item),
|
||||
) + 1,
|
||||
})
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
@@ -279,7 +343,10 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
);
|
||||
|
||||
return zip(takeAwayAvailability$, deliveryAvailability$).pipe(
|
||||
tapResponse(this.handleAddItemToShoppingCartResponse2(item), this.handleAddItemToShoppingCartError),
|
||||
tapResponse(
|
||||
this.handleAddItemToShoppingCartResponse2(item),
|
||||
this.handleAddItemToShoppingCartError,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
@@ -287,20 +354,27 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
|
||||
handleAddItemToShoppingCartResponse2 =
|
||||
(item: ItemDTO) =>
|
||||
([takeAwayAvailability, deliveryAvailability]: [AvailabilityDTO, AvailabilityDTO]) => {
|
||||
([takeAwayAvailability, deliveryAvailability]: [
|
||||
AvailabilityDTO,
|
||||
AvailabilityDTO,
|
||||
]) => {
|
||||
let isPriceMaintained = item?.catalogAvailability?.priceMaintained;
|
||||
let onlinePrice = -1;
|
||||
|
||||
if (deliveryAvailability) {
|
||||
isPriceMaintained = isPriceMaintained ?? deliveryAvailability['priceMaintained'] ?? false;
|
||||
isPriceMaintained =
|
||||
isPriceMaintained ?? deliveryAvailability['priceMaintained'] ?? false;
|
||||
onlinePrice = deliveryAvailability?.price?.value?.value ?? -1;
|
||||
}
|
||||
|
||||
// Preis und priceMaintained werden immer erst vom Katalog genommen. Bei nicht Verfügbarkeit greifen die anderen Availabilities
|
||||
const offlinePrice =
|
||||
item?.catalogAvailability?.price?.value?.value ?? takeAwayAvailability?.price?.value?.value ?? -1;
|
||||
item?.catalogAvailability?.price?.value?.value ??
|
||||
takeAwayAvailability?.price?.value?.value ??
|
||||
-1;
|
||||
const availability = takeAwayAvailability;
|
||||
availability.price = item?.catalogAvailability?.price ?? takeAwayAvailability?.price;
|
||||
availability.price =
|
||||
item?.catalogAvailability?.price ?? takeAwayAvailability?.price;
|
||||
|
||||
/**
|
||||
* Onlinepreis ist niedliger als der Offlinepreis
|
||||
@@ -314,7 +388,11 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
* wenn der Artikel Preisgebunden ist, wird der Ladenpreis verwendet
|
||||
*/
|
||||
|
||||
if (!!deliveryAvailability && onlinePrice < offlinePrice && !isPriceMaintained) {
|
||||
if (
|
||||
!!deliveryAvailability &&
|
||||
onlinePrice < offlinePrice &&
|
||||
!isPriceMaintained
|
||||
) {
|
||||
availability.price = deliveryAvailability.price;
|
||||
}
|
||||
|
||||
@@ -333,10 +411,13 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
},
|
||||
},
|
||||
promotion: {
|
||||
points: 0,
|
||||
value: 0,
|
||||
},
|
||||
itemType: item.type,
|
||||
product: { catalogProductNumber: getCatalogProductNumber(item), ...item.product },
|
||||
product: {
|
||||
catalogProductNumber: getCatalogProductNumber(item),
|
||||
...item.product,
|
||||
},
|
||||
};
|
||||
|
||||
this.addItem(addToShoppingCartDTO);
|
||||
@@ -346,15 +427,28 @@ export class KulturpassOrderModalStore extends ComponentStore<KulturpassOrderMod
|
||||
this._modal.error('Fehler beim Hinzufügen des Artikels', err);
|
||||
};
|
||||
|
||||
setAvailability = this.updater((state, data: { catalogProductNumber: string; availability: AvailabilityDTO }) => {
|
||||
return { ...state, availabilities: { ...state.availabilities, [data.catalogProductNumber]: data.availability } };
|
||||
});
|
||||
setAvailability = this.updater(
|
||||
(
|
||||
state,
|
||||
data: { catalogProductNumber: string; availability: AvailabilityDTO },
|
||||
) => {
|
||||
return {
|
||||
...state,
|
||||
availabilities: {
|
||||
...state.availabilities,
|
||||
[data.catalogProductNumber]: data.availability,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
getAvailability(catalogProductNumber: string): AvailabilityDTO | undefined {
|
||||
return this.get((state) => state.availabilities[catalogProductNumber]);
|
||||
}
|
||||
|
||||
getAvailability$(catalogProductNumber: string): Observable<AvailabilityDTO | undefined> {
|
||||
getAvailability$(
|
||||
catalogProductNumber: string,
|
||||
): Observable<AvailabilityDTO | undefined> {
|
||||
return this.select((state) => state.availabilities[catalogProductNumber]);
|
||||
}
|
||||
}
|
||||
|
||||
313
apps/isa-app/src/modal/purchase-options/README.md
Normal file
313
apps/isa-app/src/modal/purchase-options/README.md
Normal file
@@ -0,0 +1,313 @@
|
||||
# Purchase Options Modal
|
||||
|
||||
The Purchase Options Modal allows users to select how they want to receive their items (delivery, pickup, in-store, download) during the checkout process.
|
||||
|
||||
## Overview
|
||||
|
||||
This modal handles the complete purchase option selection flow including:
|
||||
- Fetching availability for each purchase option
|
||||
- Validating if items can be added to the shopping cart
|
||||
- Managing item quantities and prices
|
||||
- Supporting reward redemption flows
|
||||
|
||||
## Features
|
||||
|
||||
### Core Functionality
|
||||
- **Multiple Purchase Options**: Delivery, B2B Delivery, Digital Delivery, Pickup, In-Store, Download
|
||||
- **Availability Checking**: Real-time availability checks for each option
|
||||
- **Branch Selection**: Pick branches for pickup and in-store options
|
||||
- **Price Management**: Handle pricing, VAT, and manual price adjustments
|
||||
- **Reward Redemption**: Support for redeeming loyalty points
|
||||
|
||||
### Advanced Features
|
||||
- **Disabled Purchase Options**: Prevent specific options from being available (skips API calls)
|
||||
- **Hide Disabled Options**: Toggle visibility of disabled options (show grayed out or hide completely)
|
||||
- **Pre-selection**: Pre-select a specific purchase option on open
|
||||
- **Single Option Mode**: Show only one specific purchase option
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { PurchaseOptionsModalService } from '@modal/purchase-options';
|
||||
|
||||
constructor(private purchaseOptionsModal: PurchaseOptionsModalService) {}
|
||||
|
||||
async openModal() {
|
||||
const modalRef = await this.purchaseOptionsModal.open({
|
||||
tabId: 123,
|
||||
shoppingCartId: 456,
|
||||
type: 'add', // or 'update'
|
||||
items: [/* array of items */],
|
||||
});
|
||||
|
||||
const result = await firstValueFrom(modalRef.afterClosed$);
|
||||
}
|
||||
```
|
||||
|
||||
### Disabling Purchase Options
|
||||
|
||||
Prevent specific options from being available. The modal will **not make API calls** for disabled options.
|
||||
|
||||
```typescript
|
||||
const modalRef = await this.purchaseOptionsModal.open({
|
||||
tabId: 123,
|
||||
shoppingCartId: 456,
|
||||
type: 'add',
|
||||
items: [item1, item2],
|
||||
disabledPurchaseOptions: ['b2b-delivery'], // Disable B2B delivery
|
||||
});
|
||||
```
|
||||
|
||||
### Hide vs Show Disabled Options
|
||||
|
||||
Control whether disabled options are hidden or shown with a disabled visual state:
|
||||
|
||||
**Hide Disabled Options (default)**
|
||||
```typescript
|
||||
const modalRef = await this.purchaseOptionsModal.open({
|
||||
tabId: 123,
|
||||
shoppingCartId: 456,
|
||||
type: 'add',
|
||||
items: [item],
|
||||
disabledPurchaseOptions: ['b2b-delivery'],
|
||||
hideDisabledPurchaseOptions: true, // Default - option not visible
|
||||
});
|
||||
```
|
||||
|
||||
**Show Disabled Options (grayed out)**
|
||||
```typescript
|
||||
const modalRef = await this.purchaseOptionsModal.open({
|
||||
tabId: 123,
|
||||
shoppingCartId: 456,
|
||||
type: 'add',
|
||||
items: [item],
|
||||
disabledPurchaseOptions: ['b2b-delivery'],
|
||||
hideDisabledPurchaseOptions: false, // Show disabled with visual indicator
|
||||
});
|
||||
```
|
||||
|
||||
### Pre-selecting an Option
|
||||
|
||||
```typescript
|
||||
const modalRef = await this.purchaseOptionsModal.open({
|
||||
tabId: 123,
|
||||
shoppingCartId: 456,
|
||||
type: 'add',
|
||||
items: [item],
|
||||
preSelectOption: {
|
||||
option: 'in-store',
|
||||
showOptionOnly: false, // Optional: show only this option
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Reward Redemption Flow
|
||||
|
||||
```typescript
|
||||
const modalRef = await this.purchaseOptionsModal.open({
|
||||
tabId: 123,
|
||||
shoppingCartId: 456,
|
||||
type: 'add',
|
||||
items: [rewardItem],
|
||||
useRedemptionPoints: true,
|
||||
preSelectOption: { option: 'in-store' },
|
||||
disabledPurchaseOptions: ['b2b-delivery'], // Common for rewards
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### PurchaseOptionsModalData
|
||||
|
||||
```typescript
|
||||
interface PurchaseOptionsModalData {
|
||||
/** Tab ID for context */
|
||||
tabId: number;
|
||||
|
||||
/** Shopping cart ID to add/update items */
|
||||
shoppingCartId: number;
|
||||
|
||||
/** Action type: 'add' = new items, 'update' = existing items */
|
||||
type: 'add' | 'update';
|
||||
|
||||
/** Enable redemption points mode */
|
||||
useRedemptionPoints?: boolean;
|
||||
|
||||
/** Items to show in the modal */
|
||||
items: Array<ItemDTO | ShoppingCartItemDTO>;
|
||||
|
||||
/** Pre-configured pickup branch */
|
||||
pickupBranch?: BranchDTO;
|
||||
|
||||
/** Pre-configured in-store branch */
|
||||
inStoreBranch?: BranchDTO;
|
||||
|
||||
/** Pre-select a specific purchase option */
|
||||
preSelectOption?: {
|
||||
option: PurchaseOption;
|
||||
showOptionOnly?: boolean;
|
||||
};
|
||||
|
||||
/** Purchase options to disable (no API calls) */
|
||||
disabledPurchaseOptions?: PurchaseOption[];
|
||||
|
||||
/** Hide disabled options (true) or show as grayed out (false). Default: true */
|
||||
hideDisabledPurchaseOptions?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
### PurchaseOption Type
|
||||
|
||||
```typescript
|
||||
type PurchaseOption =
|
||||
| 'delivery' // Standard delivery
|
||||
| 'dig-delivery' // Digital delivery
|
||||
| 'b2b-delivery' // B2B delivery
|
||||
| 'pickup' // Pickup at branch
|
||||
| 'in-store' // Reserve in store
|
||||
| 'download' // Digital download
|
||||
| 'catalog'; // Catalog availability
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component Structure
|
||||
|
||||
```
|
||||
purchase-options/
|
||||
├── purchase-options-modal.component.ts # Main modal component
|
||||
├── purchase-options-modal.service.ts # Service to open modal
|
||||
├── purchase-options-modal.data.ts # Data interfaces
|
||||
├── store/
|
||||
│ ├── purchase-options.store.ts # NgRx ComponentStore
|
||||
│ ├── purchase-options.service.ts # Business logic service
|
||||
│ ├── purchase-options.state.ts # State interface
|
||||
│ ├── purchase-options.types.ts # Type definitions
|
||||
│ └── purchase-options.selectors.ts # State selectors
|
||||
├── purchase-options-tile/
|
||||
│ ├── base-purchase-option.directive.ts # Base directive for tiles
|
||||
│ ├── delivery-purchase-options-tile.component.ts
|
||||
│ ├── pickup-purchase-options-tile.component.ts
|
||||
│ ├── in-store-purchase-options-tile.component.ts
|
||||
│ └── download-purchase-options-tile.component.ts
|
||||
└── purchase-options-list-item/ # Item list components
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
The modal uses NgRx ComponentStore for state management:
|
||||
- **Availability Loading**: Parallel API calls for each enabled option
|
||||
- **Can Add Validation**: Check if items can be added to cart
|
||||
- **Item Selection**: Track selected items for batch operations
|
||||
- **Branch Management**: Handle branch selection for pickup/in-store
|
||||
|
||||
### Disabled Options Flow
|
||||
|
||||
1. **Configuration**: `disabledPurchaseOptions` array passed to modal
|
||||
2. **State**: Stored in store via `initialize()` method
|
||||
3. **Availability Loading**: `isOptionDisabled()` check skips API calls
|
||||
4. **UI Rendering**:
|
||||
- If `hideDisabledPurchaseOptions: true` → not rendered
|
||||
- If `hideDisabledPurchaseOptions: false` → rendered with `.disabled` class
|
||||
5. **Click Prevention**: Disabled tiles prevent click action
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
### 1. Regular Checkout
|
||||
```typescript
|
||||
// Show all options
|
||||
await modalService.open({
|
||||
tabId: processId,
|
||||
shoppingCartId: cartId,
|
||||
type: 'add',
|
||||
items: catalogItems,
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Reward Redemption
|
||||
```typescript
|
||||
// Pre-select in-store, disable B2B
|
||||
await modalService.open({
|
||||
tabId: processId,
|
||||
shoppingCartId: rewardCartId,
|
||||
type: 'add',
|
||||
items: rewardItems,
|
||||
useRedemptionPoints: true,
|
||||
preSelectOption: { option: 'in-store' },
|
||||
disabledPurchaseOptions: ['b2b-delivery'],
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Update Existing Item
|
||||
```typescript
|
||||
// Allow user to change delivery option
|
||||
await modalService.open({
|
||||
tabId: processId,
|
||||
shoppingCartId: cartId,
|
||||
type: 'update',
|
||||
items: [existingCartItem],
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Gift Cards (In-Store Only)
|
||||
```typescript
|
||||
// Show only in-store and delivery
|
||||
await modalService.open({
|
||||
tabId: processId,
|
||||
shoppingCartId: cartId,
|
||||
type: 'add',
|
||||
items: [giftCardItem],
|
||||
disabledPurchaseOptions: ['pickup', 'b2b-delivery'],
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# Run all tests for the app
|
||||
npx nx test isa-app --skip-nx-cache
|
||||
```
|
||||
|
||||
### Testing Disabled Options
|
||||
When testing the disabled options feature:
|
||||
1. Verify no API calls are made for disabled options
|
||||
2. Check UI rendering based on `hideDisabledPurchaseOptions` flag
|
||||
3. Ensure click events are prevented on disabled tiles
|
||||
4. Validate backward compatibility (defaults work correctly)
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### From `hidePurchaseOptions` to `disabledPurchaseOptions`
|
||||
The field was renamed for clarity:
|
||||
- **Old**: `hidePurchaseOptions` (ambiguous - hides from UI)
|
||||
- **New**: `disabledPurchaseOptions` (clear - disabled functionality, may or may not be hidden)
|
||||
|
||||
This is a **breaking change** if you were using `hidePurchaseOptions`. Update all usages:
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
hidePurchaseOptions: ['b2b-delivery']
|
||||
|
||||
// After
|
||||
disabledPurchaseOptions: ['b2b-delivery']
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Disabled option still making API calls
|
||||
**Solution**: Ensure the option is in the `disabledPurchaseOptions` array and spelled correctly.
|
||||
|
||||
### Problem: Disabled option not showing even with `hideDisabledPurchaseOptions: false`
|
||||
**Solution**: Check that `showOption()` logic and availability checks are working correctly.
|
||||
|
||||
### Problem: All options disabled
|
||||
**Solution**: Don't disable all options. At least one option must be available for users to proceed.
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [Checkout Data Access](../../../libs/checkout/data-access/README.md)
|
||||
- [Shopping Cart Flow](../../page/checkout/README.md)
|
||||
- [Reward System](../../../libs/checkout/feature/reward-catalog/README.md)
|
||||
@@ -1,33 +1,48 @@
|
||||
import { ItemType, PriceDTO, PriceValueDTO, VATValueDTO } from '@generated/swagger/checkout-api';
|
||||
import { OrderType, PurchaseOption } from './store';
|
||||
|
||||
export const PURCHASE_OPTIONS: PurchaseOption[] = [
|
||||
'in-store',
|
||||
'pickup',
|
||||
'delivery',
|
||||
'dig-delivery',
|
||||
'b2b-delivery',
|
||||
'download',
|
||||
];
|
||||
|
||||
export const DELIVERY_PURCHASE_OPTIONS: PurchaseOption[] = ['delivery', 'dig-delivery', 'b2b-delivery'];
|
||||
|
||||
export const PURCHASE_OPTION_TO_ORDER_TYPE: { [purchaseOption: string]: OrderType } = {
|
||||
'in-store': 'Rücklage',
|
||||
pickup: 'Abholung',
|
||||
delivery: 'Versand',
|
||||
'dig-delivery': 'Versand',
|
||||
'b2b-delivery': 'Versand',
|
||||
};
|
||||
|
||||
export const GIFT_CARD_TYPE = 66560 as ItemType;
|
||||
|
||||
export const DEFAULT_PRICE_DTO: PriceDTO = { value: { value: undefined }, vat: { vatType: 0 } };
|
||||
|
||||
export const DEFAULT_PRICE_VALUE: PriceValueDTO = { value: 0, currency: 'EUR' };
|
||||
|
||||
export const DEFAULT_VAT_VALUE: VATValueDTO = { value: 0 };
|
||||
|
||||
export const GIFT_CARD_MAX_PRICE = 200;
|
||||
|
||||
export const PRICE_PATTERN = /^\d+(,\d{1,2})?$/;
|
||||
import {
|
||||
ItemType,
|
||||
PriceDTO,
|
||||
PriceValueDTO,
|
||||
VATValueDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
import { PurchaseOption } from './store';
|
||||
import { OrderTypeFeature } from '@isa/checkout/data-access';
|
||||
|
||||
export const PURCHASE_OPTIONS: PurchaseOption[] = [
|
||||
'in-store',
|
||||
'pickup',
|
||||
'delivery',
|
||||
'dig-delivery',
|
||||
'b2b-delivery',
|
||||
'download',
|
||||
];
|
||||
|
||||
export const DELIVERY_PURCHASE_OPTIONS: PurchaseOption[] = [
|
||||
'delivery',
|
||||
'dig-delivery',
|
||||
'b2b-delivery',
|
||||
];
|
||||
|
||||
export const PURCHASE_OPTION_TO_ORDER_TYPE: {
|
||||
[purchaseOption: string]: OrderTypeFeature;
|
||||
} = {
|
||||
'in-store': 'Rücklage',
|
||||
'pickup': 'Abholung',
|
||||
'delivery': 'Versand',
|
||||
'dig-delivery': 'Versand',
|
||||
'b2b-delivery': 'Versand',
|
||||
};
|
||||
|
||||
export const GIFT_CARD_TYPE = 66560 as ItemType;
|
||||
|
||||
export const DEFAULT_PRICE_DTO: PriceDTO = {
|
||||
value: { value: undefined },
|
||||
vat: { vatType: 0 },
|
||||
};
|
||||
|
||||
export const DEFAULT_PRICE_VALUE: PriceValueDTO = { value: 0, currency: 'EUR' };
|
||||
|
||||
export const DEFAULT_VAT_VALUE: VATValueDTO = { value: 0 };
|
||||
|
||||
export const GIFT_CARD_MAX_PRICE = 200;
|
||||
|
||||
export const PRICE_PATTERN = /^\d+(,\d{1,2})?$/;
|
||||
|
||||
@@ -1,185 +1,276 @@
|
||||
<div class="flex flex-row">
|
||||
<div class="shared-purchase-options-list-item__thumbnail w-16 max-h-28">
|
||||
<img class="rounded shadow-card max-w-full max-h-full" [src]="product?.ean | productImage" [alt]="product?.name" />
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__product grow ml-4">
|
||||
<div class="shared-purchase-options-list-item__contributors font-bold">
|
||||
{{ product?.contributors }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__name font-bold h-12" sharedScaleContent>
|
||||
{{ product?.name }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__format flex flex-row items-center">
|
||||
<shared-icon [icon]="product?.format"></shared-icon>
|
||||
<span class="ml-2 font-bold">{{ product?.formatDetail }}</span>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__manufacturer-and-ean">
|
||||
{{ product?.manufacturer }}
|
||||
@if (product?.manufacturer && product?.ean) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ product?.ean }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__volume-and-publication-date">
|
||||
{{ product?.volume }}
|
||||
@if (product?.volume && product?.publicationDate) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ product?.publicationDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__availabilities mt-3 grid grid-flow-row gap-2 justify-start">
|
||||
@if ((availabilities$ | async)?.length) {
|
||||
<div class="whitespace-nowrap self-center">Verfügbar als</div>
|
||||
}
|
||||
@for (availability of availabilities$ | async; track availability) {
|
||||
<div class="grid grid-flow-col gap-4 justify-start">
|
||||
<div
|
||||
class="shared-purchase-options-list-item__availability grid grid-flow-col gap-2 items-center"
|
||||
[attr.data-option]="availability.purchaseOption"
|
||||
>
|
||||
@switch (availability.purchaseOption) {
|
||||
@case ('delivery') {
|
||||
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
|
||||
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
|
||||
-
|
||||
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
|
||||
}
|
||||
@case ('dig-delivery') {
|
||||
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
|
||||
{{ availability.data.estimatedDelivery?.start | date: 'EE dd.MM.' }}
|
||||
-
|
||||
{{ availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.' }}
|
||||
}
|
||||
@case ('b2b-delivery') {
|
||||
<shared-icon icon="isa-b2b-truck" [size]="24"></shared-icon>
|
||||
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
}
|
||||
@case ('pickup') {
|
||||
<shared-icon
|
||||
class="cursor-pointer"
|
||||
#uiOverlayTrigger="uiOverlayTrigger"
|
||||
[uiOverlayTrigger]="orderDeadlineTooltip"
|
||||
[class.tooltip-active]="uiOverlayTrigger.opened"
|
||||
icon="isa-box-out"
|
||||
[size]="18"
|
||||
></shared-icon>
|
||||
{{ availability.data.estimatedShippingDate | date: 'dd. MMMM yyyy' }}
|
||||
<ui-tooltip
|
||||
#orderDeadlineTooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-12"
|
||||
[xOffset]="4"
|
||||
[warning]="true"
|
||||
[closeable]="true"
|
||||
>
|
||||
<b>{{ availability.data?.orderDeadline | orderDeadline }}</b>
|
||||
</ui-tooltip>
|
||||
}
|
||||
@case ('in-store') {
|
||||
<shared-icon icon="isa-shopping-bag" [size]="18"></shared-icon>
|
||||
{{ availability.data.inStock }}x
|
||||
@if (isEVT) {
|
||||
ab {{ isEVT | date: 'dd. MMMM yyyy' }}
|
||||
} @else {
|
||||
ab sofort
|
||||
}
|
||||
}
|
||||
@case ('download') {
|
||||
<shared-icon icon="isa-download" [size]="22"></shared-icon>
|
||||
Download
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__price text-right ml-4 flex flex-col items-end">
|
||||
<div class="shared-purchase-options-list-item__price-value font-bold text-xl flex flex-row items-center">
|
||||
<div class="relative flex flex-row justify-end items-start">
|
||||
@if (canEditVat$ | async) {
|
||||
<ui-select
|
||||
class="w-[6.5rem] min-h-[3.4375rem] p-4 rounded-card border border-solid border-[#AEB7C1] mr-4"
|
||||
tabindex="-1"
|
||||
[formControl]="manualVatFormControl"
|
||||
[defaultLabel]="'MwSt'"
|
||||
>
|
||||
@for (vat of vats$ | async; track vat) {
|
||||
<ui-select-option [label]="vat.name + '%'" [value]="vat.vatType"></ui-select-option>
|
||||
}
|
||||
</ui-select>
|
||||
}
|
||||
@if (canEditPrice$ | async) {
|
||||
<shared-input-control
|
||||
[class.ml-6]="priceFormControl?.invalid && priceFormControl?.dirty"
|
||||
>
|
||||
<shared-input-control-indicator>
|
||||
@if (priceFormControl?.invalid && priceFormControl?.dirty) {
|
||||
<shared-icon icon="mat-info"></shared-icon>
|
||||
}
|
||||
</shared-input-control-indicator>
|
||||
<input
|
||||
[uiOverlayTrigger]="giftCardTooltip"
|
||||
triggerOn="none"
|
||||
#quantityInput
|
||||
#priceOverlayTrigger="uiOverlayTrigger"
|
||||
sharedInputControlInput
|
||||
type="string"
|
||||
class="w-24"
|
||||
[formControl]="priceFormControl"
|
||||
placeholder="00,00"
|
||||
(sharedOnInit)="onPriceInputInit(quantityInput, priceOverlayTrigger)"
|
||||
sharedNumberValue
|
||||
/>
|
||||
<shared-input-control-suffix>EUR</shared-input-control-suffix>
|
||||
<shared-input-control-error error="required">Preis ist ungültig</shared-input-control-error>
|
||||
<shared-input-control-error error="pattern">Preis ist ungültig</shared-input-control-error>
|
||||
<shared-input-control-error error="min">Preis ist ungültig</shared-input-control-error>
|
||||
<shared-input-control-error error="max">Preis ist ungültig</shared-input-control-error>
|
||||
</shared-input-control>
|
||||
} @else {
|
||||
{{ priceValue$ | async | currency: 'EUR' : 'code' }}
|
||||
}
|
||||
|
||||
<ui-tooltip [warning]="true" xPosition="after" yPosition="below" [xOffset]="-55" [yOffset]="18" [closeable]="true" #giftCardTooltip>
|
||||
Tragen Sie hier den
|
||||
<br />
|
||||
Gutscheinbetrag ein.
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<ui-quantity-dropdown class="mt-2" [formControl]="quantityFormControl" [range]="maxSelectableQuantity$ | async"></ui-quantity-dropdown>
|
||||
<div class="pt-7">
|
||||
@if ((canAddResult$ | async)?.canAdd) {
|
||||
<input
|
||||
class="fancy-checkbox"
|
||||
[class.checked]="selectedFormControl?.value"
|
||||
[formControl]="selectedFormControl"
|
||||
type="checkbox"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (canAddResult$ | async; as canAddResult) {
|
||||
@if (!canAddResult.canAdd) {
|
||||
<span class="inline-block font-bold text-[#BE8100] mt-[14px] max-w-[19rem]">
|
||||
{{ canAddResult.message }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@if (showMaxAvailableQuantity$ | async) {
|
||||
<span class="font-bold text-[#BE8100] mt-[14px]">
|
||||
{{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar
|
||||
</span>
|
||||
}
|
||||
@if (showNotAvailable$ | async) {
|
||||
<span class="font-bold text-[#BE8100] mt-[14px]">Derzeit nicht bestellbar</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-16"></div>
|
||||
<div class="grow shared-purchase-options-list-item__availabilities"></div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="shared-purchase-options-list-item__thumbnail w-16 max-h-28">
|
||||
<img
|
||||
class="rounded shadow-card max-w-full max-h-full"
|
||||
[src]="product?.ean | productImage"
|
||||
[alt]="product?.name"
|
||||
/>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__product grow ml-4">
|
||||
<div class="shared-purchase-options-list-item__contributors font-bold">
|
||||
{{ product?.contributors }}
|
||||
</div>
|
||||
<div
|
||||
class="shared-purchase-options-list-item__name font-bold h-12"
|
||||
sharedScaleContent
|
||||
>
|
||||
{{ product?.name }}
|
||||
</div>
|
||||
<div
|
||||
class="shared-purchase-options-list-item__format flex flex-row items-center"
|
||||
>
|
||||
<shared-icon [icon]="product?.format"></shared-icon>
|
||||
<span class="ml-2 font-bold">{{ product?.formatDetail }}</span>
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__manufacturer-and-ean">
|
||||
{{ product?.manufacturer }}
|
||||
@if (product?.manufacturer && product?.ean) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ product?.ean }}
|
||||
</div>
|
||||
<div class="shared-purchase-options-list-item__volume-and-publication-date">
|
||||
{{ product?.volume }}
|
||||
@if (product?.volume && product?.publicationDate) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ product?.publicationDate | date: 'dd. MMMM yyyy' }}
|
||||
</div>
|
||||
<div
|
||||
class="shared-purchase-options-list-item__availabilities mt-3 grid grid-flow-row gap-2 justify-start"
|
||||
>
|
||||
@if ((availabilities$ | async)?.length) {
|
||||
<div class="whitespace-nowrap self-center">Verfügbar als</div>
|
||||
}
|
||||
@for (availability of availabilities$ | async; track availability) {
|
||||
<div class="grid grid-flow-col gap-4 justify-start">
|
||||
<div
|
||||
class="shared-purchase-options-list-item__availability grid grid-flow-col gap-2 items-center"
|
||||
[attr.data-option]="availability.purchaseOption"
|
||||
>
|
||||
@switch (availability.purchaseOption) {
|
||||
@case ('delivery') {
|
||||
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
|
||||
{{
|
||||
availability.data.estimatedDelivery?.start | date: 'EE dd.MM.'
|
||||
}}
|
||||
-
|
||||
{{
|
||||
availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.'
|
||||
}}
|
||||
}
|
||||
@case ('dig-delivery') {
|
||||
<shared-icon icon="isa-truck" [size]="22"></shared-icon>
|
||||
{{
|
||||
availability.data.estimatedDelivery?.start | date: 'EE dd.MM.'
|
||||
}}
|
||||
-
|
||||
{{
|
||||
availability.data.estimatedDelivery?.stop | date: 'EE dd.MM.'
|
||||
}}
|
||||
}
|
||||
@case ('b2b-delivery') {
|
||||
<shared-icon icon="isa-b2b-truck" [size]="24"></shared-icon>
|
||||
{{
|
||||
availability.data.estimatedShippingDate
|
||||
| date: 'dd. MMMM yyyy'
|
||||
}}
|
||||
}
|
||||
@case ('pickup') {
|
||||
<shared-icon
|
||||
class="cursor-pointer"
|
||||
#uiOverlayTrigger="uiOverlayTrigger"
|
||||
[uiOverlayTrigger]="orderDeadlineTooltip"
|
||||
[class.tooltip-active]="uiOverlayTrigger.opened"
|
||||
icon="isa-box-out"
|
||||
[size]="18"
|
||||
></shared-icon>
|
||||
{{
|
||||
availability.data.estimatedShippingDate
|
||||
| date: 'dd. MMMM yyyy'
|
||||
}}
|
||||
<ui-tooltip
|
||||
#orderDeadlineTooltip
|
||||
yPosition="above"
|
||||
xPosition="after"
|
||||
[yOffset]="-12"
|
||||
[xOffset]="4"
|
||||
[warning]="true"
|
||||
[closeable]="true"
|
||||
>
|
||||
<b>{{ availability.data?.orderDeadline | orderDeadline }}</b>
|
||||
</ui-tooltip>
|
||||
}
|
||||
@case ('in-store') {
|
||||
<shared-icon icon="isa-shopping-bag" [size]="18"></shared-icon>
|
||||
{{ availability.data.inStock }}x
|
||||
@if (isEVT) {
|
||||
ab {{ isEVT | date: 'dd. MMMM yyyy' }}
|
||||
} @else {
|
||||
ab sofort
|
||||
}
|
||||
}
|
||||
@case ('download') {
|
||||
<shared-icon icon="isa-download" [size]="22"></shared-icon>
|
||||
Download
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="shared-purchase-options-list-item__price text-right ml-4 flex flex-col items-end"
|
||||
>
|
||||
<div
|
||||
class="shared-purchase-options-list-item__price-value font-bold text-xl flex flex-row items-center"
|
||||
>
|
||||
<div class="relative flex flex-row justify-end items-start">
|
||||
@if (showRedemptionPoints()) {
|
||||
<span class="isa-text-body-2-regular text-isa-neutral-600"
|
||||
>Einlösen für:</span
|
||||
>
|
||||
<span class="ml-2 isa-text-body-2-bold text-isa-secondary-900"
|
||||
>{{
|
||||
redemptionPoints() * quantityFormControl.value
|
||||
}}
|
||||
Lesepunkte</span
|
||||
>
|
||||
} @else {
|
||||
@if (canEditVat$ | async) {
|
||||
<ui-select
|
||||
class="w-[6.5rem] min-h-[3.4375rem] p-4 rounded-card border border-solid border-[#AEB7C1] mr-4"
|
||||
tabindex="-1"
|
||||
[formControl]="manualVatFormControl"
|
||||
[defaultLabel]="'MwSt'"
|
||||
>
|
||||
@for (vat of vats$ | async; track vat) {
|
||||
<ui-select-option
|
||||
[label]="vat.name + '%'"
|
||||
[value]="vat.vatType"
|
||||
></ui-select-option>
|
||||
}
|
||||
</ui-select>
|
||||
}
|
||||
@if (canEditPrice$ | async) {
|
||||
<shared-input-control
|
||||
[class.ml-6]="
|
||||
priceFormControl?.invalid && priceFormControl?.dirty
|
||||
"
|
||||
>
|
||||
<shared-input-control-indicator>
|
||||
@if (priceFormControl?.invalid && priceFormControl?.dirty) {
|
||||
<shared-icon icon="mat-info"></shared-icon>
|
||||
}
|
||||
</shared-input-control-indicator>
|
||||
<input
|
||||
[uiOverlayTrigger]="giftCardTooltip"
|
||||
triggerOn="none"
|
||||
#quantityInput
|
||||
#priceOverlayTrigger="uiOverlayTrigger"
|
||||
sharedInputControlInput
|
||||
type="string"
|
||||
class="w-24"
|
||||
[formControl]="priceFormControl"
|
||||
placeholder="00,00"
|
||||
(sharedOnInit)="
|
||||
onPriceInputInit(quantityInput, priceOverlayTrigger)
|
||||
"
|
||||
sharedNumberValue
|
||||
/>
|
||||
<shared-input-control-suffix>EUR</shared-input-control-suffix>
|
||||
<shared-input-control-error error="required"
|
||||
>Preis ist ungültig</shared-input-control-error
|
||||
>
|
||||
<shared-input-control-error error="pattern"
|
||||
>Preis ist ungültig</shared-input-control-error
|
||||
>
|
||||
<shared-input-control-error error="min"
|
||||
>Preis ist ungültig</shared-input-control-error
|
||||
>
|
||||
<shared-input-control-error error="max"
|
||||
>Preis ist ungültig</shared-input-control-error
|
||||
>
|
||||
</shared-input-control>
|
||||
} @else {
|
||||
{{ priceValue$ | async | currency: 'EUR' : 'code' }}
|
||||
}
|
||||
}
|
||||
<ui-tooltip
|
||||
[warning]="true"
|
||||
xPosition="after"
|
||||
yPosition="below"
|
||||
[xOffset]="-55"
|
||||
[yOffset]="18"
|
||||
[closeable]="true"
|
||||
#giftCardTooltip
|
||||
>
|
||||
Tragen Sie hier den
|
||||
<br />
|
||||
Gutscheinbetrag ein.
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<ui-quantity-dropdown
|
||||
class="mt-2"
|
||||
[formControl]="quantityFormControl"
|
||||
[range]="maxSelectableQuantity$ | async"
|
||||
data-what="purchase-option-quantity"
|
||||
[attr.data-which]="product?.ean"
|
||||
></ui-quantity-dropdown>
|
||||
<div class="pt-7">
|
||||
@if ((canAddResult$ | async)?.canAdd) {
|
||||
<input
|
||||
class="fancy-checkbox"
|
||||
[class.checked]="selectedFormControl?.value"
|
||||
[formControl]="selectedFormControl"
|
||||
type="checkbox"
|
||||
data-what="purchase-option-selector"
|
||||
[attr.data-which]="product?.ean"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (canAddResult$ | async; as canAddResult) {
|
||||
@if (!canAddResult.canAdd) {
|
||||
<span
|
||||
class="inline-block font-bold text-[#BE8100] mt-[14px] max-w-[19rem]"
|
||||
>
|
||||
{{ canAddResult.message }}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
@if (showNoDownloadAvailability$ | async) {
|
||||
<span
|
||||
class="inline-block font-bold text-[#BE8100] mt-[14px] max-w-[19rem]"
|
||||
>
|
||||
Derzeit nicht verfügbar
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (showMaxAvailableQuantity$ | async) {
|
||||
<span class="font-bold text-[#BE8100] mt-[14px]">
|
||||
{{ (availability$ | async)?.inStock }} Exemplare sofort lieferbar
|
||||
</span>
|
||||
}
|
||||
@if (showNotAvailable$ | async) {
|
||||
<span class="font-bold text-[#BE8100] mt-[14px]"
|
||||
>Derzeit nicht bestellbar</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="w-16"></div>
|
||||
<div class="grow shared-purchase-options-list-item__availabilities"></div>
|
||||
</div>
|
||||
@if (showLowStockMessage()) {
|
||||
<div
|
||||
class="text-isa-accent-red isa-text-body-2-bold mt-6 flex flex-row items-center gap-2"
|
||||
>
|
||||
<ng-icon name="isaOtherInfo" size="1.5rem"></ng-icon>
|
||||
<div>Geringer Bestand - Artikel holen vor Abschluss</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,349 +1,513 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, Input, OnInit, OnDestroy, OnChanges, SimpleChanges } from '@angular/core';
|
||||
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { InputControlModule } from '@shared/components/input-control';
|
||||
import { ElementLifecycleModule } from '@shared/directives/element-lifecycle';
|
||||
import { UiCommonModule, UiOverlayTriggerDirective } from '@ui/common';
|
||||
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { combineLatest, ReplaySubject, Subscription } from 'rxjs';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { map, take, shareReplay, startWith, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
|
||||
import { Item, PurchaseOptionsStore, isItemDTO, isShoppingCartItemDTO } from '../store';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { UiSelectModule } from '@ui/select';
|
||||
import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
|
||||
import { ScaleContentComponent } from '@shared/components/scale-content';
|
||||
import moment from 'moment';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-list-item',
|
||||
templateUrl: 'purchase-options-list-item.component.html',
|
||||
styleUrls: ['purchase-options-list-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
UiQuantityDropdownModule,
|
||||
UiSelectModule,
|
||||
ProductImageModule,
|
||||
IconComponent,
|
||||
UiSpinnerModule,
|
||||
ReactiveFormsModule,
|
||||
InputControlModule,
|
||||
FormsModule,
|
||||
ElementLifecycleModule,
|
||||
UiTooltipModule,
|
||||
UiCommonModule,
|
||||
ScaleContentComponent,
|
||||
OrderDeadlinePipeModule,
|
||||
],
|
||||
host: { class: 'shared-purchase-options-list-item' },
|
||||
})
|
||||
export class PurchaseOptionsListItemComponent implements OnInit, OnDestroy, OnChanges {
|
||||
private _subscriptions = new Subscription();
|
||||
|
||||
private _itemSubject = new ReplaySubject<Item>(1);
|
||||
|
||||
@Input() item: Item;
|
||||
|
||||
get item$() {
|
||||
return this._itemSubject.asObservable();
|
||||
}
|
||||
|
||||
get product() {
|
||||
return this.item.product;
|
||||
}
|
||||
|
||||
quantityFormControl = new FormControl<number>(null);
|
||||
|
||||
private readonly _giftCardValidators = [
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.max(GIFT_CARD_MAX_PRICE),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
private readonly _defaultValidators = [
|
||||
Validators.required,
|
||||
Validators.min(0.01),
|
||||
Validators.max(999.99),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
priceFormControl = new FormControl<string>(null);
|
||||
|
||||
manualVatFormControl = new FormControl<string>('', [Validators.required]);
|
||||
|
||||
selectedFormControl = new FormControl<boolean>(false);
|
||||
|
||||
availabilities$ = this.item$.pipe(switchMap((item) => this._store.getAvailabilitiesForItem$(item.id)));
|
||||
|
||||
availability$ = combineLatest([this.item$, this._store.purchaseOption$]).pipe(
|
||||
switchMap(([item, purchaseOption]) => this._store.getAvailabilityWithPurchaseOption$(item.id, purchaseOption)),
|
||||
map((availability) => availability?.data),
|
||||
);
|
||||
|
||||
price$ = this.item$.pipe(switchMap((item) => this._store.getPrice$(item.id)));
|
||||
|
||||
priceValue$ = this.price$.pipe(map((price) => price?.value?.value));
|
||||
|
||||
// Ticket #4074 analog zu Ticket #2244
|
||||
// take(2) um die Response des Katalogpreises und danach um die Response der OLAs abzuwarten
|
||||
// Logik gilt ausschließlich für Archivartikel
|
||||
setManualPrice$ = this.price$.pipe(
|
||||
take(2),
|
||||
map((price) => {
|
||||
// Logik nur beim Hinzufügen über Kaufoptionen, da über Ändern im Warenkorb die Info fehlt ob das jeweilige ShoppingCartItem ein Archivartikel ist oder nicht
|
||||
const features = this.item?.features as KeyValueDTOOfStringAndString[];
|
||||
if (!!features && Array.isArray(features)) {
|
||||
const isArchive = !!features?.find((feature) => feature?.enabled === true && feature?.key === 'ARC');
|
||||
return isArchive ? !price?.value?.value || price?.vat === undefined : false;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
vats$ = this._store.vats$.pipe(shareReplay());
|
||||
|
||||
priceVat$ = this.price$.pipe(map((price) => price?.vat?.vatType));
|
||||
|
||||
canAddResult$ = this.item$.pipe(
|
||||
switchMap((item) => this._store.getCanAddResultForItemAndCurrentPurchaseOption$(item.id)),
|
||||
);
|
||||
|
||||
canEditPrice$ = this.item$.pipe(
|
||||
switchMap((item) => combineLatest([this.canAddResult$, this._store.getCanEditPrice$(item.id)])),
|
||||
map(([canAddResult, canEditPrice]) => canAddResult?.canAdd && canEditPrice),
|
||||
);
|
||||
|
||||
canEditVat$ = this.item$.pipe(
|
||||
switchMap((item) => combineLatest([this.canAddResult$, this._store.getCanEditVat$(item.id)])),
|
||||
map(([canAddResult, canEditVat]) => canAddResult?.canAdd && canEditVat),
|
||||
);
|
||||
|
||||
isGiftCard$ = this.item$.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id)));
|
||||
|
||||
maxSelectableQuantity$ = combineLatest([this._store.purchaseOption$, this.availability$]).pipe(
|
||||
map(([purchaseOption, availability]) => {
|
||||
if (purchaseOption === 'in-store') {
|
||||
return availability?.inStock;
|
||||
}
|
||||
|
||||
return 999;
|
||||
}),
|
||||
startWith(999),
|
||||
);
|
||||
|
||||
showMaxAvailableQuantity$ = combineLatest([this._store.purchaseOption$, this.availability$, this.item$]).pipe(
|
||||
map(([purchaseOption, availability, item]) => {
|
||||
if (purchaseOption === 'pickup' && availability?.inStock < item.quantity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
fetchingAvailabilities$ = this.item$
|
||||
.pipe(switchMap((item) => this._store.getFetchingAvailabilitiesForItem$(item.id)))
|
||||
.pipe(map((fetchingAvailabilities) => fetchingAvailabilities.length > 0));
|
||||
|
||||
showNotAvailable$ = combineLatest([this.availabilities$, this.fetchingAvailabilities$]).pipe(
|
||||
map(([availabilities, fetchingAvailabilities]) => {
|
||||
if (fetchingAvailabilities) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availabilities.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
// Ticket #4813 Artikeldetailseite // EVT-Datum bei der Kaufoption Rücklage anzeigen
|
||||
get isEVT() {
|
||||
// Einstieg über Kaufoptionen - Hier wird die Katalogavailability verwendet die am ItemDTO hängt
|
||||
if (isItemDTO(this.item, this._store.type)) {
|
||||
const firstDayOfSale = this.item?.catalogAvailability?.firstDayOfSale;
|
||||
return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
|
||||
}
|
||||
|
||||
// Einstieg über Warenkorb - Da wir hier ein ShoppingCartItemDTO haben werden hier die Katalogdaten neu gefetched und eine Katalogavailability drangehängt
|
||||
if (isShoppingCartItemDTO(this.item, this._store.type)) {
|
||||
const catalogAvailabilities = this._store.availabilities?.filter(
|
||||
(availability) => availability?.purchaseOption === 'catalog',
|
||||
);
|
||||
// #4813 Fix: Hier muss als Kriterium auf EAN statt itemId verglichen werden, denn ein ShoppingCartItemDTO (this.item) hat eine andere ItemId wie das ItemDTO (availability.itemId)
|
||||
const firstDayOfSale = catalogAvailabilities?.find(
|
||||
(availability) => this.item?.product?.ean === availability?.ean,
|
||||
)?.data?.firstDayOfSale;
|
||||
return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
constructor(private _store: PurchaseOptionsStore) {}
|
||||
|
||||
firstDayOfSaleBiggerThanToday(firstDayOfSale: string): Date {
|
||||
if (!!firstDayOfSale && moment(firstDayOfSale)?.isAfter(moment())) {
|
||||
return moment(firstDayOfSale).toDate();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onPriceInputInit(target: HTMLElement, overlayTrigger: UiOverlayTriggerDirective) {
|
||||
if (this._store.getIsGiftCard(this.item.id)) {
|
||||
overlayTrigger.open();
|
||||
}
|
||||
|
||||
target?.focus();
|
||||
}
|
||||
|
||||
// Wichtig für das korrekte Setzen des Preises an das Item für den Endpoint request
|
||||
parsePrice(value: string) {
|
||||
if (PRICE_PATTERN.test(value)) {
|
||||
return parseFloat(value.replace(',', '.'));
|
||||
}
|
||||
}
|
||||
|
||||
stringifyPrice(value: number) {
|
||||
if (!value) return '';
|
||||
|
||||
const price = value.toFixed(2).replace('.', ',');
|
||||
if (price.includes(',')) {
|
||||
const [integer, decimal] = price.split(',');
|
||||
return `${integer},${decimal.padEnd(2, '0')}`;
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initPriceValidatorSubscription();
|
||||
this.initQuantitySubscription();
|
||||
this.initPriceSubscription();
|
||||
this.initVatSubscription();
|
||||
this.initSelectedSubscription();
|
||||
}
|
||||
|
||||
ngOnChanges({ item }: SimpleChanges) {
|
||||
if (item) {
|
||||
this._itemSubject.next(this.item);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._itemSubject.complete();
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
initPriceValidatorSubscription() {
|
||||
const sub = this.item$.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id))).subscribe((isGiftCard) => {
|
||||
if (isGiftCard) {
|
||||
this.priceFormControl.setValidators(this._giftCardValidators);
|
||||
} else {
|
||||
this.priceFormControl.setValidators(this._defaultValidators);
|
||||
}
|
||||
});
|
||||
|
||||
this._subscriptions.add(sub);
|
||||
}
|
||||
|
||||
initQuantitySubscription() {
|
||||
const sub = this.item$.subscribe((item) => {
|
||||
if (this.quantityFormControl.value !== item.quantity) {
|
||||
this.quantityFormControl.setValue(item.quantity);
|
||||
}
|
||||
});
|
||||
|
||||
const valueChangesSub = this.quantityFormControl.valueChanges.subscribe((quantity) => {
|
||||
if (this.item.quantity !== quantity) {
|
||||
this._store.setItemQuantity(this.item.id, quantity);
|
||||
}
|
||||
});
|
||||
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initPriceSubscription() {
|
||||
const sub = combineLatest([this.canEditPrice$, this.price$]).subscribe(([canEditPrice, price]) => {
|
||||
if (!canEditPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const priceStr = this.stringifyPrice(price?.value?.value);
|
||||
if (priceStr === '') return;
|
||||
|
||||
if (this.parsePrice(this.priceFormControl.value) !== price?.value?.value) {
|
||||
this.priceFormControl.setValue(priceStr);
|
||||
}
|
||||
});
|
||||
|
||||
const valueChangesSub = combineLatest([this.canEditPrice$, this.priceFormControl.valueChanges]).subscribe(
|
||||
([canEditPrice, value]) => {
|
||||
if (!canEditPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const price = this._store.getPrice(this.item.id);
|
||||
const parsedPrice = this.parsePrice(value);
|
||||
|
||||
if (!parsedPrice) {
|
||||
this._store.setPrice(this.item.id, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item.id] !== parsedPrice) {
|
||||
this._store.setPrice(this.item.id, this.parsePrice(value));
|
||||
}
|
||||
},
|
||||
);
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initVatSubscription() {
|
||||
const valueChangesSub = this.manualVatFormControl.valueChanges
|
||||
.pipe(withLatestFrom(this.vats$))
|
||||
.subscribe(([formVatType, vats]) => {
|
||||
const price = this._store.getPrice(this.item.id);
|
||||
|
||||
const vat = vats.find((vat) => vat?.vatType === Number(formVatType));
|
||||
|
||||
if (!vat) {
|
||||
this._store.setVat(this.item.id, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item.id]?.vat?.vatType !== vat?.vatType) {
|
||||
this._store.setVat(this.item.id, vat);
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initSelectedSubscription() {
|
||||
const sub = this.item$
|
||||
.pipe(switchMap((item) => this._store.selectedItemIds$.pipe(map((ids) => ids.includes(item.id)))))
|
||||
.subscribe((selected) => {
|
||||
if (this.selectedFormControl.value !== selected) {
|
||||
this.selectedFormControl.setValue(selected);
|
||||
}
|
||||
});
|
||||
const valueChangesSub = this.selectedFormControl.valueChanges.subscribe((selected) => {
|
||||
const current = this._store.selectedItemIds.includes(this.item.id);
|
||||
if (current !== selected) {
|
||||
this._store.setSelectedItem(this.item.id, selected);
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
}
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
OnChanges,
|
||||
SimpleChanges,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FormControl,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from '@angular/forms';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { InputControlModule } from '@shared/components/input-control';
|
||||
import { ElementLifecycleModule } from '@shared/directives/element-lifecycle';
|
||||
import { UiCommonModule, UiOverlayTriggerDirective } from '@ui/common';
|
||||
import { UiQuantityDropdownModule } from '@ui/quantity-dropdown';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { combineLatest, ReplaySubject, Subscription } from 'rxjs';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import {
|
||||
map,
|
||||
take,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
import { GIFT_CARD_MAX_PRICE, PRICE_PATTERN } from '../constants';
|
||||
import {
|
||||
Item,
|
||||
PurchaseOptionsStore,
|
||||
isDownload,
|
||||
isItemDTO,
|
||||
isShoppingCartItemDTO,
|
||||
} from '../store';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { UiSelectModule } from '@ui/select';
|
||||
import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
|
||||
import { ScaleContentComponent } from '@shared/components/scale-content';
|
||||
import moment from 'moment';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||
import { isaOtherInfo } from '@isa/icons';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-list-item',
|
||||
templateUrl: 'purchase-options-list-item.component.html',
|
||||
styleUrls: ['purchase-options-list-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
CommonModule,
|
||||
UiQuantityDropdownModule,
|
||||
UiSelectModule,
|
||||
ProductImageModule,
|
||||
IconComponent,
|
||||
UiSpinnerModule,
|
||||
ReactiveFormsModule,
|
||||
InputControlModule,
|
||||
FormsModule,
|
||||
ElementLifecycleModule,
|
||||
UiTooltipModule,
|
||||
UiCommonModule,
|
||||
ScaleContentComponent,
|
||||
OrderDeadlinePipeModule,
|
||||
NgIcon,
|
||||
],
|
||||
host: { class: 'shared-purchase-options-list-item' },
|
||||
providers: [provideIcons({ isaOtherInfo })],
|
||||
})
|
||||
export class PurchaseOptionsListItemComponent
|
||||
implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
private _subscriptions = new Subscription();
|
||||
|
||||
private _itemSubject = new ReplaySubject<Item>(1);
|
||||
|
||||
item = input.required<Item>();
|
||||
|
||||
get item$() {
|
||||
return this._itemSubject.asObservable();
|
||||
}
|
||||
|
||||
get product() {
|
||||
return this.item().product;
|
||||
}
|
||||
|
||||
redemptionPoints = computed(() => {
|
||||
const item = this.item();
|
||||
if (isShoppingCartItemDTO(item, this._store.type)) {
|
||||
return item.loyalty?.value;
|
||||
}
|
||||
|
||||
return item.redemptionPoints;
|
||||
});
|
||||
|
||||
showRedemptionPoints = toSignal(this._store.useRedemptionPoints$);
|
||||
|
||||
quantityFormControl = new FormControl<number>(null);
|
||||
|
||||
private readonly _giftCardValidators = [
|
||||
Validators.required,
|
||||
Validators.min(1),
|
||||
Validators.max(GIFT_CARD_MAX_PRICE),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
private readonly _defaultValidators = [
|
||||
Validators.required,
|
||||
Validators.min(0.01),
|
||||
Validators.max(999.99),
|
||||
Validators.pattern(PRICE_PATTERN),
|
||||
];
|
||||
|
||||
priceFormControl = new FormControl<string>(null);
|
||||
|
||||
manualVatFormControl = new FormControl<string>('', [Validators.required]);
|
||||
|
||||
selectedFormControl = new FormControl<boolean>(false);
|
||||
|
||||
availabilities$ = this.item$.pipe(
|
||||
switchMap((item) => this._store.getAvailabilitiesForItem$(item.id)),
|
||||
);
|
||||
|
||||
availability$ = combineLatest([this.item$, this._store.purchaseOption$]).pipe(
|
||||
switchMap(([item, purchaseOption]) =>
|
||||
this._store.getAvailabilityWithPurchaseOption$(item.id, purchaseOption),
|
||||
),
|
||||
map((availability) => availability?.data),
|
||||
);
|
||||
|
||||
availability = toSignal(this.availability$);
|
||||
|
||||
price$ = this.item$.pipe(switchMap((item) => this._store.getPrice$(item.id)));
|
||||
|
||||
priceValue$ = this.price$.pipe(map((price) => price?.value?.value));
|
||||
|
||||
// Ticket #4074 analog zu Ticket #2244
|
||||
// take(2) um die Response des Katalogpreises und danach um die Response der OLAs abzuwarten
|
||||
// Logik gilt ausschließlich für Archivartikel
|
||||
setManualPrice$ = this.price$.pipe(
|
||||
take(2),
|
||||
map((price) => {
|
||||
// Logik nur beim Hinzufügen über Kaufoptionen, da über Ändern im Warenkorb die Info fehlt ob das jeweilige ShoppingCartItem ein Archivartikel ist oder nicht
|
||||
const features = this.item().features as KeyValueDTOOfStringAndString[];
|
||||
if (!!features && Array.isArray(features)) {
|
||||
const isArchive = !!features?.find(
|
||||
(feature) => feature?.enabled === true && feature?.key === 'ARC',
|
||||
);
|
||||
return isArchive
|
||||
? !price?.value?.value || price?.vat === undefined
|
||||
: false;
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
vats$ = this._store.vats$.pipe(shareReplay());
|
||||
|
||||
priceVat$ = this.price$.pipe(map((price) => price?.vat?.vatType));
|
||||
|
||||
canAddResult$ = this.item$.pipe(
|
||||
switchMap((item) =>
|
||||
this._store.getCanAddResultForItemAndCurrentPurchaseOption$(item.id),
|
||||
),
|
||||
);
|
||||
|
||||
canEditPrice$ = this.item$.pipe(
|
||||
switchMap((item) =>
|
||||
combineLatest([
|
||||
this.canAddResult$,
|
||||
this._store.getCanEditPrice$(item.id),
|
||||
]),
|
||||
),
|
||||
map(([canAddResult, canEditPrice]) => canAddResult?.canAdd && canEditPrice),
|
||||
);
|
||||
|
||||
canEditVat$ = this.item$.pipe(
|
||||
switchMap((item) =>
|
||||
combineLatest([this.canAddResult$, this._store.getCanEditVat$(item.id)]),
|
||||
),
|
||||
map(([canAddResult, canEditVat]) => canAddResult?.canAdd && canEditVat),
|
||||
);
|
||||
|
||||
isGiftCard$ = this.item$.pipe(
|
||||
switchMap((item) => this._store.getIsGiftCard$(item.id)),
|
||||
);
|
||||
|
||||
maxSelectableQuantity$ = combineLatest([
|
||||
this._store.purchaseOption$,
|
||||
this.availability$,
|
||||
]).pipe(
|
||||
map(([purchaseOption, availability]) => {
|
||||
if (purchaseOption === 'in-store') {
|
||||
return availability?.inStock;
|
||||
}
|
||||
|
||||
return 999;
|
||||
}),
|
||||
startWith(999),
|
||||
);
|
||||
|
||||
showMaxAvailableQuantity$ = combineLatest([
|
||||
this._store.purchaseOption$,
|
||||
this.availability$,
|
||||
this.item$,
|
||||
]).pipe(
|
||||
map(([purchaseOption, availability, item]) => {
|
||||
if (
|
||||
purchaseOption === 'pickup' &&
|
||||
availability?.inStock < item.quantity
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
fetchingAvailabilitiesArray$ = this.item$.pipe(
|
||||
switchMap((item) => this._store.getFetchingAvailabilitiesForItem$(item.id)),
|
||||
);
|
||||
|
||||
fetchingAvailabilities$ = this.fetchingAvailabilitiesArray$.pipe(
|
||||
map((fetchingAvailabilities) => fetchingAvailabilities.length > 0),
|
||||
);
|
||||
|
||||
fetchingInStoreAvailability$ = this.fetchingAvailabilitiesArray$.pipe(
|
||||
map((fetchingAvailabilities) =>
|
||||
fetchingAvailabilities.some((fa) => fa.purchaseOption === 'in-store'),
|
||||
),
|
||||
);
|
||||
|
||||
isFetchingInStore = toSignal(this.fetchingInStoreAvailability$, {
|
||||
initialValue: false,
|
||||
});
|
||||
|
||||
showNotAvailable$ = combineLatest([
|
||||
this.availabilities$,
|
||||
this.fetchingAvailabilities$,
|
||||
]).pipe(
|
||||
map(([availabilities, fetchingAvailabilities]) => {
|
||||
if (fetchingAvailabilities) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availabilities.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
|
||||
isDownload$ = this.item$.pipe(map((item) => isDownload(item)));
|
||||
|
||||
isDownloadItem = toSignal(this.isDownload$, { initialValue: false });
|
||||
|
||||
showNoDownloadAvailability$ = combineLatest([
|
||||
this.isDownload$,
|
||||
this.availabilities$,
|
||||
this.fetchingAvailabilities$,
|
||||
]).pipe(
|
||||
map(([isDownloadItem, availabilities, fetchingAvailabilities]) => {
|
||||
// Only check for download items
|
||||
if (!isDownloadItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't show error while loading
|
||||
if (fetchingAvailabilities) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if download availability exists
|
||||
const hasDownloadAvailability = availabilities.some(
|
||||
(a) => a.purchaseOption === 'download',
|
||||
);
|
||||
|
||||
return !hasDownloadAvailability;
|
||||
}),
|
||||
);
|
||||
|
||||
// Ticket #4813 Artikeldetailseite // EVT-Datum bei der Kaufoption Rücklage anzeigen
|
||||
get isEVT() {
|
||||
// Einstieg über Kaufoptionen - Hier wird die Katalogavailability verwendet die am ItemDTO hängt
|
||||
if (isItemDTO(this.item, this._store.type)) {
|
||||
const firstDayOfSale = this.item?.catalogAvailability?.firstDayOfSale;
|
||||
return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
|
||||
}
|
||||
|
||||
// Einstieg über Warenkorb - Da wir hier ein ShoppingCartItemDTO haben werden hier die Katalogdaten neu gefetched und eine Katalogavailability drangehängt
|
||||
if (isShoppingCartItemDTO(this.item, this._store.type)) {
|
||||
const catalogAvailabilities = this._store.availabilities?.filter(
|
||||
(availability) => availability?.purchaseOption === 'catalog',
|
||||
);
|
||||
// #4813 Fix: Hier muss als Kriterium auf EAN statt itemId verglichen werden, denn ein ShoppingCartItemDTO (this.item) hat eine andere ItemId wie das ItemDTO (availability.itemId)
|
||||
const firstDayOfSale = catalogAvailabilities?.find(
|
||||
(availability) => this.item().product?.ean === availability?.ean,
|
||||
)?.data?.firstDayOfSale;
|
||||
return this.firstDayOfSaleBiggerThanToday(firstDayOfSale);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
useRedemptionPoints = toSignal(this._store.useRedemptionPoints$);
|
||||
|
||||
purchaseOption = toSignal(this._store.purchaseOption$);
|
||||
|
||||
isReservePurchaseOption = computed(() => {
|
||||
return this.purchaseOption() === 'in-store';
|
||||
});
|
||||
|
||||
showLowStockMessage = computed(() => {
|
||||
const availability = this.availability();
|
||||
const inStock = availability?.inStock ?? 0;
|
||||
|
||||
return (
|
||||
this.useRedemptionPoints() &&
|
||||
this.isReservePurchaseOption() &&
|
||||
!this.isDownloadItem() &&
|
||||
!this.isFetchingInStore() &&
|
||||
inStock > 0 &&
|
||||
inStock < 2
|
||||
);
|
||||
});
|
||||
|
||||
constructor(private _store: PurchaseOptionsStore) {}
|
||||
|
||||
firstDayOfSaleBiggerThanToday(firstDayOfSale: string): Date {
|
||||
if (!!firstDayOfSale && moment(firstDayOfSale)?.isAfter(moment())) {
|
||||
return moment(firstDayOfSale).toDate();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
onPriceInputInit(
|
||||
target: HTMLElement,
|
||||
overlayTrigger: UiOverlayTriggerDirective,
|
||||
) {
|
||||
if (this._store.getIsGiftCard(this.item().id)) {
|
||||
overlayTrigger.open();
|
||||
}
|
||||
|
||||
target?.focus();
|
||||
}
|
||||
|
||||
// Wichtig für das korrekte Setzen des Preises an das Item für den Endpoint request
|
||||
parsePrice(value: string) {
|
||||
if (PRICE_PATTERN.test(value)) {
|
||||
return parseFloat(value.replace(',', '.'));
|
||||
}
|
||||
}
|
||||
|
||||
stringifyPrice(value: number) {
|
||||
if (!value) return '';
|
||||
|
||||
const price = value.toFixed(2).replace('.', ',');
|
||||
if (price.includes(',')) {
|
||||
const [integer, decimal] = price.split(',');
|
||||
return `${integer},${decimal.padEnd(2, '0')}`;
|
||||
}
|
||||
|
||||
return price;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initPriceValidatorSubscription();
|
||||
this.initQuantitySubscription();
|
||||
this.initPriceSubscription();
|
||||
this.initVatSubscription();
|
||||
this.initSelectedSubscription();
|
||||
}
|
||||
|
||||
ngOnChanges({ item }: SimpleChanges) {
|
||||
if (item) {
|
||||
this._itemSubject.next(this.item());
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._itemSubject.complete();
|
||||
this._subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
initPriceValidatorSubscription() {
|
||||
const sub = this.item$
|
||||
.pipe(switchMap((item) => this._store.getIsGiftCard$(item.id)))
|
||||
.subscribe((isGiftCard) => {
|
||||
if (isGiftCard) {
|
||||
this.priceFormControl.setValidators(this._giftCardValidators);
|
||||
} else {
|
||||
this.priceFormControl.setValidators(this._defaultValidators);
|
||||
}
|
||||
});
|
||||
|
||||
this._subscriptions.add(sub);
|
||||
}
|
||||
|
||||
initQuantitySubscription() {
|
||||
const sub = this.item$.subscribe((item) => {
|
||||
if (this.quantityFormControl.value !== item.quantity) {
|
||||
this.quantityFormControl.setValue(item.quantity);
|
||||
}
|
||||
});
|
||||
|
||||
const valueChangesSub = this.quantityFormControl.valueChanges.subscribe(
|
||||
(quantity) => {
|
||||
if (this.item().quantity !== quantity) {
|
||||
this._store.setItemQuantity(this.item().id, quantity);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initPriceSubscription() {
|
||||
const sub = combineLatest([this.canEditPrice$, this.price$]).subscribe(
|
||||
([canEditPrice, price]) => {
|
||||
if (!canEditPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const priceStr = this.stringifyPrice(price?.value?.value);
|
||||
if (priceStr === '') return;
|
||||
|
||||
if (
|
||||
this.parsePrice(this.priceFormControl.value) !== price?.value?.value
|
||||
) {
|
||||
this.priceFormControl.setValue(priceStr);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const valueChangesSub = combineLatest([
|
||||
this.canEditPrice$,
|
||||
this.priceFormControl.valueChanges,
|
||||
]).subscribe(([canEditPrice, value]) => {
|
||||
if (!canEditPrice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const price = this._store.getPrice(this.item().id);
|
||||
const parsedPrice = this.parsePrice(value);
|
||||
|
||||
if (!parsedPrice) {
|
||||
this._store.setPrice(this.item().id, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item().id] !== parsedPrice) {
|
||||
this._store.setPrice(this.item().id, this.parsePrice(value));
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initVatSubscription() {
|
||||
const valueChangesSub = this.manualVatFormControl.valueChanges
|
||||
.pipe(withLatestFrom(this.vats$))
|
||||
.subscribe(([formVatType, vats]) => {
|
||||
const price = this._store.getPrice(this.item().id);
|
||||
|
||||
const vat = vats.find((vat) => vat?.vatType === Number(formVatType));
|
||||
|
||||
if (!vat) {
|
||||
this._store.setVat(this.item().id, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (price[this.item().id]?.vat?.vatType !== vat?.vatType) {
|
||||
this._store.setVat(this.item().id, vat);
|
||||
}
|
||||
});
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
|
||||
initSelectedSubscription() {
|
||||
const sub = this.item$
|
||||
.pipe(
|
||||
switchMap((item) =>
|
||||
this._store.selectedItemIds$.pipe(
|
||||
map((ids) => ids.includes(item.id)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe((selected) => {
|
||||
if (this.selectedFormControl.value !== selected) {
|
||||
this.selectedFormControl.setValue(selected);
|
||||
}
|
||||
});
|
||||
const valueChangesSub = this.selectedFormControl.valueChanges.subscribe(
|
||||
(selected) => {
|
||||
const current = this._store.selectedItemIds.includes(this.item().id);
|
||||
if (current !== selected) {
|
||||
this._store.setSelectedItem(this.item().id, selected);
|
||||
}
|
||||
},
|
||||
);
|
||||
this._subscriptions.add(sub);
|
||||
this._subscriptions.add(valueChangesSub);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,145 +1,204 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, TrackByFunction, HostBinding } from '@angular/core';
|
||||
import { UiModalRef } from '@ui/modal';
|
||||
import { PurchaseOptionsModalData } from './purchase-options-modal.data';
|
||||
|
||||
import { PurchaseOptionsListHeaderComponent } from './purchase-options-list-header';
|
||||
import { PurchaseOptionsListItemComponent } from './purchase-options-list-item';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Subject, zip } from 'rxjs';
|
||||
import {
|
||||
DeliveryPurchaseOptionTileComponent,
|
||||
DownloadPurchaseOptionTileComponent,
|
||||
InStorePurchaseOptionTileComponent,
|
||||
PickupPurchaseOptionTileComponent,
|
||||
} from './purchase-options-tile';
|
||||
import { isGiftCard, Item, PurchaseOption, PurchaseOptionsStore } from './store';
|
||||
import { delay, map, shareReplay, skip, switchMap, takeUntil, tap } from 'rxjs/operators';
|
||||
import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
|
||||
import { provideComponentStore } from '@ngrx/component-store';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-modal',
|
||||
templateUrl: 'purchase-options-modal.component.html',
|
||||
styleUrls: ['purchase-options-modal.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [provideComponentStore(PurchaseOptionsStore)],
|
||||
imports: [
|
||||
CommonModule,
|
||||
PurchaseOptionsListHeaderComponent,
|
||||
PurchaseOptionsListItemComponent,
|
||||
DeliveryPurchaseOptionTileComponent,
|
||||
InStorePurchaseOptionTileComponent,
|
||||
PickupPurchaseOptionTileComponent,
|
||||
DownloadPurchaseOptionTileComponent,
|
||||
],
|
||||
})
|
||||
export class PurchaseOptionsModalComponent implements OnInit, OnDestroy {
|
||||
get type() {
|
||||
return this._uiModalRef.data.type;
|
||||
}
|
||||
|
||||
@HostBinding('attr.data-loading')
|
||||
get fetchingData() {
|
||||
return this.store.fetchingAvailabilities.length > 0;
|
||||
}
|
||||
|
||||
items$ = this.store.items$;
|
||||
|
||||
hasPrice$ = this.items$.pipe(
|
||||
switchMap((items) =>
|
||||
items.map((item) => {
|
||||
let isArchive = false;
|
||||
const features = item?.features as KeyValueDTOOfStringAndString[];
|
||||
// Ticket #4074 analog zu Ticket #2244
|
||||
// Ob Archivartikel kann nur über Kaufoptionen herausgefunden werden, nicht über Ändern im Warenkorb da am ShoppingCartItem das Archivartikel Feature fehlt
|
||||
if (!!features && Array.isArray(features)) {
|
||||
isArchive = !!features?.find((feature) => feature?.enabled === true && feature?.key === 'ARC');
|
||||
}
|
||||
return zip(
|
||||
this.store
|
||||
?.getPrice$(item?.id)
|
||||
.pipe(
|
||||
map((price) =>
|
||||
isArchive
|
||||
? !!price?.value?.value && price?.vat !== undefined && price?.vat?.value !== undefined
|
||||
: !!price?.value?.value,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
switchMap((hasPrices) => hasPrices),
|
||||
map((hasPrices) => {
|
||||
const containsItemWithNoPrice = hasPrices?.filter((hasPrice) => hasPrice === false) ?? [];
|
||||
return containsItemWithNoPrice?.length === 0;
|
||||
}),
|
||||
);
|
||||
|
||||
purchasingOptions$ = this.store.getPurchaseOptionsInAvailabilities$;
|
||||
|
||||
isDownloadOnly$ = this.purchasingOptions$.pipe(
|
||||
map((purchasingOptions) => purchasingOptions.length === 1 && purchasingOptions[0] === 'download'),
|
||||
);
|
||||
|
||||
isGiftCardOnly$ = this.store.items$.pipe(map((items) => items.every((item) => isGiftCard(item, this.store.type))));
|
||||
|
||||
hasDownload$ = this.purchasingOptions$.pipe(map((purchasingOptions) => purchasingOptions.includes('download')));
|
||||
|
||||
canContinue$ = this.store.canContinue$;
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
saving = false;
|
||||
|
||||
constructor(
|
||||
private _uiModalRef: UiModalRef<string, PurchaseOptionsModalData>,
|
||||
public store: PurchaseOptionsStore,
|
||||
) {
|
||||
this.store.initialize(this._uiModalRef.data);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.items$.pipe(takeUntil(this._onDestroy$), skip(1), delay(100)).subscribe((items) => {
|
||||
if (items.length === 0) {
|
||||
this._uiModalRef.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._uiModalRef.data?.preSelectOption?.option) {
|
||||
this.store.setPurchaseOption(this._uiModalRef.data?.preSelectOption?.option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
itemTrackBy: TrackByFunction<Item> = (_, item) => item.id;
|
||||
|
||||
showOption(option: PurchaseOption): boolean {
|
||||
return this._uiModalRef.data?.preSelectOption?.showOptionOnly
|
||||
? this._uiModalRef.data?.preSelectOption?.option === option
|
||||
: true;
|
||||
}
|
||||
|
||||
async save(action: string) {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await this.store.save();
|
||||
|
||||
if (this.store.items.length === 0) {
|
||||
this._uiModalRef.close(action);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
TrackByFunction,
|
||||
HostBinding,
|
||||
} from '@angular/core';
|
||||
import { UiModalRef } from '@ui/modal';
|
||||
import { PurchaseOptionsModalContext } from './purchase-options-modal.data';
|
||||
|
||||
import { PurchaseOptionsListHeaderComponent } from './purchase-options-list-header';
|
||||
import { PurchaseOptionsListItemComponent } from './purchase-options-list-item';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Subject, zip } from 'rxjs';
|
||||
import {
|
||||
DeliveryPurchaseOptionTileComponent,
|
||||
DownloadPurchaseOptionTileComponent,
|
||||
InStorePurchaseOptionTileComponent,
|
||||
PickupPurchaseOptionTileComponent,
|
||||
} from './purchase-options-tile';
|
||||
import {
|
||||
isDownload,
|
||||
isGiftCard,
|
||||
Item,
|
||||
PurchaseOption,
|
||||
PurchaseOptionsStore,
|
||||
} from './store';
|
||||
import {
|
||||
delay,
|
||||
map,
|
||||
shareReplay,
|
||||
skip,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
import { KeyValueDTOOfStringAndString } from '@generated/swagger/cat-search-api';
|
||||
import { provideComponentStore } from '@ngrx/component-store';
|
||||
|
||||
@Component({
|
||||
selector: 'shared-purchase-options-modal',
|
||||
templateUrl: 'purchase-options-modal.component.html',
|
||||
styleUrls: ['purchase-options-modal.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: [provideComponentStore(PurchaseOptionsStore)],
|
||||
imports: [
|
||||
CommonModule,
|
||||
PurchaseOptionsListHeaderComponent,
|
||||
PurchaseOptionsListItemComponent,
|
||||
DeliveryPurchaseOptionTileComponent,
|
||||
InStorePurchaseOptionTileComponent,
|
||||
PickupPurchaseOptionTileComponent,
|
||||
DownloadPurchaseOptionTileComponent,
|
||||
],
|
||||
})
|
||||
export class PurchaseOptionsModalComponent implements OnInit, OnDestroy {
|
||||
get type() {
|
||||
return this._uiModalRef.data.type;
|
||||
}
|
||||
|
||||
@HostBinding('attr.data-loading')
|
||||
get fetchingData() {
|
||||
return this.store.fetchingAvailabilities.length > 0;
|
||||
}
|
||||
|
||||
items$ = this.store.items$;
|
||||
|
||||
hasPrice$ = this.items$.pipe(
|
||||
switchMap((items) =>
|
||||
items.map((item) => {
|
||||
let isArchive = false;
|
||||
const features = item?.features as KeyValueDTOOfStringAndString[];
|
||||
// Ticket #4074 analog zu Ticket #2244
|
||||
// Ob Archivartikel kann nur über Kaufoptionen herausgefunden werden, nicht über Ändern im Warenkorb da am ShoppingCartItem das Archivartikel Feature fehlt
|
||||
if (!!features && Array.isArray(features)) {
|
||||
isArchive = !!features?.find(
|
||||
(feature) => feature?.enabled === true && feature?.key === 'ARC',
|
||||
);
|
||||
}
|
||||
return zip(
|
||||
this.store
|
||||
?.getPrice$(item?.id)
|
||||
.pipe(
|
||||
map((price) =>
|
||||
isArchive
|
||||
? !!price?.value?.value &&
|
||||
price?.vat !== undefined &&
|
||||
price?.vat?.value !== undefined
|
||||
: !!price?.value?.value,
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
switchMap((hasPrices) => hasPrices),
|
||||
map((hasPrices) => {
|
||||
const containsItemWithNoPrice =
|
||||
hasPrices?.filter((hasPrice) => hasPrice === false) ?? [];
|
||||
return containsItemWithNoPrice?.length === 0;
|
||||
}),
|
||||
);
|
||||
|
||||
purchasingOptions$ = this.store.getPurchaseOptionsInAvailabilities$;
|
||||
|
||||
isDownloadOnly$ = this.store.items$.pipe(
|
||||
map((items) => items.length > 0 && items.every((item) => isDownload(item))),
|
||||
);
|
||||
|
||||
isGiftCardOnly$ = this.store.items$.pipe(
|
||||
map((items) => items.every((item) => isGiftCard(item, this.store.type))),
|
||||
);
|
||||
|
||||
hasDownload$ = this.store.items$.pipe(
|
||||
map((items) => items.some((item) => isDownload(item))),
|
||||
);
|
||||
|
||||
canContinue$ = this.store.canContinue$;
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
saving = false;
|
||||
|
||||
constructor(
|
||||
private _uiModalRef: UiModalRef<string, PurchaseOptionsModalContext>,
|
||||
public store: PurchaseOptionsStore,
|
||||
) {
|
||||
this.store.initialize(this._uiModalRef.data);
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.items$
|
||||
.pipe(takeUntil(this._onDestroy$), skip(1), delay(100))
|
||||
.subscribe((items) => {
|
||||
if (items.length === 0) {
|
||||
this._uiModalRef.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._uiModalRef.data?.preSelectOption?.option) {
|
||||
this.store.setPurchaseOption(
|
||||
this._uiModalRef.data?.preSelectOption?.option,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
itemTrackBy: TrackByFunction<Item> = (_, item) => item.id;
|
||||
|
||||
/**
|
||||
* Determines if a purchase option should be shown in the UI.
|
||||
*
|
||||
* Evaluation order:
|
||||
* 1. If option is disabled AND hideDisabledPurchaseOptions is true -> hide (return false)
|
||||
* 2. If preSelectOption.showOptionOnly is true -> show only that option
|
||||
* 3. Otherwise -> show the option
|
||||
*
|
||||
* @param option - The purchase option to check
|
||||
* @returns true if the option should be displayed, false otherwise
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In template
|
||||
* @if (showOption('delivery')) {
|
||||
* <app-delivery-purchase-options-tile></app-delivery-purchase-options-tile>
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
showOption(option: PurchaseOption): boolean {
|
||||
const disabledOptions = this._uiModalRef.data?.disabledPurchaseOptions ?? [];
|
||||
const hideDisabled = this._uiModalRef.data?.hideDisabledPurchaseOptions ?? true;
|
||||
|
||||
if (disabledOptions.includes(option) && hideDisabled) {
|
||||
return false;
|
||||
}
|
||||
return this._uiModalRef.data?.preSelectOption?.showOptionOnly
|
||||
? this._uiModalRef.data?.preSelectOption?.option === option
|
||||
: true;
|
||||
}
|
||||
|
||||
async save(action: string) {
|
||||
if (this.saving) {
|
||||
return;
|
||||
}
|
||||
this.saving = true;
|
||||
|
||||
try {
|
||||
await this.store.save();
|
||||
|
||||
if (this.store.items.length === 0) {
|
||||
this._uiModalRef.close(action);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,130 @@
|
||||
import { ItemDTO } from '@generated/swagger/cat-search-api';
|
||||
import { ShoppingCartItemDTO, BranchDTO } from '@generated/swagger/checkout-api';
|
||||
import { ActionType, PurchaseOption } from './store';
|
||||
|
||||
export interface PurchaseOptionsModalData {
|
||||
processId: number;
|
||||
type: ActionType;
|
||||
items: Array<ItemDTO | ShoppingCartItemDTO>;
|
||||
pickupBranch?: BranchDTO;
|
||||
inStoreBranch?: BranchDTO;
|
||||
preSelectOption?: { option: PurchaseOption; showOptionOnly?: boolean };
|
||||
}
|
||||
import { ItemDTO } from '@generated/swagger/cat-search-api';
|
||||
import {
|
||||
ShoppingCartItemDTO,
|
||||
BranchDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
import { Customer } from '@isa/crm/data-access';
|
||||
import { ActionType, PurchaseOption } from './store';
|
||||
|
||||
/**
|
||||
* Data interface for opening the Purchase Options Modal.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage
|
||||
* const modalRef = await purchaseOptionsModalService.open({
|
||||
* tabId: 123,
|
||||
* shoppingCartId: 456,
|
||||
* type: 'add',
|
||||
* items: [item1, item2],
|
||||
* });
|
||||
*
|
||||
* // With disabled options
|
||||
* const modalRef = await purchaseOptionsModalService.open({
|
||||
* tabId: 123,
|
||||
* shoppingCartId: 456,
|
||||
* type: 'add',
|
||||
* items: [rewardItem],
|
||||
* disabledPurchaseOptions: ['b2b-delivery'],
|
||||
* hideDisabledPurchaseOptions: true, // Hide completely (default)
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export interface PurchaseOptionsModalData {
|
||||
/** Tab ID for maintaining context across the application */
|
||||
tabId: number;
|
||||
|
||||
/** Shopping cart ID where items will be added or updated */
|
||||
shoppingCartId: number;
|
||||
|
||||
/**
|
||||
* Action type determining modal behavior:
|
||||
* - 'add': Adding new items to cart
|
||||
* - 'update': Updating existing cart items
|
||||
*/
|
||||
type: ActionType;
|
||||
|
||||
/**
|
||||
* Enable redemption points mode for reward items.
|
||||
* When true, prices are set to 0 and loyalty points are applied.
|
||||
*/
|
||||
useRedemptionPoints?: boolean;
|
||||
|
||||
/** Items to display in the modal for purchase option selection */
|
||||
items: Array<ItemDTO | ShoppingCartItemDTO>;
|
||||
|
||||
/** Pre-configured branch for pickup option */
|
||||
pickupBranch?: BranchDTO;
|
||||
|
||||
/** Pre-configured branch for in-store option */
|
||||
inStoreBranch?: BranchDTO;
|
||||
|
||||
/**
|
||||
* Pre-select a specific purchase option on modal open.
|
||||
* Set showOptionOnly to true to display only that option.
|
||||
*/
|
||||
preSelectOption?: { option: PurchaseOption; showOptionOnly?: boolean };
|
||||
|
||||
/**
|
||||
* Purchase options to disable. Disabled options:
|
||||
* - Will not have availability API calls made
|
||||
* - Will either be hidden or shown as disabled based on hideDisabledPurchaseOptions
|
||||
*
|
||||
* @example ['b2b-delivery', 'download']
|
||||
*/
|
||||
disabledPurchaseOptions?: PurchaseOption[];
|
||||
|
||||
/**
|
||||
* Controls visibility of disabled purchase options.
|
||||
* - true (default): Disabled options are completely hidden from the UI
|
||||
* - false: Disabled options are shown with a disabled visual state (grayed out, not clickable)
|
||||
*
|
||||
* @default true
|
||||
*/
|
||||
hideDisabledPurchaseOptions?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal context interface used within the modal component.
|
||||
* Extends PurchaseOptionsModalData with additional runtime data like selected customer.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export interface PurchaseOptionsModalContext {
|
||||
/** Shopping cart ID where items will be added or updated */
|
||||
shoppingCartId: number;
|
||||
|
||||
/**
|
||||
* Action type determining modal behavior:
|
||||
* - 'add': Adding new items to cart
|
||||
* - 'update': Updating existing cart items
|
||||
*/
|
||||
type: ActionType;
|
||||
|
||||
/** Enable redemption points mode for reward items */
|
||||
useRedemptionPoints: boolean;
|
||||
|
||||
/** Items to display in the modal for purchase option selection */
|
||||
items: Array<ItemDTO | ShoppingCartItemDTO>;
|
||||
|
||||
/** Customer selected in the current tab (resolved at runtime) */
|
||||
selectedCustomer?: Customer;
|
||||
|
||||
/** Default branch resolved from user settings or tab context */
|
||||
selectedBranch?: BranchDTO;
|
||||
|
||||
/** Pre-configured branch for pickup option */
|
||||
pickupBranch?: BranchDTO;
|
||||
|
||||
/** Pre-configured branch for in-store option */
|
||||
inStoreBranch?: BranchDTO;
|
||||
|
||||
/** Pre-select a specific purchase option on modal open */
|
||||
preSelectOption?: { option: PurchaseOption; showOptionOnly?: boolean };
|
||||
|
||||
/** Purchase options to disable (no API calls, conditional UI rendering) */
|
||||
disabledPurchaseOptions?: PurchaseOption[];
|
||||
|
||||
/** Controls visibility of disabled purchase options (default: true = hidden) */
|
||||
hideDisabledPurchaseOptions?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,118 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UiModalRef, UiModalService } from '@ui/modal';
|
||||
import { PurchaseOptionsModalComponent } from './purchase-options-modal.component';
|
||||
import { PurchaseOptionsModalData } from './purchase-options-modal.data';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PurchaseOptionsModalService {
|
||||
constructor(private _uiModal: UiModalService) {}
|
||||
|
||||
open(data: PurchaseOptionsModalData): UiModalRef<string, PurchaseOptionsModalData> {
|
||||
return this._uiModal.open<string, PurchaseOptionsModalData>({
|
||||
content: PurchaseOptionsModalComponent,
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
import { Injectable, inject, untracked } from '@angular/core';
|
||||
import { UiModalRef, UiModalService } from '@ui/modal';
|
||||
import { PurchaseOptionsModalComponent } from './purchase-options-modal.component';
|
||||
import {
|
||||
PurchaseOptionsModalData,
|
||||
PurchaseOptionsModalContext,
|
||||
} from './purchase-options-modal.data';
|
||||
import {
|
||||
CustomerFacade,
|
||||
Customer,
|
||||
CrmTabMetadataService,
|
||||
} from '@isa/crm/data-access';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { BranchDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
/**
|
||||
* Service for opening and managing the Purchase Options Modal.
|
||||
*
|
||||
* The Purchase Options Modal allows users to select how they want to receive items
|
||||
* (delivery, pickup, in-store, download) and manages availability checking, pricing,
|
||||
* and shopping cart operations.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage
|
||||
* const modalRef = await this.purchaseOptionsModalService.open({
|
||||
* tabId: 123,
|
||||
* shoppingCartId: 456,
|
||||
* type: 'add',
|
||||
* items: [item1, item2],
|
||||
* });
|
||||
*
|
||||
* // Await modal close
|
||||
* const result = await firstValueFrom(modalRef.afterClosed$);
|
||||
* ```
|
||||
*/
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PurchaseOptionsModalService {
|
||||
#uiModal = inject(UiModalService);
|
||||
#tabService = inject(TabService);
|
||||
#crmTabMetadataService = inject(CrmTabMetadataService);
|
||||
#customerFacade = inject(CustomerFacade);
|
||||
|
||||
/**
|
||||
* Opens the Purchase Options Modal.
|
||||
*
|
||||
* @param data - Configuration data for the modal
|
||||
* @returns Promise resolving to a modal reference
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Add new items with disabled B2B delivery
|
||||
* const modalRef = await this.purchaseOptionsModalService.open({
|
||||
* tabId: processId,
|
||||
* shoppingCartId: cartId,
|
||||
* type: 'add',
|
||||
* items: [item1, item2],
|
||||
* disabledPurchaseOptions: ['b2b-delivery'],
|
||||
* });
|
||||
*
|
||||
* // Wait for modal to close
|
||||
* const action = await firstValueFrom(modalRef.afterClosed$);
|
||||
* if (action === 'continue') {
|
||||
* // Proceed to next step
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async open(
|
||||
data: PurchaseOptionsModalData,
|
||||
): Promise<UiModalRef<string, PurchaseOptionsModalData>> {
|
||||
const context: PurchaseOptionsModalContext = {
|
||||
useRedemptionPoints: !!data.useRedemptionPoints,
|
||||
...data,
|
||||
};
|
||||
|
||||
context.selectedCustomer = await this.#getSelectedCustomer(data);
|
||||
context.selectedBranch = this.#getSelectedBranch(data.tabId);
|
||||
return this.#uiModal.open<string, PurchaseOptionsModalContext>({
|
||||
content: PurchaseOptionsModalComponent,
|
||||
data: context,
|
||||
});
|
||||
}
|
||||
|
||||
#getSelectedCustomer({
|
||||
tabId,
|
||||
}: {
|
||||
tabId: number;
|
||||
}): Promise<Customer | undefined> {
|
||||
const customerId = this.#crmTabMetadataService.selectedCustomerId(tabId);
|
||||
|
||||
if (!customerId) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
return this.#customerFacade.fetchCustomer({ customerId });
|
||||
}
|
||||
|
||||
#getSelectedBranch(tabId: number): BranchDTO | undefined {
|
||||
const tab = untracked(() =>
|
||||
this.#tabService.entities().find((t) => t.id === tabId),
|
||||
);
|
||||
|
||||
if (!tab) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const legacyProcessData = tab?.metadata?.process_data;
|
||||
|
||||
if (
|
||||
typeof legacyProcessData === 'object' &&
|
||||
'selectedBranch' in legacyProcessData
|
||||
) {
|
||||
return legacyProcessData.selectedBranch as BranchDTO;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,27 @@ import { ChangeDetectorRef, Directive, HostBinding, HostListener } from '@angula
|
||||
import { asapScheduler } from 'rxjs';
|
||||
import { PurchaseOption, PurchaseOptionsStore } from '../store';
|
||||
|
||||
/**
|
||||
* Base directive for purchase option tile components.
|
||||
*
|
||||
* Provides common functionality for all purchase option tiles:
|
||||
* - Visual selected state binding
|
||||
* - Visual disabled state binding
|
||||
* - Click handling with disabled check
|
||||
* - Auto-selection of available items
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* export class DeliveryPurchaseOptionTileComponent extends BasePurchaseOptionDirective {
|
||||
* constructor(
|
||||
* protected store: PurchaseOptionsStore,
|
||||
* protected cdr: ChangeDetectorRef,
|
||||
* ) {
|
||||
* super('delivery');
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
@Directive({
|
||||
standalone: false,
|
||||
})
|
||||
@@ -9,15 +30,46 @@ export abstract class BasePurchaseOptionDirective {
|
||||
protected abstract store: PurchaseOptionsStore;
|
||||
protected abstract cdr: ChangeDetectorRef;
|
||||
|
||||
/**
|
||||
* Binds the 'selected' CSS class to the host element.
|
||||
* Applied when this purchase option is the currently selected one.
|
||||
*/
|
||||
@HostBinding('class.selected')
|
||||
get selected() {
|
||||
return this.store.purchaseOption === this.purchaseOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the 'disabled' CSS class to the host element.
|
||||
* Applied when this purchase option is in the disabledPurchaseOptions array.
|
||||
* Disabled options:
|
||||
* - Have no availability API calls made
|
||||
* - Are shown with reduced opacity and not-allowed cursor
|
||||
* - Prevent click interactions
|
||||
*/
|
||||
@HostBinding('class.disabled')
|
||||
get disabled() {
|
||||
return this.store.disabledPurchaseOptions.includes(this.purchaseOption);
|
||||
}
|
||||
|
||||
constructor(protected purchaseOption: PurchaseOption) {}
|
||||
|
||||
/**
|
||||
* Handles click events on the purchase option tile.
|
||||
*
|
||||
* Behavior:
|
||||
* 1. If disabled, prevents any action
|
||||
* 2. Sets this option as the selected purchase option
|
||||
* 3. Resets selected items
|
||||
* 4. Auto-selects all items that have availability and can be added for this option
|
||||
*
|
||||
* @listens click
|
||||
*/
|
||||
@HostListener('click')
|
||||
setPurchaseOptions() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this.store.setPurchaseOption(this.purchaseOption);
|
||||
this.store.resetSelectedItems();
|
||||
asapScheduler.schedule(() => {
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
@apply bg-[#D8DFE5] border-[#0556B4];
|
||||
}
|
||||
|
||||
:host.disabled {
|
||||
@apply opacity-50 cursor-not-allowed bg-gray-100;
|
||||
}
|
||||
|
||||
.purchase-options-tile__heading {
|
||||
@apply flex flex-row justify-center items-center;
|
||||
}
|
||||
|
||||
@@ -1,158 +1,180 @@
|
||||
import { PriceDTO } from '@generated/swagger/availability-api';
|
||||
import { ItemDTO } from '@generated/swagger/cat-search-api';
|
||||
import { AvailabilityDTO, OLAAvailabilityDTO, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
||||
import { GIFT_CARD_TYPE } from '../constants';
|
||||
import {
|
||||
ActionType,
|
||||
Item,
|
||||
ItemData,
|
||||
ItemPayloadWithSourceId,
|
||||
OrderType,
|
||||
PurchaseOption,
|
||||
} from './purchase-options.types';
|
||||
|
||||
export function isItemDTO(item: any, type: ActionType): item is ItemDTO {
|
||||
return type === 'add';
|
||||
}
|
||||
|
||||
export function isItemDTOArray(items: any, type: ActionType): items is ItemDTO[] {
|
||||
return type === 'add';
|
||||
}
|
||||
|
||||
export function isShoppingCartItemDTO(item: any, type: ActionType): item is ShoppingCartItemDTO {
|
||||
return type === 'update';
|
||||
}
|
||||
|
||||
export function isShoppingCartItemDTOArray(items: any, type: ActionType): items is ShoppingCartItemDTO[] {
|
||||
return type === 'update';
|
||||
}
|
||||
|
||||
export function mapToItemData(item: Item, type: ActionType): ItemData {
|
||||
const price: PriceDTO = {};
|
||||
|
||||
if (isItemDTO(item, type)) {
|
||||
price.value = item?.catalogAvailability?.price?.value ?? {};
|
||||
price.vat = item?.catalogAvailability?.price?.vat ?? {};
|
||||
|
||||
return {
|
||||
ean: item.product.ean,
|
||||
itemId: item.id,
|
||||
price,
|
||||
sourceId: item.id,
|
||||
quantity: item.quantity ?? 1,
|
||||
};
|
||||
} else {
|
||||
price.value = item?.unitPrice?.value ?? {};
|
||||
price.vat = item?.unitPrice?.vat ?? {};
|
||||
|
||||
return {
|
||||
ean: item.product.ean,
|
||||
itemId: Number(item.product.catalogProductNumber),
|
||||
price,
|
||||
sourceId: item.id,
|
||||
quantity: item.quantity ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function isDownload(item: Item): boolean {
|
||||
return item.product.format === 'DL' || item.product.format === 'EB';
|
||||
}
|
||||
|
||||
export function isGiftCard(item: Item, type: ActionType): boolean {
|
||||
if (isItemDTO(item, type)) {
|
||||
return item?.type === GIFT_CARD_TYPE;
|
||||
} else {
|
||||
return item?.itemType === GIFT_CARD_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
export function isArchive(item: Item, type: ActionType): boolean {
|
||||
if (isItemDTO(item, type)) {
|
||||
return item?.features?.some((f) => f.key === 'ARC');
|
||||
} else {
|
||||
return !!item?.features?.['ARC'];
|
||||
}
|
||||
}
|
||||
|
||||
export function mapToItemPayload({
|
||||
item,
|
||||
quantity,
|
||||
availability,
|
||||
type,
|
||||
}: {
|
||||
item: ItemDTO | ShoppingCartItemDTO;
|
||||
quantity: number;
|
||||
availability: AvailabilityDTO;
|
||||
type: ActionType;
|
||||
}): ItemPayloadWithSourceId {
|
||||
return {
|
||||
availabilities: [mapToOlaAvailability({ item, quantity, availability, type })],
|
||||
id: String(getCatalogId(item, type)),
|
||||
sourceId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCatalogId(item: ItemDTO | ShoppingCartItemDTO, type: ActionType): number | string {
|
||||
return isItemDTO(item, type) ? item.id : item.product.catalogProductNumber;
|
||||
}
|
||||
|
||||
export function mapToOlaAvailability({
|
||||
availability,
|
||||
item,
|
||||
quantity,
|
||||
type,
|
||||
}: {
|
||||
availability: AvailabilityDTO;
|
||||
item: ItemDTO | ShoppingCartItemDTO;
|
||||
quantity: number;
|
||||
type: ActionType;
|
||||
}): OLAAvailabilityDTO {
|
||||
return {
|
||||
status: availability?.availabilityType,
|
||||
at: availability?.estimatedShippingDate,
|
||||
ean: item?.product?.ean,
|
||||
itemId: Number(getCatalogId(item, type)),
|
||||
format: item?.product?.format,
|
||||
isPrebooked: availability?.isPrebooked,
|
||||
logisticianId: availability?.logistician?.id,
|
||||
price: availability?.price,
|
||||
qty: quantity,
|
||||
ssc: availability?.ssc,
|
||||
sscText: availability?.sscText,
|
||||
supplierId: availability?.supplier?.id,
|
||||
supplierProductNumber: availability?.supplierProductNumber,
|
||||
};
|
||||
}
|
||||
|
||||
export function getOrderTypeForPurchaseOption(purchaseOption: PurchaseOption): OrderType | undefined {
|
||||
switch (purchaseOption) {
|
||||
case 'delivery':
|
||||
case 'dig-delivery':
|
||||
case 'b2b-delivery':
|
||||
return 'Versand';
|
||||
case 'pickup':
|
||||
return 'Abholung';
|
||||
case 'in-store':
|
||||
return 'Rücklage';
|
||||
case 'download':
|
||||
return 'Download';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPurchaseOptionForOrderType(orderType: OrderType): PurchaseOption | undefined {
|
||||
switch (orderType) {
|
||||
case 'Versand':
|
||||
return 'delivery';
|
||||
case 'Abholung':
|
||||
return 'pickup';
|
||||
case 'Rücklage':
|
||||
return 'in-store';
|
||||
case 'Download':
|
||||
return 'download';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
import { PriceDTO } from '@generated/swagger/availability-api';
|
||||
import { ItemDTO } from '@generated/swagger/cat-search-api';
|
||||
import {
|
||||
AvailabilityDTO,
|
||||
OLAAvailabilityDTO,
|
||||
ShoppingCartItemDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
import { GIFT_CARD_TYPE } from '../constants';
|
||||
import {
|
||||
ActionType,
|
||||
Item,
|
||||
ItemData,
|
||||
ItemPayloadWithSourceId,
|
||||
PurchaseOption,
|
||||
} from './purchase-options.types';
|
||||
import { OrderTypeFeature } from '@isa/checkout/data-access';
|
||||
|
||||
export function isItemDTO(item: any, type: ActionType): item is ItemDTO {
|
||||
return type === 'add';
|
||||
}
|
||||
|
||||
export function isItemDTOArray(
|
||||
items: any,
|
||||
type: ActionType,
|
||||
): items is ItemDTO[] {
|
||||
return type === 'add';
|
||||
}
|
||||
|
||||
export function isShoppingCartItemDTO(
|
||||
item: any,
|
||||
type: ActionType,
|
||||
): item is ShoppingCartItemDTO {
|
||||
return type === 'update';
|
||||
}
|
||||
|
||||
export function isShoppingCartItemDTOArray(
|
||||
items: any,
|
||||
type: ActionType,
|
||||
): items is ShoppingCartItemDTO[] {
|
||||
return type === 'update';
|
||||
}
|
||||
|
||||
export function mapToItemData(item: Item, type: ActionType): ItemData {
|
||||
const price: PriceDTO = {};
|
||||
|
||||
if (isItemDTO(item, type)) {
|
||||
price.value = item?.catalogAvailability?.price?.value ?? {};
|
||||
price.vat = item?.catalogAvailability?.price?.vat ?? {};
|
||||
|
||||
return {
|
||||
ean: item.product.ean,
|
||||
itemId: item.id,
|
||||
price,
|
||||
sourceId: item.id,
|
||||
quantity: item.quantity ?? 1,
|
||||
};
|
||||
} else {
|
||||
price.value = item?.unitPrice?.value ?? {};
|
||||
price.vat = item?.unitPrice?.vat ?? {};
|
||||
|
||||
return {
|
||||
ean: item.product.ean,
|
||||
itemId: Number(item.product.catalogProductNumber),
|
||||
price,
|
||||
sourceId: item.id,
|
||||
quantity: item.quantity ?? 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function isDownload(item: Item): boolean {
|
||||
return item.product.format === 'DL' || item.product.format === 'EB';
|
||||
}
|
||||
|
||||
export function isGiftCard(item: Item, type: ActionType): boolean {
|
||||
if (isItemDTO(item, type)) {
|
||||
return item?.type === GIFT_CARD_TYPE;
|
||||
} else {
|
||||
return item?.itemType === GIFT_CARD_TYPE;
|
||||
}
|
||||
}
|
||||
|
||||
export function isArchive(item: Item, type: ActionType): boolean {
|
||||
if (isItemDTO(item, type)) {
|
||||
return item?.features?.some((f) => f.key === 'ARC');
|
||||
} else {
|
||||
return !!item?.features?.['ARC'];
|
||||
}
|
||||
}
|
||||
|
||||
export function mapToItemPayload({
|
||||
item,
|
||||
quantity,
|
||||
availability,
|
||||
type,
|
||||
}: {
|
||||
item: ItemDTO | ShoppingCartItemDTO;
|
||||
quantity: number;
|
||||
availability: AvailabilityDTO;
|
||||
type: ActionType;
|
||||
}): ItemPayloadWithSourceId {
|
||||
return {
|
||||
availabilities: [
|
||||
mapToOlaAvailability({ item, quantity, availability, type }),
|
||||
],
|
||||
id: String(getCatalogId(item, type)),
|
||||
sourceId: item.id,
|
||||
};
|
||||
}
|
||||
|
||||
export function getCatalogId(
|
||||
item: ItemDTO | ShoppingCartItemDTO,
|
||||
type: ActionType,
|
||||
): number | string {
|
||||
return isItemDTO(item, type) ? item.id : item.product.catalogProductNumber;
|
||||
}
|
||||
|
||||
export function mapToOlaAvailability({
|
||||
availability,
|
||||
item,
|
||||
quantity,
|
||||
type,
|
||||
}: {
|
||||
availability: AvailabilityDTO;
|
||||
item: ItemDTO | ShoppingCartItemDTO;
|
||||
quantity: number;
|
||||
type: ActionType;
|
||||
}): OLAAvailabilityDTO {
|
||||
return {
|
||||
status: availability?.availabilityType,
|
||||
at: availability?.estimatedShippingDate,
|
||||
ean: item?.product?.ean,
|
||||
itemId: Number(getCatalogId(item, type)),
|
||||
format: item?.product?.format,
|
||||
isPrebooked: availability?.isPrebooked,
|
||||
logisticianId: availability?.logistician?.id,
|
||||
price: availability?.price,
|
||||
qty: quantity,
|
||||
ssc: availability?.ssc,
|
||||
sscText: availability?.sscText,
|
||||
supplierId: availability?.supplier?.id,
|
||||
supplierProductNumber: availability?.supplierProductNumber,
|
||||
};
|
||||
}
|
||||
|
||||
export function getOrderTypeForPurchaseOption(
|
||||
purchaseOption: PurchaseOption,
|
||||
): OrderTypeFeature | undefined {
|
||||
switch (purchaseOption) {
|
||||
case 'delivery':
|
||||
case 'dig-delivery':
|
||||
case 'b2b-delivery':
|
||||
return 'Versand';
|
||||
case 'pickup':
|
||||
return 'Abholung';
|
||||
case 'in-store':
|
||||
return 'Rücklage';
|
||||
case 'download':
|
||||
return 'Download';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export function getPurchaseOptionForOrderType(
|
||||
orderType: OrderTypeFeature,
|
||||
): PurchaseOption | undefined {
|
||||
switch (orderType) {
|
||||
case 'Versand':
|
||||
return 'delivery';
|
||||
case 'Abholung':
|
||||
return 'pickup';
|
||||
case 'Rücklage':
|
||||
return 'in-store';
|
||||
case 'Download':
|
||||
return 'download';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,178 +1,238 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import {
|
||||
AddToShoppingCartDTO,
|
||||
AvailabilityDTO,
|
||||
EntityDTOContainerOfDestinationDTO,
|
||||
ItemPayload,
|
||||
ItemsResult,
|
||||
ShoppingCartDTO,
|
||||
UpdateShoppingCartItemDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay, take } from 'rxjs/operators';
|
||||
import { Branch, ItemData } from './purchase-options.types';
|
||||
import { memorize } from '@utils/common';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainOmsService } from '@domain/oms';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PurchaseOptionsService {
|
||||
constructor(
|
||||
private _availabilityService: DomainAvailabilityService,
|
||||
private _checkoutService: DomainCheckoutService,
|
||||
private _omsService: DomainOmsService,
|
||||
private _auth: AuthService,
|
||||
private _app: ApplicationService,
|
||||
) {}
|
||||
|
||||
getVats$() {
|
||||
return this._omsService.getVATs();
|
||||
}
|
||||
|
||||
getSelectedBranchForProcess(processId: number): Observable<Branch> {
|
||||
return this._app.getSelectedBranch$(processId).pipe(take(1), shareReplay(1));
|
||||
}
|
||||
|
||||
getCustomerFeatures(processId: number): Observable<Record<string, string>> {
|
||||
return this._checkoutService.getCustomerFeatures({ processId }).pipe(take(1), shareReplay(1));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
fetchDefaultBranch(): Observable<Branch> {
|
||||
return this.getBranch({ branchNumber: this._auth.getClaimByKey('branch_no') }).pipe(take(1), shareReplay(1));
|
||||
}
|
||||
|
||||
fetchPickupAvailability(item: ItemData, quantity: number, branch: Branch): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService
|
||||
.getPickUpAvailability({
|
||||
branch,
|
||||
quantity,
|
||||
item,
|
||||
})
|
||||
.pipe(map((res) => (Array.isArray(res) ? res[0] : undefined)));
|
||||
}
|
||||
|
||||
fetchInStoreAvailability(item: ItemData, quantity: number, branch: Branch): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getTakeAwayAvailability({
|
||||
item,
|
||||
quantity,
|
||||
branch,
|
||||
});
|
||||
}
|
||||
|
||||
fetchDeliveryAvailability(item: ItemData, quantity: number): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
fetchDigDeliveryAvailability(item: ItemData, quantity: number): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getDigDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
fetchB2bDeliveryAvailability(item: ItemData, quantity: number): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getB2bDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
fetchDownloadAvailability(item: ItemData): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getDownloadAvailability({
|
||||
item,
|
||||
});
|
||||
}
|
||||
|
||||
isAvailable(availability: AvailabilityDTO): boolean {
|
||||
return this._availabilityService.isAvailable({ availability });
|
||||
}
|
||||
|
||||
fetchCanAdd(processId: number, orderType: string, payload: ItemPayload[]): Observable<ItemsResult[]> {
|
||||
return this._checkoutService.canAddItems({
|
||||
processId,
|
||||
orderType,
|
||||
payload,
|
||||
});
|
||||
}
|
||||
|
||||
removeItemFromShoppingCart(processId: number, shoppingCartItemId: number): Promise<ShoppingCartDTO> {
|
||||
return this._checkoutService
|
||||
.updateItemInShoppingCart({
|
||||
processId,
|
||||
shoppingCartItemId,
|
||||
update: {
|
||||
availability: null,
|
||||
quantity: 0,
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
getInStoreDestination(branch: Branch): EntityDTOContainerOfDestinationDTO {
|
||||
return {
|
||||
data: { target: 1, targetBranch: { id: branch.id } },
|
||||
};
|
||||
}
|
||||
|
||||
getPickupDestination(branch: Branch): EntityDTOContainerOfDestinationDTO {
|
||||
return {
|
||||
data: { target: 1, targetBranch: { id: branch.id } },
|
||||
};
|
||||
}
|
||||
|
||||
getDeliveryDestination(availability: AvailabilityDTO): EntityDTOContainerOfDestinationDTO {
|
||||
return {
|
||||
data: { target: 2, logistician: availability?.logistician },
|
||||
};
|
||||
}
|
||||
|
||||
getDownloadDestination(availability: AvailabilityDTO): EntityDTOContainerOfDestinationDTO {
|
||||
return {
|
||||
data: { target: 16, logistician: availability?.logistician },
|
||||
};
|
||||
}
|
||||
|
||||
addItemToShoppingCart(processId: number, items: AddToShoppingCartDTO[]) {
|
||||
return this._checkoutService.addItemToShoppingCart({
|
||||
processId,
|
||||
items,
|
||||
});
|
||||
}
|
||||
|
||||
updateItemInShoppingCart(processId: number, shoppingCartItemId: number, payload: UpdateShoppingCartItemDTO) {
|
||||
return this._checkoutService.updateItemInShoppingCart({
|
||||
processId,
|
||||
shoppingCartItemId,
|
||||
update: payload,
|
||||
});
|
||||
}
|
||||
|
||||
@memorize({ comparer: (_) => true })
|
||||
getBranches(): Observable<Branch[]> {
|
||||
return this._availabilityService.getBranches().pipe(
|
||||
map((branches) => {
|
||||
return branches.filter((branch) => branch.isShippingEnabled == true);
|
||||
}),
|
||||
shareReplay(1),
|
||||
);
|
||||
}
|
||||
|
||||
getBranch(params: { id: number }): Observable<Branch>;
|
||||
getBranch(params: { branchNumber: string }): Observable<Branch>;
|
||||
getBranch(params: { id: number; branchNumber: string }): Observable<Branch>;
|
||||
getBranch(params: { id?: number; branchNumber?: string }): Observable<Branch> {
|
||||
return this.getBranches().pipe(
|
||||
map((branches) => {
|
||||
const branch = branches.find((branch) => branch.id == params.id || branch.branchNumber == params.branchNumber);
|
||||
return branch;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import {
|
||||
AddToShoppingCartDTO,
|
||||
AvailabilityDTO,
|
||||
EntityDTOContainerOfDestinationDTO,
|
||||
ItemPayload,
|
||||
ItemsResult,
|
||||
ShoppingCartDTO,
|
||||
UpdateShoppingCartItemDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map, shareReplay, take } from 'rxjs/operators';
|
||||
import { Branch, ItemData } from './purchase-options.types';
|
||||
import { memorize } from '@utils/common';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainOmsService } from '@domain/oms';
|
||||
import {
|
||||
OrderTypeFeature,
|
||||
PurchaseOptionsFacade,
|
||||
} from '@isa/checkout/data-access';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PurchaseOptionsService {
|
||||
#purchaseOptionsFacade = inject(PurchaseOptionsFacade);
|
||||
|
||||
constructor(
|
||||
private _availabilityService: DomainAvailabilityService,
|
||||
private _checkoutService: DomainCheckoutService,
|
||||
private _omsService: DomainOmsService,
|
||||
private _auth: AuthService,
|
||||
) {}
|
||||
|
||||
getVats$() {
|
||||
return this._omsService.getVATs();
|
||||
}
|
||||
|
||||
getCustomerFeatures(processId: number): Observable<Record<string, string>> {
|
||||
return this._checkoutService
|
||||
.getCustomerFeatures({ processId })
|
||||
.pipe(take(1), shareReplay(1));
|
||||
}
|
||||
|
||||
@memorize()
|
||||
fetchDefaultBranch(): Observable<Branch> {
|
||||
return this.getBranch({
|
||||
branchNumber: this._auth.getClaimByKey('branch_no'),
|
||||
}).pipe(take(1), shareReplay(1));
|
||||
}
|
||||
|
||||
fetchPickupAvailability(
|
||||
item: ItemData,
|
||||
quantity: number,
|
||||
branch: Branch,
|
||||
): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService
|
||||
.getPickUpAvailability({
|
||||
branch,
|
||||
quantity,
|
||||
item,
|
||||
})
|
||||
.pipe(map((res) => (Array.isArray(res) ? res[0] : undefined)));
|
||||
}
|
||||
|
||||
fetchInStoreAvailability(
|
||||
item: ItemData,
|
||||
quantity: number,
|
||||
branch: Branch,
|
||||
): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getTakeAwayAvailability({
|
||||
item,
|
||||
quantity,
|
||||
branch,
|
||||
});
|
||||
}
|
||||
|
||||
fetchDeliveryAvailability(
|
||||
item: ItemData,
|
||||
quantity: number,
|
||||
): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
fetchDigDeliveryAvailability(
|
||||
item: ItemData,
|
||||
quantity: number,
|
||||
): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getDigDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
fetchB2bDeliveryAvailability(
|
||||
item: ItemData,
|
||||
quantity: number,
|
||||
): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getB2bDeliveryAvailability({
|
||||
item,
|
||||
quantity,
|
||||
});
|
||||
}
|
||||
|
||||
fetchDownloadAvailability(item: ItemData): Observable<AvailabilityDTO> {
|
||||
return this._availabilityService.getDownloadAvailability({
|
||||
item,
|
||||
});
|
||||
}
|
||||
|
||||
isAvailable(availability: AvailabilityDTO): boolean {
|
||||
return this._availabilityService.isAvailable({ availability });
|
||||
}
|
||||
|
||||
fetchCanAdd(
|
||||
shoppingCartId: number,
|
||||
orderType: OrderTypeFeature,
|
||||
payload: ItemPayload[],
|
||||
customerFeatures: Record<string, string>,
|
||||
): Promise<ItemsResult[]> {
|
||||
return this.#purchaseOptionsFacade.canAddItems({
|
||||
shoppingCartId,
|
||||
payload: payload.map((p) => ({
|
||||
...p,
|
||||
customerFeatures: customerFeatures,
|
||||
orderType: orderType,
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
removeItemFromShoppingCart(
|
||||
shoppingCartId: number,
|
||||
shoppingCartItemId: number,
|
||||
): Promise<ShoppingCartDTO> {
|
||||
const shoppingCart = this.#purchaseOptionsFacade.removeItem({
|
||||
shoppingCartId,
|
||||
shoppingCartItemId,
|
||||
});
|
||||
|
||||
return shoppingCart;
|
||||
}
|
||||
|
||||
getInStoreDestination(branch: Branch): EntityDTOContainerOfDestinationDTO {
|
||||
return {
|
||||
data: { target: 1, targetBranch: { id: branch.id } },
|
||||
};
|
||||
}
|
||||
|
||||
getPickupDestination(branch: Branch): EntityDTOContainerOfDestinationDTO {
|
||||
return {
|
||||
data: { target: 1, targetBranch: { id: branch.id } },
|
||||
};
|
||||
}
|
||||
|
||||
getDeliveryDestination(
|
||||
availability: AvailabilityDTO,
|
||||
): EntityDTOContainerOfDestinationDTO {
|
||||
return {
|
||||
data: { target: 2, logistician: availability?.logistician },
|
||||
};
|
||||
}
|
||||
|
||||
getDownloadDestination(
|
||||
availability: AvailabilityDTO,
|
||||
): EntityDTOContainerOfDestinationDTO {
|
||||
return {
|
||||
data: { target: 16, logistician: availability?.logistician },
|
||||
};
|
||||
}
|
||||
|
||||
async addItemToShoppingCart(
|
||||
shoppingCartId: number,
|
||||
items: AddToShoppingCartDTO[],
|
||||
) {
|
||||
const shoppingCart = await this.#purchaseOptionsFacade.addItem({
|
||||
shoppingCartId,
|
||||
items,
|
||||
});
|
||||
console.log('added item to cart', { shoppingCart });
|
||||
// FIX BUILD ERRORS
|
||||
// this._checkoutService.updateProcessCount(
|
||||
// this._app.activatedProcessId,
|
||||
// shoppingCart,
|
||||
// );
|
||||
return shoppingCart;
|
||||
}
|
||||
|
||||
async updateItemInShoppingCart(
|
||||
shoppingCartId: number,
|
||||
shoppingCartItemId: number,
|
||||
payload: UpdateShoppingCartItemDTO,
|
||||
) {
|
||||
const shoppingCart = await this.#purchaseOptionsFacade.updateItem({
|
||||
shoppingCartId,
|
||||
shoppingCartItemId,
|
||||
values: payload,
|
||||
});
|
||||
console.log('updated item in cart', { shoppingCart });
|
||||
// FIX BUILD ERRORS
|
||||
// this._checkoutService.updateProcessCount(
|
||||
// this._app.activatedProcessId,
|
||||
// shoppingCart,
|
||||
// );
|
||||
}
|
||||
|
||||
@memorize({ comparer: (_) => true })
|
||||
getBranches(): Observable<Branch[]> {
|
||||
return this._availabilityService.getBranches().pipe(
|
||||
map((branches) => {
|
||||
return branches.filter((branch) => branch.isShippingEnabled == true);
|
||||
}),
|
||||
shareReplay(1),
|
||||
);
|
||||
}
|
||||
|
||||
getBranch(params: { id: number }): Observable<Branch>;
|
||||
getBranch(params: { branchNumber: string }): Observable<Branch>;
|
||||
getBranch(params: { id: number; branchNumber: string }): Observable<Branch>;
|
||||
getBranch(params: {
|
||||
id?: number;
|
||||
branchNumber?: string;
|
||||
}): Observable<Branch> {
|
||||
return this.getBranches().pipe(
|
||||
map((branches) => {
|
||||
const branch = branches.find(
|
||||
(branch) =>
|
||||
branch.id == params.id ||
|
||||
branch.branchNumber == params.branchNumber,
|
||||
);
|
||||
return branch;
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +1,42 @@
|
||||
import { PriceDTO } from '@generated/swagger/checkout-api';
|
||||
import {
|
||||
ActionType,
|
||||
Availability,
|
||||
Branch,
|
||||
CanAdd,
|
||||
FetchingAvailability,
|
||||
Item,
|
||||
PurchaseOption,
|
||||
} from './purchase-options.types';
|
||||
|
||||
export interface PurchaseOptionsState {
|
||||
type: ActionType;
|
||||
|
||||
processId: number;
|
||||
|
||||
items: Item[];
|
||||
|
||||
availabilities: Availability[];
|
||||
|
||||
canAddResults: CanAdd[];
|
||||
|
||||
purchaseOption: PurchaseOption;
|
||||
|
||||
selectedItemIds: number[];
|
||||
|
||||
prices: { [itemId: number]: PriceDTO };
|
||||
|
||||
defaultBranch: Branch;
|
||||
|
||||
pickupBranch: Branch;
|
||||
|
||||
inStoreBranch: Branch;
|
||||
|
||||
customerFeatures: Record<string, string>;
|
||||
|
||||
fetchingAvailabilities: Array<FetchingAvailability>;
|
||||
}
|
||||
import { PriceDTO } from '@generated/swagger/checkout-api';
|
||||
import {
|
||||
ActionType,
|
||||
Availability,
|
||||
Branch,
|
||||
CanAdd,
|
||||
FetchingAvailability,
|
||||
Item,
|
||||
PurchaseOption,
|
||||
} from './purchase-options.types';
|
||||
|
||||
export interface PurchaseOptionsState {
|
||||
shoppingCartId: number;
|
||||
|
||||
type: ActionType;
|
||||
|
||||
items: Item[];
|
||||
|
||||
availabilities: Availability[];
|
||||
|
||||
canAddResults: CanAdd[];
|
||||
|
||||
purchaseOption: PurchaseOption;
|
||||
|
||||
selectedItemIds: number[];
|
||||
|
||||
prices: { [itemId: number]: PriceDTO };
|
||||
|
||||
defaultBranch: Branch;
|
||||
|
||||
pickupBranch: Branch;
|
||||
|
||||
inStoreBranch: Branch;
|
||||
|
||||
customerFeatures: Record<string, string>;
|
||||
|
||||
fetchingAvailabilities: Array<FetchingAvailability>;
|
||||
|
||||
useRedemptionPoints: boolean;
|
||||
|
||||
disabledPurchaseOptions: PurchaseOption[];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,73 @@
|
||||
import { ItemData as AvailabilityItemData } from '@domain/availability';
|
||||
import { ItemDTO } from '@generated/swagger/cat-search-api';
|
||||
import { AvailabilityDTO, BranchDTO, ItemPayload, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
export type ActionType = 'add' | 'update';
|
||||
|
||||
export type PurchaseOption =
|
||||
| 'delivery'
|
||||
| 'dig-delivery'
|
||||
| 'b2b-delivery'
|
||||
| 'pickup'
|
||||
| 'in-store'
|
||||
| 'download'
|
||||
| 'catalog';
|
||||
|
||||
export type OrderType = 'Rücklage' | 'Abholung' | 'Versand' | 'Download';
|
||||
|
||||
export type ItemDTOWithQuantity = ItemDTO & { quantity?: number };
|
||||
|
||||
export type Item = ItemDTOWithQuantity | ShoppingCartItemDTO;
|
||||
|
||||
export type Branch = BranchDTO;
|
||||
|
||||
export type Availability = {
|
||||
itemId: number;
|
||||
purchaseOption: PurchaseOption;
|
||||
data: AvailabilityDTO & { priceMaintained?: boolean; orderDeadline?: string; firstDayOfSale?: string };
|
||||
ean?: string;
|
||||
};
|
||||
|
||||
export type ItemData = AvailabilityItemData & { sourceId: number; quantity: number };
|
||||
|
||||
export type ItemPayloadWithSourceId = ItemPayload & { sourceId: number };
|
||||
|
||||
export type CanAdd = { itemId: number; purchaseOption: PurchaseOption; canAdd: boolean; message?: string };
|
||||
|
||||
export type FetchingAvailability = { id: string; itemId: number; purchaseOption?: PurchaseOption };
|
||||
import { ItemData as AvailabilityItemData } from '@domain/availability';
|
||||
import { ItemDTO } from '@generated/swagger/cat-search-api';
|
||||
import {
|
||||
AvailabilityDTO,
|
||||
BranchDTO,
|
||||
ItemPayload,
|
||||
ShoppingCartItemDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
|
||||
/**
|
||||
* Action type for the purchase options modal.
|
||||
* - 'add': Adding new items to the shopping cart
|
||||
* - 'update': Updating existing items in the shopping cart
|
||||
*/
|
||||
export type ActionType = 'add' | 'update';
|
||||
|
||||
/**
|
||||
* Available purchase options for item delivery/fulfillment.
|
||||
*
|
||||
* Each option represents a different way customers can receive their items:
|
||||
* - `delivery`: Standard home/address delivery
|
||||
* - `dig-delivery`: Digital delivery (special handling)
|
||||
* - `b2b-delivery`: Business-to-business delivery
|
||||
* - `pickup`: Pickup at a branch location
|
||||
* - `in-store`: Reserve and collect in store
|
||||
* - `download`: Digital download (e-books, digital content)
|
||||
* - `catalog`: Catalog availability (reference only)
|
||||
*/
|
||||
export type PurchaseOption =
|
||||
| 'delivery'
|
||||
| 'dig-delivery'
|
||||
| 'b2b-delivery'
|
||||
| 'pickup'
|
||||
| 'in-store'
|
||||
| 'download'
|
||||
| 'catalog';
|
||||
|
||||
export type ItemDTOWithQuantity = ItemDTO & { quantity?: number };
|
||||
|
||||
export type Item = ItemDTOWithQuantity | ShoppingCartItemDTO;
|
||||
|
||||
export type Branch = BranchDTO;
|
||||
|
||||
export type Availability = {
|
||||
itemId: number;
|
||||
purchaseOption: PurchaseOption;
|
||||
data: AvailabilityDTO & {
|
||||
priceMaintained?: boolean;
|
||||
orderDeadline?: string;
|
||||
firstDayOfSale?: string;
|
||||
};
|
||||
ean?: string;
|
||||
};
|
||||
|
||||
export type ItemData = AvailabilityItemData & {
|
||||
sourceId: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export type ItemPayloadWithSourceId = ItemPayload & { sourceId: number };
|
||||
|
||||
export type CanAdd = {
|
||||
itemId: number;
|
||||
purchaseOption: PurchaseOption;
|
||||
canAdd: boolean;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export type FetchingAvailability = {
|
||||
id: string;
|
||||
itemId: number;
|
||||
purchaseOption?: PurchaseOption;
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,47 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ArticleDetailsComponent } from './article-details.component';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { UiStarsModule } from '@ui/stars';
|
||||
import { UiSliderModule } from '@ui/slider';
|
||||
import { ArticleRecommendationsComponent } from './recommendations/article-recommendations.component';
|
||||
import { PipesModule } from '../shared/pipes/pipes.module';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { ArticleDetailsTextComponent } from './article-details-text/article-details-text.component';
|
||||
import { IconBadgeComponent } from 'apps/isa-app/src/shared/components/icon/badge/icon-badge.component';
|
||||
import { MatomoModule } from 'ngx-matomo-client';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ProductImageModule,
|
||||
UiIconModule,
|
||||
RouterModule,
|
||||
UiStarsModule,
|
||||
UiSliderModule,
|
||||
UiCommonModule,
|
||||
UiTooltipModule,
|
||||
IconModule,
|
||||
PipesModule,
|
||||
OrderDeadlinePipeModule,
|
||||
ArticleDetailsTextComponent,
|
||||
IconBadgeComponent,
|
||||
MatomoModule,
|
||||
],
|
||||
exports: [ArticleDetailsComponent, ArticleRecommendationsComponent],
|
||||
declarations: [ArticleDetailsComponent, ArticleRecommendationsComponent],
|
||||
})
|
||||
export class ArticleDetailsModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ArticleDetailsComponent } from './article-details.component';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { UiIconModule } from '@ui/icon';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { UiStarsModule } from '@ui/stars';
|
||||
import { UiSliderModule } from '@ui/slider';
|
||||
import { ArticleRecommendationsComponent } from './recommendations/article-recommendations.component';
|
||||
import { PipesModule } from '../shared/pipes/pipes.module';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { OrderDeadlinePipeModule } from '@shared/pipes/order-deadline';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { ArticleDetailsTextComponent } from './article-details-text/article-details-text.component';
|
||||
import { IconBadgeComponent } from 'apps/isa-app/src/shared/components/icon/badge/icon-badge.component';
|
||||
import { MatomoModule } from 'ngx-matomo-client';
|
||||
import {
|
||||
RewardSelectionService,
|
||||
RewardSelectionPopUpService,
|
||||
} from '@isa/checkout/shared/reward-selection-dialog';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
ProductImageModule,
|
||||
UiIconModule,
|
||||
RouterModule,
|
||||
UiStarsModule,
|
||||
UiSliderModule,
|
||||
UiCommonModule,
|
||||
UiTooltipModule,
|
||||
IconModule,
|
||||
PipesModule,
|
||||
OrderDeadlinePipeModule,
|
||||
ArticleDetailsTextComponent,
|
||||
IconBadgeComponent,
|
||||
MatomoModule,
|
||||
],
|
||||
exports: [ArticleDetailsComponent, ArticleRecommendationsComponent],
|
||||
declarations: [ArticleDetailsComponent, ArticleRecommendationsComponent],
|
||||
providers: [
|
||||
RewardSelectionService,
|
||||
RewardSelectionPopUpService,
|
||||
],
|
||||
})
|
||||
export class ArticleDetailsModule {}
|
||||
|
||||
@@ -22,12 +22,21 @@ import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { CacheService } from '@core/cache';
|
||||
import { isEqual } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
|
||||
import { debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
|
||||
import {
|
||||
debounceTime,
|
||||
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';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { Filter, FilterInputGroupMainComponent } from '@shared/components/filter';
|
||||
import {
|
||||
Filter,
|
||||
FilterInputGroupMainComponent,
|
||||
} from '@shared/components/filter';
|
||||
import { DomainAvailabilityService, ItemData } from '@domain/availability';
|
||||
import { asapScheduler } from 'rxjs';
|
||||
import { ShellService } from '@shared/shell';
|
||||
@@ -39,8 +48,11 @@ import { ShellService } from '@shared/shell';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
@ViewChildren(SearchResultItemComponent) listItems: QueryList<SearchResultItemComponent>;
|
||||
export class ArticleSearchResultsComponent
|
||||
implements OnInit, OnDestroy, AfterViewInit
|
||||
{
|
||||
@ViewChildren(SearchResultItemComponent)
|
||||
listItems: QueryList<SearchResultItemComponent>;
|
||||
@ViewChild(CdkVirtualScrollViewport, { static: false })
|
||||
scrollContainer: CdkVirtualScrollViewport;
|
||||
|
||||
@@ -59,7 +71,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
|
||||
selectedItems$ = combineLatest([this.results$, this.selectedItemIds$]).pipe(
|
||||
map(([items, selectedItemIds]) => {
|
||||
return items?.filter((item) => selectedItemIds?.find((selectedItemId) => item.id === selectedItemId));
|
||||
return items?.filter((item) =>
|
||||
selectedItemIds?.find((selectedItemId) => item.id === selectedItemId),
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -81,7 +95,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
return this._environment.matchDesktopLarge();
|
||||
}
|
||||
|
||||
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
|
||||
hasFilter$ = combineLatest([
|
||||
this.searchService.filter$,
|
||||
this.searchService.defaultSettings$,
|
||||
]).pipe(
|
||||
map(([filter, defaultFilter]) => {
|
||||
const filterQueryParams = filter?.getQueryParams();
|
||||
return !isEqual(
|
||||
@@ -100,11 +117,15 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
get filterQueryParams() {
|
||||
return this.cleanupQueryParams(this.searchService?.filter?.getQueryParams());
|
||||
return this.cleanupQueryParams(
|
||||
this.searchService?.filter?.getQueryParams(),
|
||||
);
|
||||
}
|
||||
|
||||
get primaryOutletActive$() {
|
||||
return this._environment.matchDesktop$.pipe(map((matches) => matches && this.route.outlet === 'primary'));
|
||||
return this._environment.matchDesktop$.pipe(
|
||||
map((matches) => matches && this.route.outlet === 'primary'),
|
||||
);
|
||||
}
|
||||
|
||||
private readonly SCROLL_INDEX_TOKEN = 'CATALOG_RESULTS_LIST_SCROLL_INDEX';
|
||||
@@ -129,28 +150,42 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
|
||||
ngOnInit() {
|
||||
this.subscriptions.add(
|
||||
combineLatest([this.application.activatedProcessId$, this.route.queryParams])
|
||||
combineLatest([
|
||||
this.application.activatedProcessId$,
|
||||
this.route.queryParams,
|
||||
])
|
||||
.pipe(
|
||||
debounceTime(0),
|
||||
switchMap(([processId, queryParams]) =>
|
||||
this.application
|
||||
.getSelectedBranch$(processId)
|
||||
.pipe(map((selectedBranch) => ({ processId, queryParams, selectedBranch }))),
|
||||
this.application.getSelectedBranch$(processId).pipe(
|
||||
map((selectedBranch) => ({
|
||||
processId,
|
||||
queryParams,
|
||||
selectedBranch,
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.subscribe(async ({ processId, queryParams, selectedBranch }) => {
|
||||
const processChanged = processId !== this.searchService.processId;
|
||||
|
||||
const branchChanged = selectedBranch?.id !== this.searchService?.selectedBranch?.id;
|
||||
const branchChanged =
|
||||
selectedBranch?.id !== this.searchService?.selectedBranch?.id;
|
||||
|
||||
if (processChanged) {
|
||||
if (!!this.searchService.processId && this.searchService.filter instanceof Filter) {
|
||||
if (
|
||||
!!this.searchService.processId &&
|
||||
this.searchService.filter instanceof Filter
|
||||
) {
|
||||
this.cacheCurrentData(
|
||||
this.searchService.processId,
|
||||
this.searchService.filter.getQueryParams(),
|
||||
this.searchService?.selectedBranch?.id,
|
||||
);
|
||||
this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
this.updateBreadcrumbs(
|
||||
this.searchService.processId,
|
||||
this.searchService.filter.getQueryParams(),
|
||||
);
|
||||
}
|
||||
this.searchService.setProcess(processId);
|
||||
}
|
||||
@@ -169,9 +204,20 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
this.scrollToItem(await this._getScrollIndexFromCache());
|
||||
}
|
||||
|
||||
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
|
||||
if (
|
||||
!isEqual(
|
||||
cleanQueryParams,
|
||||
this.cleanupQueryParams(
|
||||
this.searchService.filter.getQueryParams(),
|
||||
),
|
||||
)
|
||||
) {
|
||||
await this.searchService.setDefaultFilter(queryParams);
|
||||
const data = await this.getCachedData(processId, queryParams, selectedBranch?.id);
|
||||
const data = await this.getCachedData(
|
||||
processId,
|
||||
queryParams,
|
||||
selectedBranch?.id,
|
||||
);
|
||||
|
||||
if (data.items?.length > 0) {
|
||||
this.searchService.setItems(data.items);
|
||||
@@ -179,21 +225,29 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
if (
|
||||
data.items?.length === 0 &&
|
||||
this.route?.parent?.children?.find((childRoute) => childRoute?.outlet === 'side')?.snapshot?.routeConfig
|
||||
?.path !== 'filter'
|
||||
this.route?.parent?.children?.find(
|
||||
(childRoute) => childRoute?.outlet === 'side',
|
||||
)?.snapshot?.routeConfig?.path !== 'filter'
|
||||
) {
|
||||
this.search({ clear: true });
|
||||
} else {
|
||||
const selectedItemIds: Array<string> = queryParams?.selected_item_ids?.split(',') ?? [];
|
||||
const selectedItemIds: Array<string> =
|
||||
queryParams?.selected_item_ids?.split(',') ?? [];
|
||||
for (const id of selectedItemIds) {
|
||||
if (id) {
|
||||
this.searchService.setSelected({ selected: true, itemId: Number(id) });
|
||||
this.searchService.setSelected({
|
||||
selected: true,
|
||||
itemId: Number(id),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const process = await this.application.getProcessById$(processId).pipe(first()).toPromise();
|
||||
const process = await this.application
|
||||
.getProcessById$(processId)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
if (process) {
|
||||
await this.updateBreadcrumbs(processId, queryParams);
|
||||
await this.createBreadcrumb(processId, queryParams);
|
||||
@@ -240,7 +294,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
})
|
||||
.navigate();
|
||||
}
|
||||
} else if (searchCompleted?.clear || this.route.outlet === 'primary') {
|
||||
} else if (
|
||||
searchCompleted?.clear ||
|
||||
this.route.outlet === 'primary'
|
||||
) {
|
||||
const ean = this.route?.snapshot?.params?.ean;
|
||||
|
||||
if (ean) {
|
||||
@@ -253,7 +310,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
.navigate();
|
||||
} else {
|
||||
await this._navigationService
|
||||
.getArticleSearchResultsPath(processId, { queryParams: params })
|
||||
.getArticleSearchResultsPath(processId, {
|
||||
queryParams: params,
|
||||
})
|
||||
.navigate();
|
||||
}
|
||||
}
|
||||
@@ -266,7 +325,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
this.searchService.searchStarted.subscribe(async (options) => {
|
||||
if (!options?.clear) {
|
||||
const queryParams = {
|
||||
...this.cleanupQueryParams(this.searchService.filter.getQueryParams()),
|
||||
...this.cleanupQueryParams(
|
||||
this.searchService.filter.getQueryParams(),
|
||||
),
|
||||
main_qs: this.sharedFilterInputGroupMain?.uiInput?.value,
|
||||
};
|
||||
|
||||
@@ -281,11 +342,19 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
private _addScrollIndexToCache(index: number): void {
|
||||
this.cache.set<number>({ processId: this.getProcessId(), token: this.SCROLL_INDEX_TOKEN }, index);
|
||||
this.cache.set<number>(
|
||||
{ processId: this.getProcessId(), token: this.SCROLL_INDEX_TOKEN },
|
||||
index,
|
||||
);
|
||||
}
|
||||
|
||||
private async _getScrollIndexFromCache(): Promise<number> {
|
||||
return (await this.cache.get<number>({ processId: this.getProcessId(), token: this.SCROLL_INDEX_TOKEN })) ?? 0;
|
||||
return (
|
||||
(await this.cache.get<number>({
|
||||
processId: this.getProcessId(),
|
||||
token: this.SCROLL_INDEX_TOKEN,
|
||||
})) ?? 0
|
||||
);
|
||||
}
|
||||
|
||||
async scrollToItem(i?: number) {
|
||||
@@ -303,12 +372,14 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
scrolledIndexChange(index: number) {
|
||||
const completeListFetched = this.searchService.items.length === this.searchService.hits;
|
||||
const completeListFetched =
|
||||
this.searchService.items.length === this.searchService.hits;
|
||||
|
||||
if (
|
||||
index &&
|
||||
!completeListFetched &&
|
||||
this.searchService.items.length <= this.scrollContainer?.getRenderedRange()?.end
|
||||
this.searchService.items.length <=
|
||||
this.scrollContainer?.getRenderedRange()?.end
|
||||
) {
|
||||
this.search({ clear: false });
|
||||
}
|
||||
@@ -326,7 +397,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
this.searchService.filter.getQueryParams(),
|
||||
this.searchService?.selectedBranch?.id,
|
||||
);
|
||||
await this.updateBreadcrumbs(this.searchService.processId, this.searchService.filter.getQueryParams());
|
||||
await this.updateBreadcrumbs(
|
||||
this.searchService.processId,
|
||||
this.searchService.filter.getQueryParams(),
|
||||
);
|
||||
|
||||
this.unselectAll();
|
||||
}
|
||||
@@ -345,7 +419,15 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
return clean;
|
||||
}
|
||||
|
||||
search({ filter, clear = false, orderBy = false }: { filter?: Filter; clear?: boolean; orderBy?: boolean }) {
|
||||
search({
|
||||
filter,
|
||||
clear = false,
|
||||
orderBy = false,
|
||||
}: {
|
||||
filter?: Filter;
|
||||
clear?: boolean;
|
||||
orderBy?: boolean;
|
||||
}) {
|
||||
if (filter) {
|
||||
this.sharedFilterInputGroupMain.cancelAutocomplete();
|
||||
}
|
||||
@@ -354,19 +436,28 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
getDetailsPath(itemId: number) {
|
||||
return this._navigationService.getArticleDetailsPath({ processId: this.application.activatedProcessId, itemId })
|
||||
.path;
|
||||
return this._navigationService.getArticleDetailsPath({
|
||||
processId: this.application.activatedProcessId,
|
||||
itemId,
|
||||
}).path;
|
||||
}
|
||||
|
||||
async updateBreadcrumbs(
|
||||
processId: number = this.searchService.processId,
|
||||
queryParams: Record<string, string> = this.searchService.filter?.getQueryParams(),
|
||||
queryParams: Record<
|
||||
string,
|
||||
string
|
||||
> = this.searchService.filter?.getQueryParams(),
|
||||
) {
|
||||
const selected_item_ids = this.searchService?.selectedItemIds?.toString();
|
||||
|
||||
if (queryParams) {
|
||||
const crumbs = await this.breadcrumb
|
||||
.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'filter', 'results'])
|
||||
.getBreadcrumbsByKeyAndTags$(processId, [
|
||||
'catalog',
|
||||
'filter',
|
||||
'results',
|
||||
])
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
|
||||
@@ -382,13 +473,18 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
}
|
||||
|
||||
async createBreadcrumb(processId: number, queryParams: Record<string, string>) {
|
||||
async createBreadcrumb(
|
||||
processId: number,
|
||||
queryParams: Record<string, string>,
|
||||
) {
|
||||
if (queryParams) {
|
||||
const name = queryParams.main_qs ? queryParams.main_qs : 'Alle Artikel';
|
||||
await this.breadcrumb.addBreadcrumbIfNotExists({
|
||||
key: processId,
|
||||
name,
|
||||
path: this._navigationService.getArticleSearchResultsPath(processId, { queryParams }).path,
|
||||
path: this._navigationService.getArticleSearchResultsPath(processId, {
|
||||
queryParams,
|
||||
}).path,
|
||||
params: queryParams,
|
||||
section: 'customer',
|
||||
tags: ['catalog', 'filter', 'results'],
|
||||
@@ -405,8 +501,16 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
crumbs?.forEach((crumb) => this.breadcrumb.removeBreadcrumb(crumb.id));
|
||||
}
|
||||
|
||||
cacheCurrentData(processId: number, params: Record<string, string> = {}, branchId: number) {
|
||||
const qparams = this.cleanupQueryParams({ ...params, processId: String(processId), branchId: String(branchId) });
|
||||
cacheCurrentData(
|
||||
processId: number,
|
||||
params: Record<string, string> = {},
|
||||
branchId: number,
|
||||
) {
|
||||
const qparams = this.cleanupQueryParams({
|
||||
...params,
|
||||
processId: String(processId),
|
||||
branchId: String(branchId),
|
||||
});
|
||||
|
||||
this.cache.set(qparams, {
|
||||
items: this.searchService.items,
|
||||
@@ -414,8 +518,16 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
});
|
||||
}
|
||||
|
||||
async getCachedData(processId: number, params: Record<string, string> = {}, branchId: number) {
|
||||
const qparams = this.cleanupQueryParams({ ...params, processId: String(processId), branchId: String(branchId) });
|
||||
async getCachedData(
|
||||
processId: number,
|
||||
params: Record<string, string> = {},
|
||||
branchId: number,
|
||||
) {
|
||||
const qparams = this.cleanupQueryParams({
|
||||
...params,
|
||||
processId: String(processId),
|
||||
branchId: String(branchId),
|
||||
});
|
||||
const cacheData = await this.cache.get<{
|
||||
items: ItemDTO[];
|
||||
hits: number;
|
||||
@@ -452,7 +564,12 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
|
||||
unselectAll() {
|
||||
this.listItems.forEach((listItem) => this.searchService.setSelected({ selected: false, itemId: listItem.item.id }));
|
||||
this.listItems.forEach((listItem) =>
|
||||
this.searchService.setSelected({
|
||||
selected: false,
|
||||
itemId: listItem.item.id,
|
||||
}),
|
||||
);
|
||||
this.searchService.patchState({ selectedItemIds: [] });
|
||||
}
|
||||
|
||||
@@ -474,39 +591,46 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
availability: {
|
||||
availabilityType: item?.catalogAvailability?.status,
|
||||
price: item?.catalogAvailability?.price,
|
||||
supplierProductNumber: item?.ids?.dig ? String(item?.ids?.dig) : item?.product?.supplierProductNumber,
|
||||
supplierProductNumber: item?.ids?.dig
|
||||
? String(item?.ids?.dig)
|
||||
: item?.product?.supplierProductNumber,
|
||||
},
|
||||
product: {
|
||||
catalogProductNumber: String(item?.id),
|
||||
...item?.product,
|
||||
},
|
||||
itemType: item?.type,
|
||||
promotion: { points: item?.promoPoints },
|
||||
promotion: { value: item?.promoPoints },
|
||||
};
|
||||
}
|
||||
|
||||
async addItemsToCart(item?: ItemDTO) {
|
||||
const selectedItems = !item ? await this.selectedItems$.pipe(first()).toPromise() : [item];
|
||||
const selectedItems = !item
|
||||
? await this.selectedItems$.pipe(first()).toPromise()
|
||||
: [item];
|
||||
const items: AddToShoppingCartDTO[] = [];
|
||||
|
||||
const canAddItemsPayload = [];
|
||||
|
||||
for (const item of selectedItems) {
|
||||
const isDownload = item?.product?.format === 'EB' || item?.product?.format === 'DL';
|
||||
const isDownload =
|
||||
item?.product?.format === 'EB' || item?.product?.format === 'DL';
|
||||
const price = item?.catalogAvailability?.price;
|
||||
const shoppingCartItem: AddToShoppingCartDTO = {
|
||||
quantity: 1,
|
||||
availability: {
|
||||
availabilityType: item?.catalogAvailability?.status,
|
||||
price,
|
||||
supplierProductNumber: item?.ids?.dig ? String(item.ids?.dig) : item?.product?.supplierProductNumber,
|
||||
supplierProductNumber: item?.ids?.dig
|
||||
? String(item.ids?.dig)
|
||||
: item?.product?.supplierProductNumber,
|
||||
},
|
||||
product: {
|
||||
catalogProductNumber: String(item?.id),
|
||||
...item?.product,
|
||||
},
|
||||
itemType: item.type,
|
||||
promotion: { points: item?.promoPoints },
|
||||
promotion: { value: item?.promoPoints },
|
||||
};
|
||||
|
||||
if (isDownload) {
|
||||
@@ -519,9 +643,14 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
.getDownloadAvailability({ item: downloadItem })
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
shoppingCartItem.destination = { data: { target: 16, logistician: downloadAvailability?.logistician } };
|
||||
shoppingCartItem.destination = {
|
||||
data: { target: 16, logistician: downloadAvailability?.logistician },
|
||||
};
|
||||
if (downloadAvailability) {
|
||||
shoppingCartItem.availability = { ...shoppingCartItem.availability, ...downloadAvailability };
|
||||
shoppingCartItem.availability = {
|
||||
...shoppingCartItem.availability,
|
||||
...downloadAvailability,
|
||||
};
|
||||
}
|
||||
canAddItemsPayload.push({
|
||||
availabilities: [{ ...item.catalogAvailability, format: 'DL' }],
|
||||
@@ -546,7 +675,10 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
if (response) {
|
||||
const cantAdd = (response as any)?.filter((r) => r.status >= 2);
|
||||
if (cantAdd?.length > 0) {
|
||||
this.openModal({ itemLength: cantAdd.length, canAddMessage: cantAdd[0].message });
|
||||
this.openModal({
|
||||
itemLength: cantAdd.length,
|
||||
canAddMessage: cantAdd[0].message,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -571,7 +703,15 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
|
||||
}
|
||||
}
|
||||
|
||||
openModal({ itemLength, canAddMessage, error }: { itemLength: number; canAddMessage?: string; error?: Error }) {
|
||||
openModal({
|
||||
itemLength,
|
||||
canAddMessage,
|
||||
error,
|
||||
}: {
|
||||
itemLength: number;
|
||||
canAddMessage?: string;
|
||||
error?: Error;
|
||||
}) {
|
||||
const modal = this._uiModal.open({
|
||||
title:
|
||||
!error && !canAddMessage
|
||||
|
||||
@@ -114,15 +114,21 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
|
||||
readonly processId$ = this._application.activatedProcessId$;
|
||||
|
||||
readonly customer$ = this.processId$.pipe(switchMap((processId) => this._checkoutService.getBuyer({ processId })));
|
||||
readonly customer$ = this.processId$.pipe(
|
||||
switchMap((processId) => this._checkoutService.getBuyer({ processId })),
|
||||
);
|
||||
|
||||
readonly customerFeatures$ = this.processId$.pipe(
|
||||
switchMap((processId) => this._checkoutService.getCustomerFeatures({ processId })),
|
||||
switchMap((processId) =>
|
||||
this._checkoutService.getCustomerFeatures({ processId }),
|
||||
),
|
||||
);
|
||||
|
||||
readonly customerFilter$ = this.customerFeatures$.pipe(
|
||||
withLatestFrom(this.processId$),
|
||||
switchMap(([customerFeatures, processId]) => this._checkoutService.canSetCustomer({ processId, customerFeatures })),
|
||||
switchMap(([customerFeatures, processId]) =>
|
||||
this._checkoutService.canSetCustomer({ processId, customerFeatures }),
|
||||
),
|
||||
map((res) => res.filter),
|
||||
);
|
||||
|
||||
@@ -169,7 +175,11 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
tapResponse(
|
||||
(res) => {
|
||||
const item = res.result[0];
|
||||
if (!!item && item?.product?.format !== 'EB' && item?.product?.format !== 'DL') {
|
||||
if (
|
||||
!!item &&
|
||||
item?.product?.format !== 'EB' &&
|
||||
item?.product?.format !== 'DL'
|
||||
) {
|
||||
this.patchState({
|
||||
item: res.result[0],
|
||||
message: '',
|
||||
@@ -229,12 +239,22 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
updateCart = this.effect((cb$: Observable<Function>) =>
|
||||
cb$.pipe(
|
||||
tap((_) => this.patchState({ fetching: true })),
|
||||
withLatestFrom(this.processId$, this.addToCartItem$, this.shoppingCartItemId$),
|
||||
withLatestFrom(
|
||||
this.processId$,
|
||||
this.addToCartItem$,
|
||||
this.shoppingCartItemId$,
|
||||
),
|
||||
switchMap(([cb, processId, newItem, shoppingCartItemId]) => {
|
||||
const availability = newItem.availability;
|
||||
const quantity = newItem.quantity;
|
||||
const destination = newItem.destination;
|
||||
return this.updateCartRequest({ processId, shoppingCartItemId, availability, quantity, destination }).pipe(
|
||||
return this.updateCartRequest({
|
||||
processId,
|
||||
shoppingCartItemId,
|
||||
availability,
|
||||
quantity,
|
||||
destination,
|
||||
}).pipe(
|
||||
tapResponse(
|
||||
(res) => {
|
||||
this.patchState({
|
||||
@@ -270,7 +290,10 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
}
|
||||
|
||||
addToCartRequest(processId: number, newItem: AddToShoppingCartDTO) {
|
||||
return this._checkoutService.addItemToShoppingCart({ processId, items: [newItem] });
|
||||
return this._checkoutService.addItemToShoppingCart({
|
||||
processId,
|
||||
items: [newItem],
|
||||
});
|
||||
}
|
||||
|
||||
updateCartRequest({
|
||||
@@ -297,7 +320,11 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
});
|
||||
}
|
||||
|
||||
async createAddToCartItem(control: UntypedFormGroup, branch: BranchDTO, update?: boolean) {
|
||||
async createAddToCartItem(
|
||||
control: UntypedFormGroup,
|
||||
branch: BranchDTO,
|
||||
update?: boolean,
|
||||
) {
|
||||
let item: ItemDTO;
|
||||
const quantity = Number(control.get('quantity').value);
|
||||
const price = this._createPriceDTO(control);
|
||||
@@ -305,7 +332,11 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
// Check if item exists or ean inside the control changed in the meantime
|
||||
if (!!this.item && this.item.product.ean === control.get('ean').value) {
|
||||
item = this.item;
|
||||
promoPoints = await this._getPromoPoints({ itemId: item.id, quantity, price: price.value.value });
|
||||
promoPoints = await this._getPromoPoints({
|
||||
itemId: item.id,
|
||||
quantity,
|
||||
price: price.value.value,
|
||||
});
|
||||
} else {
|
||||
item = undefined;
|
||||
}
|
||||
@@ -316,21 +347,33 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
quantity,
|
||||
availability,
|
||||
product,
|
||||
promotion: item ? { points: promoPoints } : undefined,
|
||||
promotion: item ? { value: promoPoints } : undefined,
|
||||
destination: {
|
||||
data: { target: 1, targetBranch: { id: branch?.id } },
|
||||
},
|
||||
itemType: this.item?.type ?? this.get((s) => s.shoppingCartItem)?.itemType,
|
||||
itemType:
|
||||
this.item?.type ?? this.get((s) => s.shoppingCartItem)?.itemType,
|
||||
};
|
||||
|
||||
if (update) {
|
||||
const existingItem = this.get((s) => s.shoppingCartItem);
|
||||
this.patchState({ addToCartItem: newItem, shoppingCartItemId: existingItem?.id });
|
||||
this.patchState({
|
||||
addToCartItem: newItem,
|
||||
shoppingCartItemId: existingItem?.id,
|
||||
});
|
||||
}
|
||||
this.patchState({ addToCartItem: newItem });
|
||||
}
|
||||
|
||||
private async _getPromoPoints({ itemId, quantity, price }: { itemId: number; quantity: number; price: number }) {
|
||||
private async _getPromoPoints({
|
||||
itemId,
|
||||
quantity,
|
||||
price,
|
||||
}: {
|
||||
itemId: number;
|
||||
quantity: number;
|
||||
price: number;
|
||||
}) {
|
||||
let points: number;
|
||||
try {
|
||||
points = await this._catalogService
|
||||
@@ -371,7 +414,13 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
};
|
||||
}
|
||||
|
||||
private _createAvailabilityDTO({ price, control }: { price: PriceDTO; control: UntypedFormGroup }): AvailabilityDTO {
|
||||
private _createAvailabilityDTO({
|
||||
price,
|
||||
control,
|
||||
}: {
|
||||
price: PriceDTO;
|
||||
control: UntypedFormGroup;
|
||||
}): AvailabilityDTO {
|
||||
return {
|
||||
availabilityType: 1024,
|
||||
supplier: {
|
||||
@@ -383,7 +432,13 @@ export class CheckoutDummyStore extends ComponentStore<CheckoutDummyState> {
|
||||
};
|
||||
}
|
||||
|
||||
private _createProductDTO({ item, control }: { item?: ItemDTO; control: UntypedFormGroup }): ProductDTO {
|
||||
private _createProductDTO({
|
||||
item,
|
||||
control,
|
||||
}: {
|
||||
item?: ItemDTO;
|
||||
control: UntypedFormGroup;
|
||||
}): ProductDTO {
|
||||
const formValues: Partial<ProductDTO> = {
|
||||
ean: control.get('ean').value,
|
||||
name: control.get('name').value,
|
||||
|
||||
@@ -13,8 +13,12 @@
|
||||
keinen Artikel hinzugefügt.
|
||||
</p>
|
||||
<div class="btn-wrapper">
|
||||
<a class="cta-primary" [routerLink]="productSearchBasePath">Artikel suchen</a>
|
||||
<button class="cta-secondary" (click)="openDummyModal({})">Neuanlage</button>
|
||||
<a class="cta-primary" [routerLink]="productSearchBasePath"
|
||||
>Artikel suchen</a
|
||||
>
|
||||
<button class="cta-secondary" (click)="openDummyModal({})">
|
||||
Neuanlage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -24,11 +28,22 @@
|
||||
<div class="cta-print-wrapper">
|
||||
<button class="cta-print" (click)="openPrintModal()">Drucken</button>
|
||||
</div>
|
||||
<h1 class="header">Warenkorb</h1>
|
||||
<div class="header-container">
|
||||
<h1 class="header">Warenkorb</h1>
|
||||
@if (orderTypesExist$ | async) {
|
||||
<lib-reward-selection-trigger
|
||||
class="pb-2 desktop-large:pb-0"
|
||||
></lib-reward-selection-trigger>
|
||||
}
|
||||
</div>
|
||||
@if (!(isDesktop$ | async)) {
|
||||
<page-checkout-review-details></page-checkout-review-details>
|
||||
}
|
||||
@for (group of groupedItems$ | async; track trackByGroupedItems($index, group); let lastGroup = $last) {
|
||||
@for (
|
||||
group of groupedItems$ | async;
|
||||
track trackByGroupedItems($index, group);
|
||||
let lastGroup = $last
|
||||
) {
|
||||
@if (group?.orderType !== undefined) {
|
||||
<hr />
|
||||
<div class="row item-group-header bg-[#F5F7FA]">
|
||||
@@ -40,20 +55,31 @@
|
||||
></shared-icon>
|
||||
}
|
||||
<div class="label" [class.dummy]="group.orderType === 'Dummy'">
|
||||
{{ group.orderType !== 'Dummy' ? group.orderType : 'Manuelle Anlage / Dummy Bestellung' }}
|
||||
{{
|
||||
group.orderType !== 'Dummy'
|
||||
? group.orderType
|
||||
: 'Manuelle Anlage / Dummy Bestellung'
|
||||
}}
|
||||
@if (group.orderType === 'Dummy') {
|
||||
<button
|
||||
class="text-brand border-none font-bold text-p1 outline-none pl-4"
|
||||
(click)="openDummyModal({ changeDataFromCart: true })"
|
||||
>
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="grow"></div>
|
||||
@if (group.orderType !== 'Download' && group.orderType !== 'Dummy') {
|
||||
@if (
|
||||
group.orderType !== 'Download' && group.orderType !== 'Dummy'
|
||||
) {
|
||||
<div class="pl-4">
|
||||
<button class="cta-edit" (click)="showPurchasingListModal(group.items)">Ändern</button>
|
||||
<button
|
||||
class="cta-edit"
|
||||
(click)="showPurchasingListModal(group.items)"
|
||||
>
|
||||
Ändern
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -62,20 +88,44 @@
|
||||
group.orderType === 'Versand' ||
|
||||
group.orderType === 'B2B-Versand' ||
|
||||
group.orderType === 'DIG-Versand'
|
||||
) {
|
||||
<hr
|
||||
/>
|
||||
) {
|
||||
<hr />
|
||||
}
|
||||
}
|
||||
@for (item of group.items; track trackByItemId(i, item); let lastItem = $last; let i = $index) {
|
||||
@if (group?.orderType !== undefined && (item.features?.orderType === 'Abholung' || item.features?.orderType === 'Rücklage')) {
|
||||
@for (
|
||||
item of group.items;
|
||||
track trackByItemId(i, item);
|
||||
let lastItem = $last;
|
||||
let i = $index
|
||||
) {
|
||||
@if (
|
||||
group?.orderType !== undefined &&
|
||||
(item.features?.orderType === 'Abholung' ||
|
||||
item.features?.orderType === 'Rücklage')
|
||||
) {
|
||||
@if (item?.destination?.data?.targetBranch?.data; as targetBranch) {
|
||||
@if (i === 0 || checkIfMultipleDestinationsForOrderTypeExist(targetBranch, group, i)) {
|
||||
@if (
|
||||
i === 0 ||
|
||||
checkIfMultipleDestinationsForOrderTypeExist(
|
||||
targetBranch,
|
||||
group,
|
||||
i
|
||||
)
|
||||
) {
|
||||
<div
|
||||
class="flex flex-row items-center px-5 pt-0 pb-[0.875rem] -mt-2 bg-[#F5F7FA]"
|
||||
[class.multiple-destinations]="checkIfMultipleDestinationsForOrderTypeExist(targetBranch, group, i)"
|
||||
[class.multiple-destinations]="
|
||||
checkIfMultipleDestinationsForOrderTypeExist(
|
||||
targetBranch,
|
||||
group,
|
||||
i
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="branch-name"
|
||||
>{{ targetBranch?.name }} |
|
||||
{{ targetBranch | branchAddress }}</span
|
||||
>
|
||||
<span class="branch-name">{{ targetBranch?.name }} | {{ targetBranch | branchAddress }}</span>
|
||||
</div>
|
||||
<hr />
|
||||
}
|
||||
@@ -85,7 +135,9 @@
|
||||
(changeItem)="changeItem($event)"
|
||||
(changeDummyItem)="changeDummyItem($event)"
|
||||
(changeQuantity)="updateItemQuantity($event)"
|
||||
[quantityError]="(quantityError$ | async)[item.product.catalogProductNumber]"
|
||||
[quantityError]="
|
||||
(quantityError$ | async)[item.product.catalogProductNumber]
|
||||
"
|
||||
[item]="item"
|
||||
[orderType]="group?.orderType"
|
||||
[loadingOnItemChangeById]="loadingOnItemChangeById$ | async"
|
||||
@@ -109,7 +161,11 @@
|
||||
}
|
||||
<div class="flex flex-col w-full">
|
||||
<strong class="total-value">
|
||||
Zwischensumme {{ shoppingCart?.total?.value | currency: shoppingCart?.total?.currency : 'code' }}
|
||||
Zwischensumme
|
||||
{{
|
||||
shoppingCart?.total?.value
|
||||
| currency: shoppingCart?.total?.currency : 'code'
|
||||
}}
|
||||
</strong>
|
||||
<span class="shipping-cost-info">ohne Versandkosten</span>
|
||||
</div>
|
||||
@@ -119,11 +175,13 @@
|
||||
(click)="order()"
|
||||
[disabled]="
|
||||
showOrderButtonSpinner ||
|
||||
((primaryCtaLabel$ | async) === 'Bestellen' && !(checkNotificationChannelControl$ | async)) ||
|
||||
((primaryCtaLabel$ | async) === 'Bestellen' &&
|
||||
!(checkNotificationChannelControl$ | async)) ||
|
||||
notificationsControl?.invalid ||
|
||||
((primaryCtaLabel$ | async) === 'Bestellen' && ((checkingOla$ | async) || (checkoutIsInValid$ | async)))
|
||||
((primaryCtaLabel$ | async) === 'Bestellen' &&
|
||||
((checkingOla$ | async) || (checkoutIsInValid$ | async)))
|
||||
"
|
||||
>
|
||||
>
|
||||
<ui-spinner [show]="showOrderButtonSpinner">
|
||||
{{ primaryCtaLabel$ | async }}
|
||||
</ui-spinner>
|
||||
@@ -137,4 +195,3 @@
|
||||
<ui-spinner [show]="true"></ui-spinner>
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@@ -72,8 +72,12 @@ button {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
@apply flex flex-col items-center justify-center desktop-large:pb-10 -mt-2;
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply text-center text-h2 desktop-large:pb-10 -mt-2;
|
||||
@apply text-center text-h2;
|
||||
}
|
||||
|
||||
hr {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,7 +19,11 @@ import { CheckoutReviewDetailsComponent } from './details/checkout-review-detail
|
||||
import { CheckoutReviewStore } from './checkout-review.store';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { LoaderComponent, SkeletonLoaderComponent } from '@shared/components/loader';
|
||||
import {
|
||||
LoaderComponent,
|
||||
SkeletonLoaderComponent,
|
||||
} from '@shared/components/loader';
|
||||
import { RewardSelectionTriggerComponent } from '@isa/checkout/shared/reward-selection-dialog';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -40,6 +44,7 @@ import { LoaderComponent, SkeletonLoaderComponent } from '@shared/components/loa
|
||||
TextFieldModule,
|
||||
LoaderComponent,
|
||||
SkeletonLoaderComponent,
|
||||
RewardSelectionTriggerComponent,
|
||||
],
|
||||
exports: [CheckoutReviewComponent, CheckoutReviewDetailsComponent],
|
||||
declarations: [
|
||||
|
||||
@@ -1,185 +1,237 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import { NotificationChannel, PayerDTO, ShoppingCartDTO, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { first, map, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
export interface CheckoutReviewState {
|
||||
payer: PayerDTO;
|
||||
shoppingCart: ShoppingCartDTO;
|
||||
shoppingCartItems: ShoppingCartItemDTO[];
|
||||
fetching: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CheckoutReviewStore extends ComponentStore<CheckoutReviewState> {
|
||||
orderCompleted = new Subject<void>();
|
||||
|
||||
get shoppingCart() {
|
||||
return this.get((s) => s.shoppingCart);
|
||||
}
|
||||
set shoppingCart(shoppingCart: ShoppingCartDTO) {
|
||||
this.patchState({ shoppingCart });
|
||||
}
|
||||
readonly shoppingCart$ = this.select((s) => s.shoppingCart);
|
||||
|
||||
get shoppingCartItems() {
|
||||
return this.get((s) => s.shoppingCartItems);
|
||||
}
|
||||
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
|
||||
this.patchState({ shoppingCartItems });
|
||||
}
|
||||
readonly shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
|
||||
|
||||
get fetching() {
|
||||
return this.get((s) => s.fetching);
|
||||
}
|
||||
set fetching(fetching: boolean) {
|
||||
this.patchState({ fetching });
|
||||
}
|
||||
readonly fetching$ = this.select((s) => s.fetching);
|
||||
|
||||
customerFeatures$ = this._application.activatedProcessId$.pipe(
|
||||
takeUntil(this.orderCompleted),
|
||||
switchMap((processId) => this._domainCheckoutService.getCustomerFeatures({ processId })),
|
||||
);
|
||||
|
||||
payer$ = this._application.activatedProcessId$.pipe(
|
||||
takeUntil(this.orderCompleted),
|
||||
switchMap((processId) => this._domainCheckoutService.getPayer({ processId })),
|
||||
);
|
||||
|
||||
buyer$ = this._application.activatedProcessId$.pipe(
|
||||
takeUntil(this.orderCompleted),
|
||||
switchMap((processId) => this._domainCheckoutService.getBuyer({ processId })),
|
||||
);
|
||||
|
||||
showBillingAddress$ = this.shoppingCartItems$.pipe(
|
||||
withLatestFrom(this.customerFeatures$),
|
||||
map(
|
||||
([items, customerFeatures]) =>
|
||||
items.some(
|
||||
(item) =>
|
||||
item.features?.orderType === 'Versand' ||
|
||||
item.features?.orderType === 'B2B-Versand' ||
|
||||
item.features?.orderType === 'DIG-Versand',
|
||||
) || !!customerFeatures?.b2b,
|
||||
),
|
||||
);
|
||||
|
||||
checkNotificationChannelControl$ = new BehaviorSubject<boolean>(true);
|
||||
|
||||
notificationChannelLoading$ = new Subject<boolean>();
|
||||
|
||||
notificationsControl: UntypedFormGroup;
|
||||
|
||||
constructor(
|
||||
private _domainCheckoutService: DomainCheckoutService,
|
||||
private _application: ApplicationService,
|
||||
private _uiModal: UiModalService,
|
||||
) {
|
||||
super({ payer: undefined, shoppingCart: undefined, shoppingCartItems: [], fetching: false });
|
||||
}
|
||||
|
||||
loadShoppingCart = this.effect(($) =>
|
||||
$.pipe(
|
||||
tap(() => (this.fetching = true)),
|
||||
withLatestFrom(this._application.activatedProcessId$),
|
||||
switchMap(([_, processId]) => {
|
||||
return this._domainCheckoutService.getShoppingCart({ processId, latest: true }).pipe(
|
||||
tapResponse(
|
||||
(shoppingCart) => {
|
||||
const shoppingCartItems = shoppingCart?.items?.map((item) => item.data) || [];
|
||||
this.patchState({
|
||||
shoppingCart,
|
||||
shoppingCartItems,
|
||||
});
|
||||
},
|
||||
(err) => {},
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
}),
|
||||
tap(() => (this.fetching = false)),
|
||||
),
|
||||
);
|
||||
|
||||
async onNotificationChange(notificationChannels?: NotificationChannel[]) {
|
||||
this.notificationChannelLoading$.next(true);
|
||||
|
||||
try {
|
||||
const control = this.notificationsControl?.getRawValue();
|
||||
const notificationChannel = notificationChannels
|
||||
? (notificationChannels.reduce((val, current) => val | current, 0) as NotificationChannel)
|
||||
: control?.notificationChannel?.selected || 0;
|
||||
const processId = await this._application.activatedProcessId$.pipe(first()).toPromise();
|
||||
const email = control?.notificationChannel?.email;
|
||||
const mobile = control?.notificationChannel?.mobile;
|
||||
|
||||
// Check if E-Mail and Mobilnumber is available if E-Mail or SMS checkbox is active
|
||||
if (notificationChannel === 3 && (!email || !mobile)) {
|
||||
this.checkNotificationChannelControl$.next(false);
|
||||
} else if (notificationChannel === 2 && !mobile) {
|
||||
this.checkNotificationChannelControl$.next(false);
|
||||
} else if (notificationChannel === 1 && !email) {
|
||||
this.checkNotificationChannelControl$.next(false);
|
||||
} else {
|
||||
this.checkNotificationChannelControl$.next(true);
|
||||
}
|
||||
|
||||
// NotificationChannel nur speichern, wenn Haken und Value gesetzt
|
||||
let setNotificationChannel = 0;
|
||||
if ((notificationChannel & 1) === 1 && email) {
|
||||
setNotificationChannel += 1;
|
||||
}
|
||||
if ((notificationChannel & 2) === 2 && mobile) {
|
||||
setNotificationChannel += 2;
|
||||
}
|
||||
|
||||
if (notificationChannel > 0) {
|
||||
this.setCommunicationDetails({ processId, notificationChannel, email, mobile });
|
||||
}
|
||||
this._domainCheckoutService.setNotificationChannels({
|
||||
processId,
|
||||
notificationChannels: (setNotificationChannel as NotificationChannel) || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
this._uiModal.open({
|
||||
content: UiErrorModalComponent,
|
||||
data: error,
|
||||
title: 'Fehler beim setzen des Benachrichtigungskanals',
|
||||
});
|
||||
}
|
||||
|
||||
this.notificationChannelLoading$.next(false);
|
||||
}
|
||||
|
||||
setCommunicationDetails({
|
||||
processId,
|
||||
notificationChannel,
|
||||
email,
|
||||
mobile,
|
||||
}: {
|
||||
processId: number;
|
||||
notificationChannel: number;
|
||||
email: string;
|
||||
mobile: string;
|
||||
}) {
|
||||
const emailValid = this.notificationsControl?.get('notificationChannel')?.get('email')?.valid;
|
||||
const mobileValid = this.notificationsControl?.get('notificationChannel')?.get('mobile')?.valid;
|
||||
|
||||
if (notificationChannel === 3 && emailValid && mobileValid) {
|
||||
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, email, mobile });
|
||||
} else if (notificationChannel === 1 && emailValid) {
|
||||
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, email });
|
||||
} else if (notificationChannel === 2 && mobileValid) {
|
||||
this._domainCheckoutService.setBuyerCommunicationDetails({ processId, mobile });
|
||||
}
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { UntypedFormGroup } from '@angular/forms';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import {
|
||||
NotificationChannel,
|
||||
PayerDTO,
|
||||
ShoppingCartDTO,
|
||||
ShoppingCartItemDTO,
|
||||
} from '@generated/swagger/checkout-api';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import {
|
||||
first,
|
||||
map,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
withLatestFrom,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
export interface CheckoutReviewState {
|
||||
payer: PayerDTO;
|
||||
shoppingCart: ShoppingCartDTO;
|
||||
shoppingCartItems: ShoppingCartItemDTO[];
|
||||
fetching: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class CheckoutReviewStore extends ComponentStore<CheckoutReviewState> {
|
||||
orderCompleted = new Subject<void>();
|
||||
|
||||
get shoppingCart() {
|
||||
return this.get((s) => s.shoppingCart);
|
||||
}
|
||||
set shoppingCart(shoppingCart: ShoppingCartDTO) {
|
||||
this.patchState({ shoppingCart });
|
||||
}
|
||||
readonly shoppingCart$ = this.select((s) => s.shoppingCart);
|
||||
|
||||
get shoppingCartItems() {
|
||||
return this.get((s) => s.shoppingCartItems);
|
||||
}
|
||||
set shoppingCartItems(shoppingCartItems: ShoppingCartItemDTO[]) {
|
||||
this.patchState({ shoppingCartItems });
|
||||
}
|
||||
readonly shoppingCartItems$ = this.select((s) => s.shoppingCartItems);
|
||||
|
||||
get fetching() {
|
||||
return this.get((s) => s.fetching);
|
||||
}
|
||||
set fetching(fetching: boolean) {
|
||||
this.patchState({ fetching });
|
||||
}
|
||||
readonly fetching$ = this.select((s) => s.fetching);
|
||||
|
||||
customerFeatures$ = this._application.activatedProcessId$.pipe(
|
||||
takeUntil(this.orderCompleted),
|
||||
switchMap((processId) =>
|
||||
this._domainCheckoutService.getCustomerFeatures({ processId }),
|
||||
),
|
||||
);
|
||||
|
||||
payer$ = this._application.activatedProcessId$.pipe(
|
||||
takeUntil(this.orderCompleted),
|
||||
switchMap((processId) =>
|
||||
this._domainCheckoutService.getPayer({ processId }),
|
||||
),
|
||||
);
|
||||
|
||||
buyer$ = this._application.activatedProcessId$.pipe(
|
||||
takeUntil(this.orderCompleted),
|
||||
switchMap((processId) =>
|
||||
this._domainCheckoutService.getBuyer({ processId }),
|
||||
),
|
||||
);
|
||||
|
||||
showBillingAddress$ = this.shoppingCartItems$.pipe(
|
||||
withLatestFrom(this.customerFeatures$),
|
||||
map(
|
||||
([items, customerFeatures]) =>
|
||||
items.some(
|
||||
(item) =>
|
||||
item.features?.orderType === 'Versand' ||
|
||||
item.features?.orderType === 'B2B-Versand' ||
|
||||
item.features?.orderType === 'DIG-Versand',
|
||||
) || !!customerFeatures?.b2b,
|
||||
),
|
||||
);
|
||||
|
||||
checkNotificationChannelControl$ = new BehaviorSubject<boolean>(true);
|
||||
|
||||
notificationChannelLoading$ = new Subject<boolean>();
|
||||
|
||||
notificationsControl: UntypedFormGroup;
|
||||
|
||||
constructor(
|
||||
private _domainCheckoutService: DomainCheckoutService,
|
||||
private _application: ApplicationService,
|
||||
private _uiModal: UiModalService,
|
||||
) {
|
||||
super({
|
||||
payer: undefined,
|
||||
shoppingCart: undefined,
|
||||
shoppingCartItems: [],
|
||||
fetching: false,
|
||||
});
|
||||
}
|
||||
|
||||
loadShoppingCart = this.effect(($) =>
|
||||
$.pipe(
|
||||
tap(() => (this.fetching = true)),
|
||||
withLatestFrom(this._application.activatedProcessId$),
|
||||
switchMap(([_, processId]) => {
|
||||
return this._domainCheckoutService
|
||||
.getShoppingCart({ processId, latest: true })
|
||||
.pipe(
|
||||
tapResponse(
|
||||
(shoppingCart) => {
|
||||
console.log('Loaded shopping cart', { shoppingCart });
|
||||
const shoppingCartItems =
|
||||
shoppingCart?.items?.map((item) => item.data) || [];
|
||||
this.patchState({
|
||||
shoppingCart,
|
||||
shoppingCartItems,
|
||||
});
|
||||
},
|
||||
(err) => {},
|
||||
() => {},
|
||||
),
|
||||
);
|
||||
}),
|
||||
tap(() => (this.fetching = false)),
|
||||
),
|
||||
);
|
||||
|
||||
async onNotificationChange(notificationChannels?: NotificationChannel[]) {
|
||||
this.notificationChannelLoading$.next(true);
|
||||
|
||||
try {
|
||||
const control = this.notificationsControl?.getRawValue();
|
||||
const notificationChannel = notificationChannels
|
||||
? (notificationChannels.reduce(
|
||||
(val, current) => val | current,
|
||||
0,
|
||||
) as NotificationChannel)
|
||||
: control?.notificationChannel?.selected || 0;
|
||||
const processId = await this._application.activatedProcessId$
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
const email = control?.notificationChannel?.email;
|
||||
const mobile = control?.notificationChannel?.mobile;
|
||||
|
||||
// Check if E-Mail and Mobilnumber is available if E-Mail or SMS checkbox is active
|
||||
if (notificationChannel === 3 && (!email || !mobile)) {
|
||||
this.checkNotificationChannelControl$.next(false);
|
||||
} else if (notificationChannel === 2 && !mobile) {
|
||||
this.checkNotificationChannelControl$.next(false);
|
||||
} else if (notificationChannel === 1 && !email) {
|
||||
this.checkNotificationChannelControl$.next(false);
|
||||
} else {
|
||||
this.checkNotificationChannelControl$.next(true);
|
||||
}
|
||||
|
||||
// NotificationChannel nur speichern, wenn Haken und Value gesetzt
|
||||
let setNotificationChannel = 0;
|
||||
if ((notificationChannel & 1) === 1 && email) {
|
||||
setNotificationChannel += 1;
|
||||
}
|
||||
if ((notificationChannel & 2) === 2 && mobile) {
|
||||
setNotificationChannel += 2;
|
||||
}
|
||||
|
||||
if (notificationChannel > 0) {
|
||||
this.setCommunicationDetails({
|
||||
processId,
|
||||
notificationChannel,
|
||||
email,
|
||||
mobile,
|
||||
});
|
||||
}
|
||||
this._domainCheckoutService.setNotificationChannels({
|
||||
processId,
|
||||
notificationChannels:
|
||||
(setNotificationChannel as NotificationChannel) || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
this._uiModal.open({
|
||||
content: UiErrorModalComponent,
|
||||
data: error,
|
||||
title: 'Fehler beim setzen des Benachrichtigungskanals',
|
||||
});
|
||||
}
|
||||
|
||||
this.notificationChannelLoading$.next(false);
|
||||
}
|
||||
|
||||
setCommunicationDetails({
|
||||
processId,
|
||||
notificationChannel,
|
||||
email,
|
||||
mobile,
|
||||
}: {
|
||||
processId: number;
|
||||
notificationChannel: number;
|
||||
email: string;
|
||||
mobile: string;
|
||||
}) {
|
||||
const emailValid = this.notificationsControl
|
||||
?.get('notificationChannel')
|
||||
?.get('email')?.valid;
|
||||
const mobileValid = this.notificationsControl
|
||||
?.get('notificationChannel')
|
||||
?.get('mobile')?.valid;
|
||||
|
||||
if (notificationChannel === 3 && emailValid && mobileValid) {
|
||||
this._domainCheckoutService.setBuyerCommunicationDetails({
|
||||
processId,
|
||||
email,
|
||||
mobile,
|
||||
});
|
||||
} else if (notificationChannel === 1 && emailValid) {
|
||||
this._domainCheckoutService.setBuyerCommunicationDetails({
|
||||
processId,
|
||||
email,
|
||||
});
|
||||
} else if (notificationChannel === 2 && mobileValid) {
|
||||
this._domainCheckoutService.setBuyerCommunicationDetails({
|
||||
processId,
|
||||
mobile,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,129 +1,169 @@
|
||||
<div class="item-thumbnail">
|
||||
<a [routerLink]="productSearchDetailsPath" [queryParams]="{ main_qs: item?.product?.ean }">
|
||||
@if (item?.product?.ean | productImage; as thumbnailUrl) {
|
||||
<img loading="lazy" [src]="thumbnailUrl" [alt]="item?.product?.name" />
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="item-contributors">
|
||||
@for (contributor of contributors$ | async; track contributor; let last = $last) {
|
||||
<a
|
||||
[routerLink]="productSearchResultsPath"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
(click)="$event?.stopPropagation()"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="item-title font-bold text-h2 mb-4"
|
||||
[class.text-h3]="item?.product?.name?.length >= 40 && isTablet"
|
||||
[class.text-p1]="item?.product?.name?.length >= 50 || !isTablet"
|
||||
[class.text-p2]="item?.product?.name?.length >= 60 && isTablet"
|
||||
[class.text-p3]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
<a [routerLink]="productSearchDetailsPath" [queryParams]="{ main_qs: item?.product?.ean }">{{ item?.product?.name }}</a>
|
||||
</div>
|
||||
|
||||
@if (item?.product?.format && item?.product?.formatDetail) {
|
||||
<div class="item-format">
|
||||
@if (item?.product?.format !== '--') {
|
||||
<img
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
}
|
||||
{{ item?.product?.formatDetail }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="item-info text-p2">
|
||||
<div class="mb-1">{{ item?.product?.manufacturer | substr: 25 }} | {{ item?.product?.ean }}</div>
|
||||
<div class="mb-1">
|
||||
{{ item?.product?.volume }}
|
||||
@if (item?.product?.volume && item?.product?.publicationDate) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ item?.product?.publicationDate | date }}
|
||||
</div>
|
||||
@if (notAvailable$ | async) {
|
||||
<div>
|
||||
<span class="text-brand item-date">Nicht verfügbar</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (refreshingAvailabilit$ | async) {
|
||||
<shared-skeleton-loader class="w-40"></shared-skeleton-loader>
|
||||
} @else {
|
||||
@if (orderType === 'Abholung') {
|
||||
<div class="item-date" [class.availability-changed]="estimatedShippingDateChanged$ | async">
|
||||
Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
|
||||
</div>
|
||||
}
|
||||
@if (orderType === 'Versand' || orderType === 'B2B-Versand' || orderType === 'DIG-Versand') {
|
||||
<div
|
||||
class="item-date"
|
||||
[class.availability-changed]="estimatedShippingDateChanged$ | async"
|
||||
>
|
||||
@if (item?.availability?.estimatedDelivery) {
|
||||
Zustellung zwischen {{ (item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }}
|
||||
und
|
||||
{{ (item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
|
||||
} @else {
|
||||
Versand {{ item?.availability?.estimatedShippingDate | date }}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@if (olaError$ | async) {
|
||||
<div class="item-availability-message">Artikel nicht verfügbar</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="item-price-stock flex flex-col">
|
||||
<div class="text-p2 font-bold">{{ item?.availability?.price?.value?.value | currency: 'EUR' : 'code' }}</div>
|
||||
<div class="text-p2 font-normal">
|
||||
@if (!(isDummy$ | async)) {
|
||||
<ui-quantity-dropdown
|
||||
[ngModel]="item?.quantity"
|
||||
(ngModelChange)="onChangeQuantity($event)"
|
||||
[showSpinner]="(loadingOnQuantityChangeById$ | async) === item?.id"
|
||||
[disabled]="(loadingOnItemChangeById$ | async) === item?.id"
|
||||
[range]="quantityRange$ | async"
|
||||
></ui-quantity-dropdown>
|
||||
} @else {
|
||||
<div class="mt-2">{{ item?.quantity }}x</div>
|
||||
}
|
||||
</div>
|
||||
@if (quantityError) {
|
||||
<div class="quantity-error">
|
||||
{{ quantityError }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (orderType !== 'Download') {
|
||||
<div class="actions">
|
||||
@if (!(hasOrderType$ | async)) {
|
||||
<button
|
||||
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
|
||||
(click)="onChangeItem()"
|
||||
>
|
||||
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">Lieferweg auswählen</ui-spinner>
|
||||
</button>
|
||||
}
|
||||
@if (canEdit$ | async) {
|
||||
<button
|
||||
[disabled]="(loadingOnQuantityChangeById$ | async) === item?.id || (loadingOnItemChangeById$ | async) === item?.id"
|
||||
(click)="onChangeItem()"
|
||||
>
|
||||
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id">Lieferweg ändern</ui-spinner>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="item-thumbnail">
|
||||
<a
|
||||
[routerLink]="productSearchDetailsPath"
|
||||
[queryParams]="{ main_qs: item?.product?.ean }"
|
||||
>
|
||||
@if (item?.product?.ean | productImage; as thumbnailUrl) {
|
||||
<img loading="lazy" [src]="thumbnailUrl" [alt]="item?.product?.name" />
|
||||
}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="item-contributors">
|
||||
@for (
|
||||
contributor of contributors$ | async;
|
||||
track contributor;
|
||||
let last = $last
|
||||
) {
|
||||
<a
|
||||
[routerLink]="productSearchResultsPath"
|
||||
[queryParams]="{ main_qs: contributor, main_author: 'author' }"
|
||||
(click)="$event?.stopPropagation()"
|
||||
>
|
||||
{{ contributor }}{{ last ? '' : ';' }}
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="item-title font-bold text-h2 mb-4"
|
||||
[class.text-h3]="item?.product?.name?.length >= 40 && isTablet"
|
||||
[class.text-p1]="item?.product?.name?.length >= 50 || !isTablet"
|
||||
[class.text-p2]="item?.product?.name?.length >= 60 && isTablet"
|
||||
[class.text-p3]="item?.product?.name?.length >= 100"
|
||||
>
|
||||
<a
|
||||
[routerLink]="productSearchDetailsPath"
|
||||
[queryParams]="{ main_qs: item?.product?.ean }"
|
||||
>{{ item?.product?.name }}</a
|
||||
>
|
||||
</div>
|
||||
|
||||
@if (item?.product?.format && item?.product?.formatDetail) {
|
||||
<div class="item-format">
|
||||
@if (item?.product?.format !== '--') {
|
||||
<img
|
||||
src="assets/images/Icon_{{ item?.product?.format }}.svg"
|
||||
[alt]="item?.product?.formatDetail"
|
||||
/>
|
||||
}
|
||||
{{ item?.product?.formatDetail }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="item-info text-p2">
|
||||
<div class="mb-1">
|
||||
{{ item?.product?.manufacturer | substr: 25 }} | {{ item?.product?.ean }}
|
||||
</div>
|
||||
<div class="mb-1">
|
||||
{{ item?.product?.volume }}
|
||||
@if (item?.product?.volume && item?.product?.publicationDate) {
|
||||
<span>|</span>
|
||||
}
|
||||
{{ item?.product?.publicationDate | date }}
|
||||
</div>
|
||||
@if (notAvailable$ | async) {
|
||||
<div>
|
||||
<span class="text-brand item-date">Nicht verfügbar</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (refreshingAvailabilit$ | async) {
|
||||
<shared-skeleton-loader class="w-40"></shared-skeleton-loader>
|
||||
} @else {
|
||||
@if (orderType === 'Abholung') {
|
||||
<div
|
||||
class="item-date"
|
||||
[class.availability-changed]="estimatedShippingDateChanged$ | async"
|
||||
>
|
||||
Abholung ab {{ item?.availability?.estimatedShippingDate | date }}
|
||||
</div>
|
||||
}
|
||||
@if (
|
||||
orderType === 'Versand' ||
|
||||
orderType === 'B2B-Versand' ||
|
||||
orderType === 'DIG-Versand'
|
||||
) {
|
||||
<div
|
||||
class="item-date"
|
||||
[class.availability-changed]="estimatedShippingDateChanged$ | async"
|
||||
>
|
||||
@if (item?.availability?.estimatedDelivery) {
|
||||
Zustellung zwischen
|
||||
{{
|
||||
(
|
||||
item?.availability?.estimatedDelivery?.start | date: 'EEE, dd.MM.'
|
||||
)?.replace('.', '')
|
||||
}}
|
||||
und
|
||||
{{
|
||||
(
|
||||
item?.availability?.estimatedDelivery?.stop | date: 'EEE, dd.MM.'
|
||||
)?.replace('.', '')
|
||||
}}
|
||||
} @else {
|
||||
Versand {{ item?.availability?.estimatedShippingDate | date }}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (olaError$ | async) {
|
||||
<div class="item-availability-message">Artikel nicht verfügbar</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="item-price-stock flex flex-col">
|
||||
<div class="text-p2 font-bold">
|
||||
{{ item?.availability?.price?.value?.value | currency: 'EUR' : 'code' }}
|
||||
</div>
|
||||
<div class="text-p2 font-normal">
|
||||
@if (!(isDummy$ | async)) {
|
||||
<ui-quantity-dropdown
|
||||
[ngModel]="item?.quantity"
|
||||
(ngModelChange)="onChangeQuantity($event)"
|
||||
[showSpinner]="(loadingOnQuantityChangeById$ | async) === item?.id"
|
||||
[disabled]="(loadingOnItemChangeById$ | async) === item?.id"
|
||||
[range]="quantityRange$ | async"
|
||||
></ui-quantity-dropdown>
|
||||
} @else {
|
||||
<div class="mt-2">{{ item?.quantity }}x</div>
|
||||
}
|
||||
</div>
|
||||
@if (quantityError) {
|
||||
<div class="quantity-error">
|
||||
{{ quantityError }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (orderType !== 'Download') {
|
||||
<div class="actions">
|
||||
@if (!(hasOrderType$ | async)) {
|
||||
<button
|
||||
[disabled]="
|
||||
(loadingOnQuantityChangeById$ | async) === item?.id ||
|
||||
(loadingOnItemChangeById$ | async) === item?.id
|
||||
"
|
||||
(click)="onChangeItem()"
|
||||
>
|
||||
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id"
|
||||
>Lieferweg auswählen</ui-spinner
|
||||
>
|
||||
</button>
|
||||
}
|
||||
@if (canEdit$ | async) {
|
||||
<button
|
||||
[disabled]="
|
||||
(loadingOnQuantityChangeById$ | async) === item?.id ||
|
||||
(loadingOnItemChangeById$ | async) === item?.id
|
||||
"
|
||||
(click)="onChangeItem()"
|
||||
>
|
||||
<ui-spinner [show]="(loadingOnItemChangeById$ | async) === item?.id"
|
||||
>Lieferweg ändern</ui-spinner
|
||||
>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,255 +1,298 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnInit,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { ItemType, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
export interface ShoppingCartItemComponentState {
|
||||
item: ShoppingCartItemDTO;
|
||||
orderType: string;
|
||||
loadingOnItemChangeById?: number;
|
||||
loadingOnQuantityChangeById?: number;
|
||||
refreshingAvailability: boolean;
|
||||
sscChanged: boolean;
|
||||
sscTextChanged: boolean;
|
||||
estimatedShippingDateChanged: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-shopping-cart-item',
|
||||
templateUrl: 'shopping-cart-item.component.html',
|
||||
styleUrls: ['shopping-cart-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ShoppingCartItemComponent extends ComponentStore<ShoppingCartItemComponentState> implements OnInit {
|
||||
private _zone = inject(NgZone);
|
||||
|
||||
@Output() changeItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
|
||||
@Output() changeDummyItem = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO }>();
|
||||
@Output() changeQuantity = new EventEmitter<{ shoppingCartItem: ShoppingCartItemDTO; quantity: number }>();
|
||||
|
||||
@Input()
|
||||
get item() {
|
||||
return this.get((s) => s.item);
|
||||
}
|
||||
set item(item: ShoppingCartItemDTO) {
|
||||
if (this.item !== item) {
|
||||
this.patchState({ item });
|
||||
}
|
||||
}
|
||||
readonly item$ = this.select((s) => s.item);
|
||||
|
||||
readonly contributors$ = this.item$.pipe(
|
||||
map((item) => item?.product?.contributors?.split(';').map((val) => val.trim())),
|
||||
);
|
||||
|
||||
@Input()
|
||||
get orderType() {
|
||||
return this.get((s) => s.orderType);
|
||||
}
|
||||
set orderType(orderType: string) {
|
||||
if (this.orderType !== orderType) {
|
||||
this.patchState({ orderType });
|
||||
}
|
||||
}
|
||||
readonly orderType$ = this.select((s) => s.orderType);
|
||||
|
||||
@Input()
|
||||
get loadingOnItemChangeById() {
|
||||
return this.get((s) => s.loadingOnItemChangeById);
|
||||
}
|
||||
set loadingOnItemChangeById(loadingOnItemChangeById: number) {
|
||||
if (this.loadingOnItemChangeById !== loadingOnItemChangeById) {
|
||||
this.patchState({ loadingOnItemChangeById });
|
||||
}
|
||||
}
|
||||
readonly loadingOnItemChangeById$ = this.select((s) => s.loadingOnItemChangeById).pipe(shareReplay());
|
||||
|
||||
@Input()
|
||||
get loadingOnQuantityChangeById() {
|
||||
return this.get((s) => s.loadingOnQuantityChangeById);
|
||||
}
|
||||
set loadingOnQuantityChangeById(loadingOnQuantityChangeById: number) {
|
||||
if (this.loadingOnQuantityChangeById !== loadingOnQuantityChangeById) {
|
||||
this.patchState({ loadingOnQuantityChangeById });
|
||||
}
|
||||
}
|
||||
readonly loadingOnQuantityChangeById$ = this.select((s) => s.loadingOnQuantityChangeById).pipe(shareReplay());
|
||||
|
||||
@Input()
|
||||
quantityError: string;
|
||||
|
||||
isDummy$ = this.item$.pipe(
|
||||
map((item) => item?.availability?.supplyChannel === 'MANUALLY'),
|
||||
shareReplay(),
|
||||
);
|
||||
hasOrderType$ = this.orderType$.pipe(
|
||||
map((orderType) => orderType !== undefined),
|
||||
shareReplay(),
|
||||
);
|
||||
|
||||
canEdit$ = combineLatest([this.isDummy$, this.hasOrderType$, this.item$]).pipe(
|
||||
map(([isDummy, hasOrderType, item]) => {
|
||||
if (item.itemType === (66560 as ItemType)) {
|
||||
return false;
|
||||
}
|
||||
return isDummy || hasOrderType;
|
||||
}),
|
||||
);
|
||||
|
||||
quantityRange$ = combineLatest([this.orderType$, this.item$]).pipe(
|
||||
map(([orderType, item]) => (orderType === 'Rücklage' ? item.availability?.inStock : 999)),
|
||||
);
|
||||
|
||||
isDownloadAvailable$ = combineLatest([this.item$, this.orderType$]).pipe(
|
||||
filter(([_, orderType]) => orderType === 'Download'),
|
||||
switchMap(([item]) =>
|
||||
this.availabilityService.getDownloadAvailability({
|
||||
item: { ean: item.product.ean, price: item.availability.price, itemId: +item.product.catalogProductNumber },
|
||||
}),
|
||||
),
|
||||
map((availability) => availability && this.availabilityService.isAvailable({ availability })),
|
||||
);
|
||||
|
||||
olaError$ = this.checkoutService
|
||||
.getOlaErrors({ processId: this.application.activatedProcessId })
|
||||
.pipe(map((ids) => ids?.find((id) => id === this.item.id)));
|
||||
|
||||
get productSearchResultsPath() {
|
||||
return this._productNavigationService.getArticleSearchResultsPath(this.application.activatedProcessId).path;
|
||||
}
|
||||
|
||||
get productSearchDetailsPath() {
|
||||
return this._productNavigationService.getArticleDetailsPathByEan({
|
||||
processId: this.application.activatedProcessId,
|
||||
ean: this.item?.product?.ean,
|
||||
}).path;
|
||||
}
|
||||
|
||||
get isTablet() {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
refreshingAvailabilit$ = this.select((s) => s.refreshingAvailability);
|
||||
|
||||
sscChanged$ = this.select((s) => s.sscChanged || s.sscTextChanged);
|
||||
|
||||
estimatedShippingDateChanged$ = this.select((s) => s.estimatedShippingDateChanged);
|
||||
|
||||
notAvailable$ = this.item$.pipe(
|
||||
map((item) => {
|
||||
const availability = item?.availability;
|
||||
|
||||
if (availability.availabilityType === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availability.inStock && item.quantity > availability.inStock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.availabilityService.isAvailable({ availability });
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private availabilityService: DomainAvailabilityService,
|
||||
private checkoutService: DomainCheckoutService,
|
||||
public application: ApplicationService,
|
||||
private _productNavigationService: ProductCatalogNavigationService,
|
||||
private _environment: EnvironmentService,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super({
|
||||
item: undefined,
|
||||
orderType: '',
|
||||
refreshingAvailability: false,
|
||||
sscChanged: false,
|
||||
sscTextChanged: false,
|
||||
estimatedShippingDateChanged: false,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
async onChangeItem() {
|
||||
const isDummy = await this.isDummy$.pipe(first()).toPromise();
|
||||
isDummy
|
||||
? this.changeDummyItem.emit({ shoppingCartItem: this.item })
|
||||
: this.changeItem.emit({ shoppingCartItem: this.item });
|
||||
}
|
||||
|
||||
onChangeQuantity(quantity: number) {
|
||||
this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
|
||||
}
|
||||
|
||||
async refreshAvailability() {
|
||||
const currentAvailability = cloneDeep(this.item.availability);
|
||||
|
||||
try {
|
||||
this.patchRefreshingAvailability(true);
|
||||
this._cdr.markForCheck();
|
||||
const availability = await this.checkoutService.refreshAvailability({
|
||||
processId: this.application.activatedProcessId,
|
||||
shoppingCartItemId: this.item.id,
|
||||
});
|
||||
|
||||
if (currentAvailability.ssc !== availability.ssc) {
|
||||
this.sscChanged();
|
||||
}
|
||||
if (currentAvailability.sscText !== availability.sscText) {
|
||||
this.ssctextChanged();
|
||||
}
|
||||
if (
|
||||
moment(currentAvailability.estimatedShippingDate)
|
||||
.startOf('day')
|
||||
.diff(moment(availability.estimatedShippingDate).startOf('day'))
|
||||
) {
|
||||
this.estimatedShippingDateChanged();
|
||||
}
|
||||
} catch (error) {}
|
||||
|
||||
this.patchRefreshingAvailability(false);
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
patchRefreshingAvailability(value: boolean) {
|
||||
this._zone.run(() => {
|
||||
this.patchState({ refreshingAvailability: value });
|
||||
this._cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ssctextChanged() {
|
||||
this.patchState({ sscTextChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
sscChanged() {
|
||||
this.patchState({ sscChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
estimatedShippingDateChanged() {
|
||||
this.patchState({ estimatedShippingDateChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
NgZone,
|
||||
OnInit,
|
||||
Output,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ApplicationService } from '@core/application';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { ProductCatalogNavigationService } from '@shared/services/navigation';
|
||||
import { ItemType, ShoppingCartItemDTO } from '@generated/swagger/checkout-api';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
|
||||
export interface ShoppingCartItemComponentState {
|
||||
item: ShoppingCartItemDTO;
|
||||
orderType: string;
|
||||
loadingOnItemChangeById?: number;
|
||||
loadingOnQuantityChangeById?: number;
|
||||
refreshingAvailability: boolean;
|
||||
sscChanged: boolean;
|
||||
sscTextChanged: boolean;
|
||||
estimatedShippingDateChanged: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-shopping-cart-item',
|
||||
templateUrl: 'shopping-cart-item.component.html',
|
||||
styleUrls: ['shopping-cart-item.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class ShoppingCartItemComponent
|
||||
extends ComponentStore<ShoppingCartItemComponentState>
|
||||
implements OnInit
|
||||
{
|
||||
private _zone = inject(NgZone);
|
||||
|
||||
@Output() changeItem = new EventEmitter<{
|
||||
shoppingCartItem: ShoppingCartItemDTO;
|
||||
}>();
|
||||
@Output() changeDummyItem = new EventEmitter<{
|
||||
shoppingCartItem: ShoppingCartItemDTO;
|
||||
}>();
|
||||
@Output() changeQuantity = new EventEmitter<{
|
||||
shoppingCartItem: ShoppingCartItemDTO;
|
||||
quantity: number;
|
||||
}>();
|
||||
|
||||
@Input()
|
||||
get item() {
|
||||
return this.get((s) => s.item);
|
||||
}
|
||||
set item(item: ShoppingCartItemDTO) {
|
||||
if (this.item !== item) {
|
||||
this.patchState({ item });
|
||||
}
|
||||
}
|
||||
readonly item$ = this.select((s) => s.item);
|
||||
|
||||
readonly contributors$ = this.item$.pipe(
|
||||
map((item) =>
|
||||
item?.product?.contributors?.split(';').map((val) => val.trim()),
|
||||
),
|
||||
);
|
||||
|
||||
get showLoyaltyValue() {
|
||||
return this.item?.loyalty?.value > 0;
|
||||
}
|
||||
|
||||
@Input()
|
||||
get orderType() {
|
||||
return this.get((s) => s.orderType);
|
||||
}
|
||||
set orderType(orderType: string) {
|
||||
if (this.orderType !== orderType) {
|
||||
this.patchState({ orderType });
|
||||
}
|
||||
}
|
||||
readonly orderType$ = this.select((s) => s.orderType);
|
||||
|
||||
@Input()
|
||||
get loadingOnItemChangeById() {
|
||||
return this.get((s) => s.loadingOnItemChangeById);
|
||||
}
|
||||
set loadingOnItemChangeById(loadingOnItemChangeById: number) {
|
||||
if (this.loadingOnItemChangeById !== loadingOnItemChangeById) {
|
||||
this.patchState({ loadingOnItemChangeById });
|
||||
}
|
||||
}
|
||||
readonly loadingOnItemChangeById$ = this.select(
|
||||
(s) => s.loadingOnItemChangeById,
|
||||
).pipe(shareReplay());
|
||||
|
||||
@Input()
|
||||
get loadingOnQuantityChangeById() {
|
||||
return this.get((s) => s.loadingOnQuantityChangeById);
|
||||
}
|
||||
set loadingOnQuantityChangeById(loadingOnQuantityChangeById: number) {
|
||||
if (this.loadingOnQuantityChangeById !== loadingOnQuantityChangeById) {
|
||||
this.patchState({ loadingOnQuantityChangeById });
|
||||
}
|
||||
}
|
||||
readonly loadingOnQuantityChangeById$ = this.select(
|
||||
(s) => s.loadingOnQuantityChangeById,
|
||||
).pipe(shareReplay());
|
||||
|
||||
@Input()
|
||||
quantityError: string;
|
||||
|
||||
isDummy$ = this.item$.pipe(
|
||||
map((item) => item?.availability?.supplyChannel === 'MANUALLY'),
|
||||
shareReplay(),
|
||||
);
|
||||
hasOrderType$ = this.orderType$.pipe(
|
||||
map((orderType) => orderType !== undefined),
|
||||
shareReplay(),
|
||||
);
|
||||
|
||||
canEdit$ = combineLatest([
|
||||
this.isDummy$,
|
||||
this.hasOrderType$,
|
||||
this.item$,
|
||||
]).pipe(
|
||||
map(([isDummy, hasOrderType, item]) => {
|
||||
if (item.itemType === (66560 as ItemType)) {
|
||||
return false;
|
||||
}
|
||||
return isDummy || hasOrderType;
|
||||
}),
|
||||
);
|
||||
|
||||
quantityRange$ = combineLatest([this.orderType$, this.item$]).pipe(
|
||||
map(([orderType, item]) =>
|
||||
orderType === 'Rücklage' ? item.availability?.inStock : 999,
|
||||
),
|
||||
);
|
||||
|
||||
isDownloadAvailable$ = combineLatest([this.item$, this.orderType$]).pipe(
|
||||
filter(([, orderType]) => orderType === 'Download'),
|
||||
switchMap(([item]) =>
|
||||
this.availabilityService.getDownloadAvailability({
|
||||
item: {
|
||||
ean: item.product.ean,
|
||||
price: item.availability.price,
|
||||
itemId: +item.product.catalogProductNumber,
|
||||
},
|
||||
}),
|
||||
),
|
||||
map(
|
||||
(availability) =>
|
||||
availability && this.availabilityService.isAvailable({ availability }),
|
||||
),
|
||||
);
|
||||
|
||||
olaError$ = this.checkoutService
|
||||
.getOlaErrors({ processId: this.application.activatedProcessId })
|
||||
.pipe(map((ids) => ids?.find((id) => id === this.item.id)));
|
||||
|
||||
get productSearchResultsPath() {
|
||||
return this._productNavigationService.getArticleSearchResultsPath(
|
||||
this.application.activatedProcessId,
|
||||
).path;
|
||||
}
|
||||
|
||||
get productSearchDetailsPath() {
|
||||
return this._productNavigationService.getArticleDetailsPathByEan({
|
||||
processId: this.application.activatedProcessId,
|
||||
ean: this.item?.product?.ean,
|
||||
}).path;
|
||||
}
|
||||
|
||||
get isTablet() {
|
||||
return this._environment.matchTablet();
|
||||
}
|
||||
|
||||
refreshingAvailabilit$ = this.select((s) => s.refreshingAvailability);
|
||||
|
||||
sscChanged$ = this.select((s) => s.sscChanged || s.sscTextChanged);
|
||||
|
||||
estimatedShippingDateChanged$ = this.select(
|
||||
(s) => s.estimatedShippingDateChanged,
|
||||
);
|
||||
|
||||
notAvailable$ = this.item$.pipe(
|
||||
map((item) => {
|
||||
const availability = item?.availability;
|
||||
|
||||
if (availability.availabilityType === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (availability.inStock && item.quantity > availability.inStock) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !this.availabilityService.isAvailable({ availability });
|
||||
}),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private availabilityService: DomainAvailabilityService,
|
||||
private checkoutService: DomainCheckoutService,
|
||||
public application: ApplicationService,
|
||||
private _productNavigationService: ProductCatalogNavigationService,
|
||||
private _environment: EnvironmentService,
|
||||
private _cdr: ChangeDetectorRef,
|
||||
) {
|
||||
super({
|
||||
item: undefined,
|
||||
orderType: '',
|
||||
refreshingAvailability: false,
|
||||
sscChanged: false,
|
||||
sscTextChanged: false,
|
||||
estimatedShippingDateChanged: false,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Component initialization
|
||||
}
|
||||
|
||||
async onChangeItem() {
|
||||
const isDummy = await this.isDummy$.pipe(first()).toPromise();
|
||||
if (isDummy) {
|
||||
this.changeDummyItem.emit({ shoppingCartItem: this.item });
|
||||
} else {
|
||||
this.changeItem.emit({ shoppingCartItem: this.item });
|
||||
}
|
||||
}
|
||||
|
||||
onChangeQuantity(quantity: number) {
|
||||
this.changeQuantity.emit({ shoppingCartItem: this.item, quantity });
|
||||
}
|
||||
|
||||
async refreshAvailability() {
|
||||
const currentAvailability = cloneDeep(this.item.availability);
|
||||
|
||||
try {
|
||||
this.patchRefreshingAvailability(true);
|
||||
this._cdr.markForCheck();
|
||||
const availability = await this.checkoutService.refreshAvailability({
|
||||
processId: this.application.activatedProcessId,
|
||||
shoppingCartItemId: this.item.id,
|
||||
});
|
||||
|
||||
if (currentAvailability.ssc !== availability.ssc) {
|
||||
this.sscChanged();
|
||||
}
|
||||
if (currentAvailability.sscText !== availability.sscText) {
|
||||
this.ssctextChanged();
|
||||
}
|
||||
if (
|
||||
moment(currentAvailability.estimatedShippingDate)
|
||||
.startOf('day')
|
||||
.diff(moment(availability.estimatedShippingDate).startOf('day'))
|
||||
) {
|
||||
this.estimatedShippingDateChanged();
|
||||
}
|
||||
} catch {
|
||||
// Error handling for availability refresh
|
||||
}
|
||||
|
||||
this.patchRefreshingAvailability(false);
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
patchRefreshingAvailability(value: boolean) {
|
||||
this._zone.run(() => {
|
||||
this.patchState({ refreshingAvailability: value });
|
||||
this._cdr.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
ssctextChanged() {
|
||||
this.patchState({ sscTextChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
sscChanged() {
|
||||
this.patchState({ sscChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
|
||||
estimatedShippingDateChanged() {
|
||||
this.patchState({ estimatedShippingDateChanged: true });
|
||||
this._cdr.markForCheck();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
<div class="summary-wrapper">
|
||||
<div class="flex flex-col bg-white rounded pt-10 mb-24">
|
||||
<div class="rounded-[50%] bg-[#26830C] w-8 h-8 flex items-center justify-center self-center">
|
||||
<div
|
||||
class="rounded-[50%] bg-[#26830C] w-8 h-8 flex items-center justify-center self-center"
|
||||
>
|
||||
<shared-icon class="text-white" icon="done" [size]="24"></shared-icon>
|
||||
</div>
|
||||
|
||||
<h1 class="text-center text-h2 my-1 font-bold">Bestellbestätigung</h1>
|
||||
<p class="text-center text-p1 mb-10">Nachfolgend erhalten Sie die Übersicht Ihrer Bestellung.</p>
|
||||
<p class="text-center text-p1 mb-10">
|
||||
Nachfolgend erhalten Sie die Übersicht Ihrer Bestellung.
|
||||
</p>
|
||||
|
||||
@for (displayOrder of displayOrders$ | async; track displayOrder; let i = $index; let orderLast = $last) {
|
||||
@for (
|
||||
displayOrder of displayOrders$ | async;
|
||||
track displayOrder;
|
||||
let i = $index;
|
||||
let orderLast = $last
|
||||
) {
|
||||
@if (i === 0) {
|
||||
<div class="flex flex-row items-center bg-white shadow-card min-h-[3.3125rem]">
|
||||
<div
|
||||
class="flex flex-row items-center bg-white shadow-card min-h-[3.3125rem]"
|
||||
>
|
||||
<div class="text-h3 font-bold px-5 py-[0.875rem]">
|
||||
{{ displayOrder?.buyer | buyerName }}
|
||||
</div>
|
||||
@@ -17,34 +28,45 @@
|
||||
<hr />
|
||||
}
|
||||
<div class="flex flex-row items-center bg-[#F5F7FA] min-h-[3.3125rem]">
|
||||
<div class="flex flex-row items-center justify-center px-5 py-[0.875rem]">
|
||||
<div
|
||||
class="flex flex-row items-center justify-center px-5 py-[0.875rem]"
|
||||
>
|
||||
@if ((displayOrder?.items)[0]?.features?.orderType !== 'Dummy') {
|
||||
<shared-icon
|
||||
class="mr-2"
|
||||
[size]="(displayOrder?.items)[0]?.features?.orderType === 'B2B-Versand' ? 36 : 24"
|
||||
[size]="
|
||||
(displayOrder?.items)[0]?.features?.orderType === 'B2B-Versand'
|
||||
? 36
|
||||
: 24
|
||||
"
|
||||
[icon]="(displayOrder?.items)[0]?.features?.orderType"
|
||||
></shared-icon>
|
||||
}
|
||||
<p class="text-p1 font-bold mr-3">{{ (displayOrder?.items)[0]?.features?.orderType }}</p>
|
||||
<p class="text-p1 font-bold mr-3">
|
||||
{{ (displayOrder?.items)[0]?.features?.orderType }}
|
||||
</p>
|
||||
@if (
|
||||
(displayOrder?.items)[0]?.features?.orderType === 'Abholung' || (displayOrder?.items)[0]?.features?.orderType === 'Rücklage') {
|
||||
<div
|
||||
>
|
||||
{{ displayOrder.targetBranch?.name }}, {{ displayOrder.targetBranch | branchAddress }}
|
||||
(displayOrder?.items)[0]?.features?.orderType === 'Abholung' ||
|
||||
(displayOrder?.items)[0]?.features?.orderType === 'Rücklage'
|
||||
) {
|
||||
<div>
|
||||
{{ displayOrder.targetBranch?.name }},
|
||||
{{ displayOrder.targetBranch | branchAddress }}
|
||||
</div>
|
||||
} @else {
|
||||
{{ displayOrder.shippingAddress | branchAddress }}
|
||||
}
|
||||
@if ((displayOrder?.items)[0]?.features?.orderType === 'Download') {
|
||||
<div>
|
||||
| {{ displayOrder.buyer?.communicationDetails?.email }}
|
||||
</div>
|
||||
<div>| {{ displayOrder.buyer?.communicationDetails?.email }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<div class="flex flex-col px-5 py-4 bg-white" [attr.data-order-type]="(displayOrder?.items)[0]?.features?.orderType">
|
||||
<div
|
||||
class="flex flex-col px-5 py-4 bg-white"
|
||||
[attr.data-order-type]="(displayOrder?.items)[0]?.features?.orderType"
|
||||
>
|
||||
<div class="flex flex-row justify-between items-center mb-[0.375rem]">
|
||||
<div class="flex flex-row">
|
||||
<span class="w-32">Vorgangs-ID</span>
|
||||
@@ -54,14 +76,28 @@
|
||||
data-which="Vorgangs-ID"
|
||||
data-what="link"
|
||||
class="font-bold text-[#0556B4] no-underline"
|
||||
[routerLink]="['/kunde', processId, 'customer', 'search', customer?.id, 'orders', displayOrder.id]"
|
||||
[queryParams]="{ main_qs: customer?.customerNumber, filter_customertype: '' }"
|
||||
>
|
||||
[routerLink]="[
|
||||
'/kunde',
|
||||
processId,
|
||||
'customer',
|
||||
'search',
|
||||
customer?.id,
|
||||
'orders',
|
||||
displayOrder.id,
|
||||
]"
|
||||
[queryParams]="{
|
||||
main_qs: customer?.customerNumber,
|
||||
filter_customertype: '',
|
||||
}"
|
||||
>
|
||||
{{ displayOrder.orderNumber }}
|
||||
</a>
|
||||
}
|
||||
}
|
||||
<ui-spinner class="text-[#0556B4] h-4 w-4" [show]="!(customer$ | async)"></ui-spinner>
|
||||
<ui-spinner
|
||||
class="text-[#0556B4] h-4 w-4"
|
||||
[show]="!(customer$ | async)"
|
||||
></ui-spinner>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
@@ -77,7 +113,7 @@
|
||||
type="button"
|
||||
class="text-[#0556B4] font-bold flex flex-row items-center justify-center"
|
||||
[class.flex-row-reverse]="!expanded[i]"
|
||||
>
|
||||
>
|
||||
<shared-icon
|
||||
class="mr-1"
|
||||
icon="arrow-back"
|
||||
@@ -95,18 +131,26 @@
|
||||
class="page-checkout-summary__items-tablet px-5 pb-[1.875rem] bg-white"
|
||||
[class.page-checkout-summary__items]="isDesktop$ | async"
|
||||
[class.last]="last"
|
||||
>
|
||||
>
|
||||
<div class="page-checkout-summary__items-thumbnail flex flex-row">
|
||||
<a [routerLink]="getProductSearchDetailsPath(order?.product?.ean)" [queryParams]="getProductSearchDetailsQueryParams(order)">
|
||||
<img class="w-[3.125rem] max-h-20 mr-2" [src]="order.product?.ean | productImage: 195 : 315 : true" />
|
||||
<a
|
||||
[routerLink]="getProductSearchDetailsPath(order?.product?.ean)"
|
||||
[queryParams]="getProductSearchDetailsQueryParams(order)"
|
||||
>
|
||||
<img
|
||||
class="w-[3.125rem] max-h-20 mr-2"
|
||||
[src]="order.product?.ean | productImage: 195 : 315 : true"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="page-checkout-summary__items-title whitespace-nowrap overflow-ellipsis overflow-hidden">
|
||||
<div
|
||||
class="page-checkout-summary__items-title whitespace-nowrap overflow-ellipsis overflow-hidden"
|
||||
>
|
||||
<a
|
||||
class="font-bold no-underline text-[#0556B4]"
|
||||
[routerLink]="getProductSearchDetailsPath(order?.product?.ean)"
|
||||
[queryParams]="getProductSearchDetailsQueryParams(order)"
|
||||
>
|
||||
>
|
||||
{{ order?.product?.name }}
|
||||
</a>
|
||||
</div>
|
||||
@@ -120,40 +164,78 @@
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="page-checkout-summary__items-quantity font-bold justify-self-end">
|
||||
<div
|
||||
class="page-checkout-summary__items-quantity font-bold justify-self-end"
|
||||
>
|
||||
<span>{{ order.quantity }}x</span>
|
||||
</div>
|
||||
<div class="page-checkout-summary__items-price font-bold justify-self-end">
|
||||
<span>{{ order.price?.value?.value | currency: ' ' }} {{ order.price?.value?.currency }}</span>
|
||||
<div
|
||||
class="page-checkout-summary__items-price font-bold justify-self-end"
|
||||
>
|
||||
<span
|
||||
>{{ order.price?.value?.value | currency: ' ' }}
|
||||
{{ order.price?.value?.currency }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="page-checkout-summary__items-delivery product-details">
|
||||
<div class="delivery-row">
|
||||
@switch (order?.features?.orderType) {
|
||||
@case ('Abholung') {
|
||||
<span class="order-type">
|
||||
Abholung ab {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}
|
||||
<ng-container [ngTemplateOutlet]="abholfrist" [ngTemplateOutletContext]="{ order: order }"></ng-container>
|
||||
Abholung ab
|
||||
{{
|
||||
(order?.subsetItems)[0]?.estimatedShippingDate | date
|
||||
}}
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="abholfrist"
|
||||
[ngTemplateOutletContext]="{ order: order }"
|
||||
></ng-container>
|
||||
</span>
|
||||
}
|
||||
@case ('Rücklage') {
|
||||
<span class="order-type">
|
||||
{{ order?.features?.orderType }}
|
||||
<ng-container [ngTemplateOutlet]="abholfrist" [ngTemplateOutletContext]="{ order: order }"></ng-container>
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="abholfrist"
|
||||
[ngTemplateOutletContext]="{ order: order }"
|
||||
></ng-container>
|
||||
</span>
|
||||
}
|
||||
@case (['Versand', 'B2B-Versand', 'DIG-Versand'].indexOf(order?.features?.orderType) > -1) {
|
||||
@case (
|
||||
['Versand', 'B2B-Versand', 'DIG-Versand'].indexOf(
|
||||
order?.features?.orderType
|
||||
) > -1
|
||||
) {
|
||||
@if ((order?.subsetItems)[0]?.estimatedDelivery) {
|
||||
<span class="order-type">
|
||||
Zustellung zwischen
|
||||
{{ ((order?.subsetItems)[0]?.estimatedDelivery?.start | date: 'EEE, dd.MM.')?.replace('.', '') }} und
|
||||
{{ ((order?.subsetItems)[0]?.estimatedDelivery?.stop | date: 'EEE, dd.MM.')?.replace('.', '') }}
|
||||
{{
|
||||
(
|
||||
(order?.subsetItems)[0]?.estimatedDelivery?.start
|
||||
| date: 'EEE, dd.MM.'
|
||||
)?.replace('.', '')
|
||||
}}
|
||||
und
|
||||
{{
|
||||
(
|
||||
(order?.subsetItems)[0]?.estimatedDelivery?.stop
|
||||
| date: 'EEE, dd.MM.'
|
||||
)?.replace('.', '')
|
||||
}}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="order-type">Versanddatum {{ (order?.subsetItems)[0]?.estimatedShippingDate | date }}</span>
|
||||
<span class="order-type"
|
||||
>Versanddatum
|
||||
{{
|
||||
(order?.subsetItems)[0]?.estimatedShippingDate | date
|
||||
}}</span
|
||||
>
|
||||
}
|
||||
}
|
||||
@default {
|
||||
<span class="order-type">{{ order?.features?.orderType }}</span>
|
||||
<span class="order-type">{{
|
||||
order?.features?.orderType
|
||||
}}</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -165,21 +247,31 @@
|
||||
}
|
||||
}
|
||||
@if (orderLast) {
|
||||
<div class="flex flex-row justify-between items-center min-h-[3.3125rem] bg-white px-5 py-4 rounded-b">
|
||||
<div
|
||||
class="flex flex-row justify-between items-center min-h-[3.3125rem] bg-white px-5 py-4 rounded-b"
|
||||
>
|
||||
@if (totalReadingPoints$ | async; as totalReadingPoints) {
|
||||
<span class="text-p2 font-bold">
|
||||
{{ totalItemCount$ | async }} Artikel | {{ totalReadingPoints }} Lesepunkte
|
||||
{{ totalItemCount$ | async }} Artikel |
|
||||
{{ totalReadingPoints }} Lesepunkte
|
||||
</span>
|
||||
}
|
||||
<div class="flex flex-row items-center justify-center">
|
||||
<div class="text-p1 font-bold flex flex-row items-center">
|
||||
<div class="mr-1">Gesamtsumme {{ totalPrice$ | async | currency: ' ' }} {{ totalPriceCurrency$ | async }}</div>
|
||||
<div class="mr-1">
|
||||
Gesamtsumme {{ totalPrice$ | async | currency: ' ' }}
|
||||
{{ totalPriceCurrency$ | async }}
|
||||
</div>
|
||||
</div>
|
||||
@if ((takeNowOrders$ | async)?.length === 1 && (isB2BCustomer$ | async)) {
|
||||
@if (
|
||||
(takeNowOrders$ | async)?.length === 1 && (isB2BCustomer$ | async)
|
||||
) {
|
||||
<div
|
||||
class="bg-brand text-white font-bold text-p1 outline-none border-none rounded-full px-6 py-3 ml-2"
|
||||
>
|
||||
<button class="cta-goods-out" (click)="navigateToShelfOut()">Zur Warenausgabe</button>
|
||||
>
|
||||
<button class="cta-goods-out" (click)="navigateToShelfOut()">
|
||||
Zur Warenausgabe
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -192,12 +284,23 @@
|
||||
<ng-template #abholfrist let-order="order">
|
||||
@if (!(updatingPreferredPickUpDate$ | async)[(order?.subsetItems)[0].id]) {
|
||||
<div class="inline-flex">
|
||||
<button [uiOverlayTrigger]="deadlineDatepicker" #deadlineDatepickerTrigger="uiOverlayTrigger" class="flex flex-row items-center">
|
||||
<button
|
||||
[uiOverlayTrigger]="deadlineDatepicker"
|
||||
#deadlineDatepickerTrigger="uiOverlayTrigger"
|
||||
class="flex flex-row items-center"
|
||||
>
|
||||
<span class="mx-[0.625rem] font-normal">bis</span>
|
||||
<strong class="border-r border-[#AEB7C1] pr-4">
|
||||
{{ ((order?.subsetItems)[0]?.preferredPickUpDate | date: 'dd.MM.yy') || 'TT.MM.JJJJ' }}
|
||||
{{
|
||||
((order?.subsetItems)[0]?.preferredPickUpDate | date: 'dd.MM.yy') ||
|
||||
'TT.MM.JJJJ'
|
||||
}}
|
||||
</strong>
|
||||
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
|
||||
<shared-icon
|
||||
class="text-[#596470] ml-4"
|
||||
[size]="24"
|
||||
icon="isa-calendar"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-datepicker
|
||||
#deadlineDatepicker
|
||||
@@ -207,12 +310,15 @@
|
||||
[min]="minDateDatepicker"
|
||||
[disabledDaysOfWeek]="[0]"
|
||||
[(selected)]="selectedDate"
|
||||
>
|
||||
>
|
||||
<div #content class="grid grid-flow-row gap-2">
|
||||
<button
|
||||
class="rounded-full font-bold text-white bg-brand py-px-15 px-px-25"
|
||||
(click)="updatePreferredPickUpDate(undefined, selectedDate); deadlineDatepickerTrigger.close()"
|
||||
>
|
||||
(click)="
|
||||
updatePreferredPickUpDate(undefined, selectedDate);
|
||||
deadlineDatepickerTrigger.close()
|
||||
"
|
||||
>
|
||||
Für den Warenkorb festlegen
|
||||
</button>
|
||||
</div>
|
||||
@@ -225,15 +331,19 @@
|
||||
</ng-template>
|
||||
|
||||
<div class="relative">
|
||||
<div class="absolute left-1/2 bottom-10 inline-grid grid-flow-col gap-4 justify-center transform -translate-x-1/2">
|
||||
<div
|
||||
class="absolute left-1/2 bottom-10 flex flex-wrap w-full gap-4 justify-center transform -translate-x-1/2"
|
||||
>
|
||||
<button
|
||||
*ifRole="'Store'"
|
||||
[disabled]="isPrinting$ | async"
|
||||
type="button"
|
||||
class="px-6 py-2 rounded-full border-2 border-solid border-brand text-brand bg-white font-bold text-lg whitespace-nowrap h-14 flex flex-row items-center justify-center print-button"
|
||||
(click)="printOrderConfirmation()"
|
||||
>
|
||||
<ui-spinner class="min-h-4 min-w-4" [show]="isPrinting$ | async"
|
||||
>Bestellbestätigung drucken</ui-spinner
|
||||
>
|
||||
<ui-spinner class="min-h-4 min-w-4" [show]="isPrinting$ | async">Bestellbestätigung drucken</ui-spinner>
|
||||
</button>
|
||||
|
||||
@if (hasAbholung$ | async) {
|
||||
@@ -241,9 +351,18 @@
|
||||
type="button"
|
||||
class="px-6 py-2 rounded-full border-2 border-solid border-brand text-brand bg-white font-bold text-lg whitespace-nowrap h-14"
|
||||
(click)="sendOrderConfirmation()"
|
||||
>
|
||||
>
|
||||
Bestellbestätigung senden
|
||||
</button>
|
||||
}
|
||||
@if (displayRewardNavigation()) {
|
||||
<button
|
||||
type="button"
|
||||
class="px-6 py-2 rounded-full border-2 border-solid border-brand text-white bg-brand font-bold text-lg whitespace-nowrap h-14"
|
||||
(click)="navigateToReward()"
|
||||
>
|
||||
Zur Prämienausgabe
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
inject,
|
||||
computed,
|
||||
} from '@angular/core';
|
||||
import { DomainCheckoutService } from '@domain/checkout';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
@@ -33,6 +34,9 @@ import {
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { SendOrderConfirmationModalService } from '@modal/send-order-confirmation';
|
||||
import { ToasterService } from '@shared/shell';
|
||||
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { PrimaryCustomerCardResource } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'page-checkout-summary',
|
||||
@@ -44,6 +48,22 @@ import { ToasterService } from '@shared/shell';
|
||||
export class CheckoutSummaryComponent implements OnInit, OnDestroy {
|
||||
private _injector = inject(Injector);
|
||||
|
||||
#tabId = injectTabId();
|
||||
#primaryCustomerCardResource = inject(PrimaryCustomerCardResource);
|
||||
#rewardShoppingCartResource = inject(SelectedRewardShoppingCartResource)
|
||||
.resource;
|
||||
|
||||
readonly rewardShoppingCartResponseValue =
|
||||
this.#rewardShoppingCartResource.value.asReadonly();
|
||||
readonly primaryCustomerCardValue =
|
||||
this.#primaryCustomerCardResource.primaryCustomerCard;
|
||||
|
||||
displayRewardNavigation = computed(() => {
|
||||
const rewardShoppingCart = this.rewardShoppingCartResponseValue();
|
||||
const hasPrimaryCard = this.primaryCustomerCardValue();
|
||||
return !!rewardShoppingCart?.items?.length && hasPrimaryCard;
|
||||
});
|
||||
|
||||
get sendOrderConfirmationModalService() {
|
||||
return this._injector.get(SendOrderConfirmationModalService);
|
||||
}
|
||||
@@ -85,7 +105,7 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
|
||||
);
|
||||
|
||||
if (ordersWithMultipleFeatures) {
|
||||
for (let orderWithMultipleFeatures of ordersWithMultipleFeatures) {
|
||||
for (const orderWithMultipleFeatures of ordersWithMultipleFeatures) {
|
||||
if (orderWithMultipleFeatures?.items?.length > 1) {
|
||||
const itemsWithOrderFeature =
|
||||
orderWithMultipleFeatures.items.filter(
|
||||
@@ -397,7 +417,7 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
async navigateToShelfOut() {
|
||||
let takeNowOrders = await this.takeNowOrders$.pipe(first()).toPromise();
|
||||
const takeNowOrders = await this.takeNowOrders$.pipe(first()).toPromise();
|
||||
if (takeNowOrders.length != 1) return;
|
||||
|
||||
try {
|
||||
@@ -422,6 +442,10 @@ export class CheckoutSummaryComponent implements OnInit, OnDestroy {
|
||||
await this.sendOrderConfirmationModalService.open(orders);
|
||||
}
|
||||
|
||||
async navigateToReward() {
|
||||
await this.router.navigate([`/${this.#tabId()}`, 'reward', 'cart']);
|
||||
}
|
||||
|
||||
async printOrderConfirmation() {
|
||||
this.isPrinting$.next(true);
|
||||
const orders = await this.displayOrders$.pipe(first()).toPromise();
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { CheckoutSummaryComponent } from './checkout-summary.component';
|
||||
import { PageCheckoutPipeModule } from '../pipes/page-checkout-pipe.module';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { UiDatepickerModule } from '@ui/datepicker';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { AuthModule } from '@core/auth';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
PageCheckoutPipeModule,
|
||||
ProductImageModule,
|
||||
IconModule,
|
||||
UiCommonModule,
|
||||
UiSpinnerModule,
|
||||
UiDatepickerModule,
|
||||
AuthModule,
|
||||
UiSpinnerModule,
|
||||
],
|
||||
exports: [CheckoutSummaryComponent],
|
||||
declarations: [CheckoutSummaryComponent],
|
||||
})
|
||||
export class CheckoutSummaryModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { CheckoutSummaryComponent } from './checkout-summary.component';
|
||||
import { PageCheckoutPipeModule } from '../pipes/page-checkout-pipe.module';
|
||||
import { ProductImageModule } from '@cdn/product-image';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { UiSpinnerModule } from '@ui/spinner';
|
||||
import { UiDatepickerModule } from '@ui/datepicker';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { AuthModule } from '@core/auth';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
PageCheckoutPipeModule,
|
||||
ProductImageModule,
|
||||
IconModule,
|
||||
UiCommonModule,
|
||||
UiSpinnerModule,
|
||||
UiDatepickerModule,
|
||||
AuthModule,
|
||||
UiSpinnerModule,
|
||||
],
|
||||
exports: [CheckoutSummaryComponent],
|
||||
declarations: [CheckoutSummaryComponent],
|
||||
providers: [],
|
||||
})
|
||||
export class CheckoutSummaryModule {}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<ng-container *ngIf="orderItem$ | async; let orderItem">
|
||||
<div class="grid grid-flow-row gap-px-2">
|
||||
<div class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t">
|
||||
<div class="grid grid-flow-col gap-[0.4375rem] items-center" *ngIf="features$ | async; let features; else: featureLoading">
|
||||
<shared-icon *ngIf="features?.length > 0" [size]="24" icon="person"></shared-icon>
|
||||
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2" *ngFor="let feature of features">
|
||||
<div
|
||||
class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t"
|
||||
>
|
||||
<div
|
||||
class="grid grid-flow-col gap-[0.4375rem] items-center"
|
||||
*ngIf="customerFeature$ | async; let feature; else: featureLoading"
|
||||
>
|
||||
<shared-icon *ngIf="!!feature" [size]="24" icon="person"></shared-icon>
|
||||
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2">
|
||||
{{ feature?.description }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,29 +23,54 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="page-customer-order-details-header__details bg-white px-4 pt-4 pb-5">
|
||||
<div
|
||||
class="page-customer-order-details-header__details bg-white px-4 pt-4 pb-5"
|
||||
>
|
||||
<h2
|
||||
class="page-customer-order-details-header__details-header items-center"
|
||||
[class.mb-8]="!orderItem?.features?.paid && !isKulturpass"
|
||||
>
|
||||
<div class="text-h2">
|
||||
{{ orderItem?.organisation }}
|
||||
<ng-container *ngIf="!!orderItem?.organisation && (!!orderItem?.firstName || !!orderItem?.lastName)">-</ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!!orderItem?.organisation &&
|
||||
(!!orderItem?.firstName || !!orderItem?.lastName)
|
||||
"
|
||||
>-</ng-container
|
||||
>
|
||||
{{ orderItem?.lastName }}
|
||||
{{ orderItem?.firstName }}
|
||||
</div>
|
||||
<div class="page-customer-order-details-header__header-compartment text-h3">
|
||||
{{ orderItem?.compartmentCode }}{{ orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo }}
|
||||
<div
|
||||
class="page-customer-order-details-header__header-compartment text-h3"
|
||||
>
|
||||
{{ orderItem?.compartmentCode
|
||||
}}{{ orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo }}
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="page-customer-order-details-header__paid-marker mt-[0.375rem]" *ngIf="orderItem?.features?.paid && !isKulturpass">
|
||||
<div class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]">
|
||||
<div
|
||||
class="page-customer-order-details-header__paid-marker mt-[0.375rem]"
|
||||
*ngIf="orderItem?.features?.paid && !isKulturpass"
|
||||
>
|
||||
<div
|
||||
class="font-bold w-fit desktop-small:text-p2 px-3 py-[0.125rem] rounded text-white bg-[#26830C]"
|
||||
>
|
||||
{{ orderItem?.features?.paid }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-customer-order-details-header__paid-marker mt-[0.375rem] text-[#26830C]" *ngIf="isKulturpass">
|
||||
<svg class="fill-current mr-2" xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22">
|
||||
<div
|
||||
class="page-customer-order-details-header__paid-marker mt-[0.375rem] text-[#26830C]"
|
||||
*ngIf="isKulturpass"
|
||||
>
|
||||
<svg
|
||||
class="fill-current mr-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="22"
|
||||
viewBox="0 -960 960 960"
|
||||
width="22"
|
||||
>
|
||||
<path
|
||||
d="M880-740v520q0 24-18 42t-42 18H140q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42ZM140-631h680v-109H140v109Zm0 129v282h680v-282H140Zm0 282v-520 520Z"
|
||||
/>
|
||||
@@ -49,25 +79,49 @@
|
||||
</div>
|
||||
|
||||
<div class="page-customer-order-details-header__details-wrapper -mt-3">
|
||||
<div class="flex flex-row page-customer-order-details-header__buyer-number" data-detail-id="Kundennummer">
|
||||
<div
|
||||
class="flex flex-row page-customer-order-details-header__buyer-number"
|
||||
data-detail-id="Kundennummer"
|
||||
>
|
||||
<div class="min-w-[9rem]">Kundennummer</div>
|
||||
<div class="flex flex-row font-bold">{{ orderItem?.buyerNumber }}</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{ orderItem?.buyerNumber }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row page-customer-order-details-header__order-number" data-detail-id="VorgangId">
|
||||
<div
|
||||
class="flex flex-row page-customer-order-details-header__order-number"
|
||||
data-detail-id="VorgangId"
|
||||
>
|
||||
<div class="min-w-[9rem]">Vorgang-ID</div>
|
||||
<div class="flex flex-row font-bold">{{ orderItem?.orderNumber }}</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{ orderItem?.orderNumber }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row page-customer-order-details-header__order-date" data-detail-id="Bestelldatum">
|
||||
<div
|
||||
class="flex flex-row page-customer-order-details-header__order-date"
|
||||
data-detail-id="Bestelldatum"
|
||||
>
|
||||
<div class="min-w-[9rem]">Bestelldatum</div>
|
||||
<div class="flex flex-row font-bold">{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row page-customer-order-details-header__processing-status justify-between" data-detail-id="Status">
|
||||
<div
|
||||
class="flex flex-row page-customer-order-details-header__processing-status justify-between"
|
||||
data-detail-id="Status"
|
||||
>
|
||||
<div class="min-w-[9rem]">Status</div>
|
||||
<div *ngIf="!(changeStatusLoader$ | async)" class="flex flex-row font-bold -mr-[0.125rem]">
|
||||
<div
|
||||
*ngIf="!(changeStatusLoader$ | async)"
|
||||
class="flex flex-row font-bold -mr-[0.125rem]"
|
||||
>
|
||||
<shared-icon
|
||||
class="mr-2 text-black flex items-center justify-center"
|
||||
[size]="16"
|
||||
*ngIf="orderItem.processingStatus | processingStatus: 'icon'; let icon"
|
||||
*ngIf="
|
||||
orderItem.processingStatus | processingStatus: 'icon';
|
||||
let icon
|
||||
"
|
||||
[icon]="icon"
|
||||
></shared-icon>
|
||||
|
||||
@@ -91,18 +145,36 @@
|
||||
icon="arrow-drop-down"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-dropdown #statusDropdown yPosition="below" xPosition="after" [xOffset]="8">
|
||||
<button uiDropdownItem *ngFor="let action of statusActions$ | async" (click)="handleActionClick(action)">
|
||||
<ui-dropdown
|
||||
#statusDropdown
|
||||
yPosition="below"
|
||||
xPosition="after"
|
||||
[xOffset]="8"
|
||||
>
|
||||
<button
|
||||
uiDropdownItem
|
||||
*ngFor="let action of statusActions$ | async"
|
||||
(click)="handleActionClick(action)"
|
||||
>
|
||||
{{ action.label }}
|
||||
</button>
|
||||
</ui-dropdown>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ui-spinner *ngIf="changeStatusLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
|
||||
<ui-spinner
|
||||
*ngIf="changeStatusLoader$ | async; let loader"
|
||||
class="flex flex-row font-bold loader"
|
||||
[show]="loader"
|
||||
></ui-spinner>
|
||||
</div>
|
||||
<div class="flex flex-row page-customer-order-details-header__order-source" data-detail-id="Bestellkanal">
|
||||
<div
|
||||
class="flex flex-row page-customer-order-details-header__order-source"
|
||||
data-detail-id="Bestellkanal"
|
||||
>
|
||||
<div class="min-w-[9rem]">Bestellkanal</div>
|
||||
<div class="flex flex-row font-bold">{{ order?.features?.orderSource }}</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{ order?.features?.orderSource }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-row page-customer-order-details-header__change-date justify-between"
|
||||
@@ -124,26 +196,39 @@
|
||||
|
||||
<ng-template #changeDate>
|
||||
<div class="min-w-[9rem]">Geändert</div>
|
||||
<div class="flex flex-row font-bold">{{ orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{
|
||||
orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm'
|
||||
}}
|
||||
Uhr
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-row page-customer-order-details-header__pick-up justify-between"
|
||||
data-detail-id="Wunschdatum"
|
||||
*ngIf="orderItem.orderType === 1 && (orderItem.processingStatus === 16 || orderItem.processingStatus === 8192)"
|
||||
*ngIf="
|
||||
orderItem.orderType === 1 &&
|
||||
(orderItem.processingStatus === 16 ||
|
||||
orderItem.processingStatus === 8192)
|
||||
"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="preferredPickUpDate"></ng-container>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col page-customer-order-details-header__dig-and-notification">
|
||||
<div
|
||||
class="flex flex-col page-customer-order-details-header__dig-and-notification"
|
||||
>
|
||||
<div
|
||||
*ngIf="orderItem.orderType === 1"
|
||||
class="flex flex-row page-customer-order-details-header__notification"
|
||||
data-detail-id="Benachrichtigung"
|
||||
>
|
||||
<div class="min-w-[9rem]">Benachrichtigung</div>
|
||||
<div class="flex flex-row font-bold">{{ (notificationsChannel | notificationsChannel) || '-' }}</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{ (notificationsChannel | notificationsChannel) || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -162,38 +247,50 @@
|
||||
<div *ngIf="showFeature" class="flex flex-row items-center mr-3">
|
||||
<ng-container [ngSwitch]="order.features.orderType">
|
||||
<ng-container *ngSwitchCase="'Versand'">
|
||||
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
|
||||
<div
|
||||
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
|
||||
>
|
||||
<shared-icon [size]="24" icon="isa-truck"></shared-icon>
|
||||
</div>
|
||||
<p class="font-bold text-p1">Versand</p>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'DIG-Versand'">
|
||||
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
|
||||
<div
|
||||
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
|
||||
>
|
||||
<shared-icon [size]="24" icon="isa-truck"></shared-icon>
|
||||
</div>
|
||||
<p class="font-bold text-p1">Versand</p>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'B2B-Versand'">
|
||||
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
|
||||
<div
|
||||
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
|
||||
>
|
||||
<shared-icon [size]="24" icon="isa-b2b-truck"></shared-icon>
|
||||
</div>
|
||||
<p class="font-bold text-p1">B2B-Versand</p>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'Abholung'">
|
||||
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
|
||||
<div
|
||||
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
|
||||
>
|
||||
<shared-icon [size]="24" icon="isa-box-out"></shared-icon>
|
||||
</div>
|
||||
<p class="font-bold text-p1 mr-3">Abholung</p>
|
||||
{{ orderItem.targetBranch }}
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'Rücklage'">
|
||||
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
|
||||
<div
|
||||
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
|
||||
>
|
||||
<shared-icon [size]="24" icon="isa-shopping-bag"></shared-icon>
|
||||
</div>
|
||||
<p class="font-bold text-p1">Rücklage</p>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchCase="'Download'">
|
||||
<div class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2">
|
||||
<div
|
||||
class="flex items-center justify-center bg-[#D8DFE5] w-[2.25rem] h-[2.25rem] rounded rounded-br-none mr-2"
|
||||
>
|
||||
<shared-icon [size]="24" icon="isa-download"></shared-icon>
|
||||
</div>
|
||||
<p class="font-bold text-p1">Download</p>
|
||||
@@ -201,50 +298,93 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="page-customer-order-details-header__additional-addresses" *ngIf="showAddresses">
|
||||
<div
|
||||
class="page-customer-order-details-header__additional-addresses"
|
||||
*ngIf="showAddresses"
|
||||
>
|
||||
<button (click)="openAddresses = !openAddresses" class="text-[#0556B4]">
|
||||
Lieferadresse / Rechnungsadresse {{ openAddresses ? 'ausblenden' : 'anzeigen' }}
|
||||
Lieferadresse / Rechnungsadresse
|
||||
{{ openAddresses ? 'ausblenden' : 'anzeigen' }}
|
||||
</button>
|
||||
|
||||
<div class="page-customer-order-details-header__addresses-popover" *ngIf="openAddresses">
|
||||
<div
|
||||
class="page-customer-order-details-header__addresses-popover"
|
||||
*ngIf="openAddresses"
|
||||
>
|
||||
<button (click)="openAddresses = !openAddresses" class="close">
|
||||
<shared-icon icon="close" [size]="24"></shared-icon>
|
||||
</button>
|
||||
|
||||
<div class="page-customer-order-details-header__addresses-popover-data">
|
||||
<div *ngIf="order.shipping" class="page-customer-order-details-header__addresses-popover-delivery">
|
||||
<div
|
||||
class="page-customer-order-details-header__addresses-popover-data"
|
||||
>
|
||||
<div
|
||||
*ngIf="order.shipping"
|
||||
class="page-customer-order-details-header__addresses-popover-delivery"
|
||||
>
|
||||
<p>Lieferadresse</p>
|
||||
<div class="page-customer-order-details-header__addresses-popover-delivery-data">
|
||||
<div
|
||||
class="page-customer-order-details-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?.firstName }}
|
||||
{{ order.shipping?.data?.lastName }}
|
||||
</p>
|
||||
<p>{{ order.shipping?.data?.address?.info }}</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?.street }}
|
||||
{{ order.shipping?.data?.address?.streetNumber }}
|
||||
</p>
|
||||
<p>
|
||||
{{ order.shipping?.data?.address?.zipCode }}
|
||||
{{ order.shipping?.data?.address?.city }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="order.billing" class="page-customer-order-details-header__addresses-popover-billing">
|
||||
<div
|
||||
*ngIf="order.billing"
|
||||
class="page-customer-order-details-header__addresses-popover-billing"
|
||||
>
|
||||
<p>Rechnungsadresse</p>
|
||||
<div class="page-customer-order-details-header__addresses-popover-billing-data">
|
||||
<div
|
||||
class="page-customer-order-details-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?.firstName }}
|
||||
{{ order.billing?.data?.lastName }}
|
||||
</p>
|
||||
<p>{{ order.billing?.data?.address?.info }}</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?.street }}
|
||||
{{ order.billing?.data?.address?.streetNumber }}
|
||||
</p>
|
||||
<p>
|
||||
{{ order.billing?.data?.address?.zipCode }}
|
||||
{{ order.billing?.data?.address?.city }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-customer-order-details-header__select grow" *ngIf="showMultiselect$ | async">
|
||||
<button class="cta-select-all" (click)="selectAll()">Alle auswählen</button>
|
||||
{{ selectedOrderItemCount$ | async }} von {{ orderItemCount$ | async }} Titeln
|
||||
<div
|
||||
class="page-customer-order-details-header__select grow"
|
||||
*ngIf="showMultiselect$ | async"
|
||||
>
|
||||
<button class="cta-select-all" (click)="selectAll()">
|
||||
Alle auswählen
|
||||
</button>
|
||||
{{ selectedOrderItemCount$ | async }} von
|
||||
{{ orderItemCount$ | async }} Titeln
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,13 +403,20 @@
|
||||
<button
|
||||
[uiOverlayTrigger]="deadlineDatepicker"
|
||||
#deadlineDatepickerTrigger="uiOverlayTrigger"
|
||||
[disabled]="!isKulturpass && (!!orderItem?.features?.paid || (changeDateDisabled$ | async))"
|
||||
[disabled]="
|
||||
!isKulturpass &&
|
||||
(!!orderItem?.features?.paid || (changeDateDisabled$ | async))
|
||||
"
|
||||
class="cta-pickup-deadline"
|
||||
>
|
||||
<strong class="border-r border-[#AEB7C1] pr-4">
|
||||
{{ orderItem?.pickUpDeadline | date: 'dd.MM.yy' }}
|
||||
</strong>
|
||||
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
|
||||
<shared-icon
|
||||
class="text-[#596470] ml-4"
|
||||
[size]="24"
|
||||
icon="isa-calendar"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-datepicker
|
||||
#deadlineDatepicker
|
||||
@@ -282,25 +429,46 @@
|
||||
(save)="updatePickupDeadline($event)"
|
||||
></ui-datepicker>
|
||||
</div>
|
||||
<ui-spinner *ngIf="changeDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
|
||||
<ui-spinner
|
||||
*ngIf="changeDateLoader$ | async; let loader"
|
||||
class="flex flex-row font-bold loader"
|
||||
[show]="loader"
|
||||
></ui-spinner>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #preferredPickUpDate>
|
||||
<div class="min-w-[9rem]">Zurücklegen bis</div>
|
||||
<div *ngIf="!(changePreferredDateLoader$ | async)" class="flex flex-row font-bold">
|
||||
<div
|
||||
*ngIf="!(changePreferredDateLoader$ | async)"
|
||||
class="flex flex-row font-bold"
|
||||
>
|
||||
<button
|
||||
[uiOverlayTrigger]="preferredPickUpDatePicker"
|
||||
#preferredPickUpDatePickerTrigger="uiOverlayTrigger"
|
||||
[disabled]="(!isKulturpass && !!orderItem?.features?.paid) || (changeDateDisabled$ | async)"
|
||||
[disabled]="
|
||||
(!isKulturpass && !!orderItem?.features?.paid) ||
|
||||
(changeDateDisabled$ | async)
|
||||
"
|
||||
class="cta-pickup-preferred"
|
||||
>
|
||||
<strong class="border-r border-[#AEB7C1] pr-4" *ngIf="preferredPickUpDate$ | async; let pickUpDate; else: selectTemplate">
|
||||
<strong
|
||||
class="border-r border-[#AEB7C1] pr-4"
|
||||
*ngIf="
|
||||
preferredPickUpDate$ | async;
|
||||
let pickUpDate;
|
||||
else: selectTemplate
|
||||
"
|
||||
>
|
||||
{{ pickUpDate | date: 'dd.MM.yy' }}
|
||||
</strong>
|
||||
<ng-template #selectTemplate>
|
||||
<strong class="border-r border-[#AEB7C1] pr-4">Auswählen</strong>
|
||||
</ng-template>
|
||||
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
|
||||
<shared-icon
|
||||
class="text-[#596470] ml-4"
|
||||
[size]="24"
|
||||
icon="isa-calendar"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-datepicker
|
||||
#preferredPickUpDatePicker
|
||||
@@ -313,7 +481,11 @@
|
||||
(save)="updatePreferredPickUpDate($event)"
|
||||
></ui-datepicker>
|
||||
</div>
|
||||
<ui-spinner *ngIf="changePreferredDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
|
||||
<ui-spinner
|
||||
*ngIf="changePreferredDateLoader$ | async; let loader"
|
||||
class="flex flex-row font-bold loader"
|
||||
[show]="loader"
|
||||
></ui-spinner>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #vslLieferdatum>
|
||||
@@ -328,7 +500,11 @@
|
||||
<span class="border-r border-[#AEB7C1] pr-4">
|
||||
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
|
||||
</span>
|
||||
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
|
||||
<shared-icon
|
||||
class="text-[#596470] ml-4"
|
||||
[size]="24"
|
||||
icon="isa-calendar"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-datepicker
|
||||
#uiDatepicker
|
||||
@@ -341,6 +517,10 @@
|
||||
(save)="updateEstimatedShippingDate($event)"
|
||||
></ui-datepicker>
|
||||
</div>
|
||||
<ui-spinner *ngIf="changeDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
|
||||
<ui-spinner
|
||||
*ngIf="changeDateLoader$ | async; let loader"
|
||||
class="flex flex-row font-bold loader"
|
||||
[show]="loader"
|
||||
></ui-spinner>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
@@ -11,12 +11,17 @@ import {
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { DomainOmsService } from '@domain/oms';
|
||||
import { NotificationChannel } from '@generated/swagger/checkout-api';
|
||||
import { KeyValueDTOOfStringAndString, OrderDTO, OrderItemListItemDTO } from '@generated/swagger/oms-api';
|
||||
import {
|
||||
KeyValueDTOOfStringAndString,
|
||||
OrderDTO,
|
||||
OrderItemListItemDTO,
|
||||
} from '@generated/swagger/oms-api';
|
||||
import { DateAdapter } from '@ui/common';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { BehaviorSubject, combineLatest } from 'rxjs';
|
||||
import { filter, first, map, shareReplay, switchMap } from 'rxjs/operators';
|
||||
import { CustomerOrderDetailsStore } from '../customer-order-details.store';
|
||||
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-order-details-header',
|
||||
@@ -39,14 +44,21 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
|
||||
return this.order?.features?.orderSource === 'KulturPass';
|
||||
}
|
||||
|
||||
minDateDatepicker = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -1);
|
||||
minDateDatepicker = this.dateAdapter.addCalendarDays(
|
||||
this.dateAdapter.today(),
|
||||
-1,
|
||||
);
|
||||
today = this.dateAdapter.today();
|
||||
|
||||
selectedOrderItemCount$ = this._store.selectedeOrderItemSubsetIds$.pipe(map((ids) => ids?.length ?? 0));
|
||||
selectedOrderItemCount$ = this._store.selectedeOrderItemSubsetIds$.pipe(
|
||||
map((ids) => ids?.length ?? 0),
|
||||
);
|
||||
|
||||
orderItemCount$ = this._store.items$.pipe(map((items) => items?.length ?? 0));
|
||||
|
||||
orderItem$ = this._store.items$.pipe(map((orderItems) => orderItems?.find((_) => true)));
|
||||
orderItem$ = this._store.items$.pipe(
|
||||
map((orderItems) => orderItems?.find((_) => true)),
|
||||
);
|
||||
|
||||
preferredPickUpDate$ = new BehaviorSubject<Date>(undefined);
|
||||
|
||||
@@ -58,37 +70,57 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
|
||||
changeStatusDisabled$ = this._store.changeActionDisabled$;
|
||||
changeDateDisabled$ = this.changeStatusDisabled$;
|
||||
|
||||
features$ = this.orderItem$.pipe(
|
||||
customerFeature$ = this.orderItem$.pipe(
|
||||
filter((orderItem) => !!orderItem),
|
||||
switchMap((orderItem) =>
|
||||
this.customerService.getCustomers(orderItem.buyerNumber).pipe(
|
||||
map((res) => res.result.find((c) => c.customerNumber === orderItem.buyerNumber)),
|
||||
map((customer) => customer?.features || []),
|
||||
map((features) => features.filter((f) => f.enabled && !!f.description)),
|
||||
map((res) =>
|
||||
res.result.find((c) => c.customerNumber === orderItem.buyerNumber),
|
||||
),
|
||||
map((customer) => getEnabledCustomerFeature(customer?.features)),
|
||||
),
|
||||
),
|
||||
shareReplay(),
|
||||
);
|
||||
|
||||
statusActions$ = this.orderItem$.pipe(
|
||||
map((orderItem) => orderItem?.actions?.filter((action) => action.enabled === false)),
|
||||
map((orderItem) =>
|
||||
orderItem?.actions?.filter((action) => action.enabled === false),
|
||||
),
|
||||
);
|
||||
|
||||
showMultiselect$ = combineLatest([this._store.items$, this._store.fetchPartial$, this._store.itemsSelectable$]).pipe(
|
||||
map(([orderItems, fetchPartial, multiSelect]) => multiSelect && fetchPartial && orderItems?.length > 1),
|
||||
showMultiselect$ = combineLatest([
|
||||
this._store.items$,
|
||||
this._store.fetchPartial$,
|
||||
this._store.itemsSelectable$,
|
||||
]).pipe(
|
||||
map(
|
||||
([orderItems, fetchPartial, multiSelect]) =>
|
||||
multiSelect && fetchPartial && orderItems?.length > 1,
|
||||
),
|
||||
);
|
||||
|
||||
crudaUpdate$ = this.orderItem$.pipe(map((orederItem) => !!(orederItem?.cruda & 4)));
|
||||
crudaUpdate$ = this.orderItem$.pipe(
|
||||
map((orederItem) => !!(orederItem?.cruda & 4)),
|
||||
);
|
||||
|
||||
editButtonDisabled$ = combineLatest([this.changeStatusLoader$, this.crudaUpdate$]).pipe(
|
||||
map(([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate),
|
||||
editButtonDisabled$ = combineLatest([
|
||||
this.changeStatusLoader$,
|
||||
this.crudaUpdate$,
|
||||
]).pipe(
|
||||
map(
|
||||
([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate,
|
||||
),
|
||||
);
|
||||
|
||||
canEditStatus$ = combineLatest([this.statusActions$, this.crudaUpdate$]).pipe(
|
||||
map(([statusActions, crudaUpdate]) => statusActions?.length > 0 && crudaUpdate),
|
||||
map(
|
||||
([statusActions, crudaUpdate]) =>
|
||||
statusActions?.length > 0 && crudaUpdate,
|
||||
),
|
||||
);
|
||||
|
||||
openAddresses: boolean = false;
|
||||
openAddresses = false;
|
||||
|
||||
get digOrderNumber(): string {
|
||||
return this.order?.linkedRecords?.find((_) => true)?.number;
|
||||
@@ -96,7 +128,8 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
|
||||
|
||||
get showAddresses(): boolean {
|
||||
return (
|
||||
(this.order?.orderType === 2 || this.order?.orderType === 4) && (!!this.order?.shipping || !!this.order?.billing)
|
||||
(this.order?.orderType === 2 || this.order?.orderType === 4) &&
|
||||
(!!this.order?.shipping || !!this.order?.billing)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -130,10 +163,20 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
|
||||
this.changeStatusDisabled$.next(true);
|
||||
const orderItems = cloneDeep(this._store.items);
|
||||
for (const item of orderItems) {
|
||||
if (this.dateAdapter.compareDate(deadline, new Date(item.pickUpDeadline)) !== 0) {
|
||||
if (
|
||||
this.dateAdapter.compareDate(
|
||||
deadline,
|
||||
new Date(item.pickUpDeadline),
|
||||
) !== 0
|
||||
) {
|
||||
try {
|
||||
const res = await this.omsService
|
||||
.setPickUpDeadline(item.orderId, item.orderItemId, item.orderItemSubsetId, deadline?.toISOString())
|
||||
.setPickUpDeadline(
|
||||
item.orderId,
|
||||
item.orderItemId,
|
||||
item.orderItemSubsetId,
|
||||
deadline?.toISOString(),
|
||||
)
|
||||
.pipe(first())
|
||||
.toPromise();
|
||||
item.pickUpDeadline = deadline.toISOString();
|
||||
@@ -152,7 +195,12 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
|
||||
this.changeStatusDisabled$.next(true);
|
||||
const orderItems = cloneDeep(this._store.items);
|
||||
for (const item of orderItems) {
|
||||
if (this.dateAdapter.compareDate(estimatedShippingDate, new Date(item.pickUpDeadline)) !== 0) {
|
||||
if (
|
||||
this.dateAdapter.compareDate(
|
||||
estimatedShippingDate,
|
||||
new Date(item.pickUpDeadline),
|
||||
) !== 0
|
||||
) {
|
||||
try {
|
||||
const res = await this.omsService
|
||||
.setEstimatedShippingDate(
|
||||
@@ -198,7 +246,10 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
|
||||
try {
|
||||
await this.omsService.setPreferredPickUpDate({ data }).toPromise();
|
||||
this.order.items.forEach((item) => {
|
||||
item.data.subsetItems.forEach((subsetItem) => (subsetItem.data.preferredPickUpDate = date.toISOString()));
|
||||
item.data.subsetItems.forEach(
|
||||
(subsetItem) =>
|
||||
(subsetItem.data.preferredPickUpDate = date.toISOString()),
|
||||
);
|
||||
});
|
||||
this.findLatestPreferredPickUpDate();
|
||||
} catch (error) {
|
||||
@@ -218,7 +269,10 @@ export class CustomerOrderDetailsHeaderComponent implements OnChanges {
|
||||
if (subsetItems?.length > 0) {
|
||||
latestDate = new Date(
|
||||
subsetItems?.reduce((a, b) => {
|
||||
return new Date(a.data.preferredPickUpDate) > new Date(b.data.preferredPickUpDate) ? a : b;
|
||||
return new Date(a.data.preferredPickUpDate) >
|
||||
new Date(b.data.preferredPickUpDate)
|
||||
? a
|
||||
: b;
|
||||
})?.data?.preferredPickUpDate,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { CustomerInfoDTO } from '@generated/swagger/crm-api';
|
||||
import { CustomerLabelColor, CustomerLabelTextColor } from '../../../constants';
|
||||
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-result-list-item-full',
|
||||
@@ -15,11 +16,9 @@ export class CustomerResultListItemFullComponent {
|
||||
customerLabelTextColor = CustomerLabelTextColor;
|
||||
|
||||
get label() {
|
||||
return this.customer?.features?.find((f) => f.enabled);
|
||||
return getEnabledCustomerFeature(this.customer?.features);
|
||||
}
|
||||
|
||||
@Input()
|
||||
customer: CustomerInfoDTO;
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, ChangeDetectionStrategy, Input } from '@angular/core';
|
||||
import { CustomerInfoDTO } from '@generated/swagger/crm-api';
|
||||
import { CustomerLabelColor, CustomerLabelTextColor } from '../../../constants';
|
||||
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-result-list-item',
|
||||
@@ -15,11 +16,9 @@ export class CustomerResultListItemComponent {
|
||||
customerLabelTextColor = CustomerLabelTextColor;
|
||||
|
||||
get label() {
|
||||
return this.customer?.features?.find((f) => f.enabled);
|
||||
return getEnabledCustomerFeature(this.customer?.features);
|
||||
}
|
||||
|
||||
@Input()
|
||||
customer: CustomerInfoDTO;
|
||||
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
@@ -1,92 +1,99 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { UntypedFormBuilder, UntypedFormControl, UntypedFormGroup } from '@angular/forms';
|
||||
import { FormBlock } from '../form-block';
|
||||
import { InterestsFormBlockData } from './interests-form-block-data';
|
||||
import { LoyaltyCardService } from '@generated/swagger/crm-api';
|
||||
import { shareReplay } from 'rxjs/operators';
|
||||
import { isEqual } from 'lodash';
|
||||
import { memorize } from '@utils/common';
|
||||
|
||||
@Component({
|
||||
selector: 'app-interests-form-block',
|
||||
templateUrl: 'interests-form-block.component.html',
|
||||
styleUrls: ['interests-form-block.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class InterestsFormBlockComponent extends FormBlock<InterestsFormBlockData, UntypedFormGroup> {
|
||||
private _interests: Map<string, string>;
|
||||
|
||||
get interests(): Map<string, string> {
|
||||
return this._interests;
|
||||
}
|
||||
set interests(value: Map<string, string>) {
|
||||
if (!isEqual(this._interests, value)) {
|
||||
this._interests = value;
|
||||
if (this.control) {
|
||||
this.updateInterestControls();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get tabIndexEnd() {
|
||||
return this.tabIndexStart + this.interests?.keys.length;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private _fb: UntypedFormBuilder,
|
||||
private _LoyaltyCardService: LoyaltyCardService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.getInterests().subscribe({
|
||||
next: (response) => {
|
||||
const interests = new Map<string, string>();
|
||||
response.result.forEach((preference) => {
|
||||
interests.set(preference.key, preference.value);
|
||||
});
|
||||
this.interests = interests;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@memorize({ ttl: 28800000 })
|
||||
getInterests() {
|
||||
return this._LoyaltyCardService.LoyaltyCardListInteressen().pipe(shareReplay(1));
|
||||
}
|
||||
|
||||
updateInterestControls() {
|
||||
const fData = this.data ?? {};
|
||||
this.interests?.forEach((value, key) => {
|
||||
if (!this.control.contains(key)) {
|
||||
this.control.addControl(key, new UntypedFormControl(fData[key] ?? false));
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(this.control.controls).forEach((key) => {
|
||||
if (!this.interests.has(key)) {
|
||||
this.control.removeControl(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeControl(data?: InterestsFormBlockData): void {
|
||||
const fData = data ?? {};
|
||||
this.control = this._fb.group({});
|
||||
|
||||
this.interests?.forEach((value, key) => {
|
||||
this.control.addControl(key, new UntypedFormControl(fData[key] ?? false));
|
||||
});
|
||||
}
|
||||
|
||||
_patchValue(update: { previous: InterestsFormBlockData; current: InterestsFormBlockData }): void {
|
||||
const fData = update.current ?? {};
|
||||
|
||||
this.interests?.forEach((value, key) => {
|
||||
this.control.get(key).patchValue(fData[key] ?? false);
|
||||
});
|
||||
}
|
||||
}
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import {
|
||||
UntypedFormBuilder,
|
||||
UntypedFormControl,
|
||||
UntypedFormGroup,
|
||||
} from '@angular/forms';
|
||||
import { FormBlock } from '../form-block';
|
||||
import { InterestsFormBlockData } from './interests-form-block-data';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
@Component({
|
||||
selector: 'app-interests-form-block',
|
||||
templateUrl: 'interests-form-block.component.html',
|
||||
styleUrls: ['interests-form-block.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
})
|
||||
export class InterestsFormBlockComponent extends FormBlock<
|
||||
InterestsFormBlockData,
|
||||
UntypedFormGroup
|
||||
> {
|
||||
private _interests: Map<string, string>;
|
||||
|
||||
get interests(): Map<string, string> {
|
||||
return this._interests;
|
||||
}
|
||||
set interests(value: Map<string, string>) {
|
||||
if (!isEqual(this._interests, value)) {
|
||||
this._interests = value;
|
||||
if (this.control) {
|
||||
this.updateInterestControls();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get tabIndexEnd() {
|
||||
return this.tabIndexStart + this.interests?.keys.length;
|
||||
}
|
||||
|
||||
constructor(private _fb: UntypedFormBuilder) {
|
||||
super();
|
||||
|
||||
// this.getInterests().subscribe({
|
||||
// next: (response) => {
|
||||
// const interests = new Map<string, string>();
|
||||
// response.result.forEach((preference) => {
|
||||
// interests.set(preference.key, preference.value);
|
||||
// });
|
||||
// this.interests = interests;
|
||||
// },
|
||||
// error: (error) => {
|
||||
// console.error(error);
|
||||
// },
|
||||
// });
|
||||
}
|
||||
|
||||
// @memorize({ ttl: 28800000 })
|
||||
// getInterests() {
|
||||
// return this._LoyaltyCardService.LoyaltyCardListInteressen().pipe(shareReplay(1));
|
||||
// }
|
||||
|
||||
updateInterestControls() {
|
||||
const fData = this.data ?? {};
|
||||
this.interests?.forEach((value, key) => {
|
||||
if (!this.control.contains(key)) {
|
||||
this.control.addControl(
|
||||
key,
|
||||
new UntypedFormControl(fData[key] ?? false),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Object.keys(this.control.controls).forEach((key) => {
|
||||
if (!this.interests.has(key)) {
|
||||
this.control.removeControl(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initializeControl(data?: InterestsFormBlockData): void {
|
||||
const fData = data ?? {};
|
||||
this.control = this._fb.group({});
|
||||
|
||||
this.interests?.forEach((value, key) => {
|
||||
this.control.addControl(key, new UntypedFormControl(fData[key] ?? false));
|
||||
});
|
||||
}
|
||||
|
||||
_patchValue(update: {
|
||||
previous: InterestsFormBlockData;
|
||||
current: InterestsFormBlockData;
|
||||
}): void {
|
||||
const fData = update.current ?? {};
|
||||
|
||||
this.interests?.forEach((value, key) => {
|
||||
this.control.get(key).patchValue(fData[key] ?? false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<div class="wrapper text-center" [@cardFlip]="state" (@cardFlip.done)="flipAnimationDone($event)">
|
||||
<div
|
||||
class="wrapper text-center"
|
||||
[@cardFlip]="state"
|
||||
(@cardFlip.done)="flipAnimationDone($event)"
|
||||
>
|
||||
@if (cardDetails) {
|
||||
<div class="card-main">
|
||||
<div class="icons text-brand">
|
||||
@@ -36,12 +40,18 @@
|
||||
<div class="barcode-button">
|
||||
@if (!isCustomerCard || (isCustomerCard && !frontside)) {
|
||||
<div class="barcode-field">
|
||||
<img class="barcode" src="/assets/images/barcode.png" alt="Barcode" />
|
||||
<img
|
||||
class="barcode"
|
||||
src="/assets/images/barcode.png"
|
||||
alt="Barcode"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@if (isCustomerCard && frontside) {
|
||||
<div>
|
||||
<button class="button" (click)="onRewardShop()">Zum Prämienshop</button>
|
||||
<button class="button" (click)="navigateToReward()">
|
||||
Zum Prämienshop
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -55,7 +65,11 @@
|
||||
}
|
||||
@if (isCustomerCard && frontside) {
|
||||
<div class="logo ml-2">
|
||||
<img class="logo-picture" src="/assets/images/Hugendubel_Logo.png" alt="Hugendubel Logo" />
|
||||
<img
|
||||
class="logo-picture"
|
||||
src="/assets/images/Hugendubel_Logo.png"
|
||||
alt="Hugendubel Logo"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
||||
import {
|
||||
animate,
|
||||
state,
|
||||
style,
|
||||
transition,
|
||||
trigger,
|
||||
} from '@angular/animations';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnInit, inject } from '@angular/core';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { Router } from '@angular/router';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-kundenkarte',
|
||||
@@ -35,18 +45,45 @@ import { BonusCardInfoDTO } from '@generated/swagger/crm-api';
|
||||
],
|
||||
})
|
||||
export class KundenkarteComponent implements OnInit {
|
||||
#tabId = injectTabId();
|
||||
#router = inject(Router);
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#customerNavigationService = inject(CustomerSearchNavigation);
|
||||
|
||||
@Input() cardDetails: BonusCardInfoDTO;
|
||||
@Input() isCustomerCard: boolean;
|
||||
@Input() customerId: number;
|
||||
|
||||
frontside: boolean;
|
||||
state: 'front' | 'flip' | 'back' = 'front';
|
||||
|
||||
constructor() {}
|
||||
|
||||
ngOnInit() {
|
||||
this.frontside = true;
|
||||
}
|
||||
|
||||
onRewardShop(): void {}
|
||||
async navigateToReward() {
|
||||
const tabId = this.#tabId();
|
||||
const customerId = this.customerId;
|
||||
|
||||
if (!customerId || !tabId) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#navigationState.preserveContext(
|
||||
{
|
||||
returnUrl: `/${tabId}/reward`,
|
||||
autoTriggerContinueFn: true,
|
||||
},
|
||||
'select-customer',
|
||||
);
|
||||
|
||||
await this.#router.navigate(
|
||||
this.#customerNavigationService.detailsRoute({
|
||||
processId: tabId,
|
||||
customerId,
|
||||
}).path,
|
||||
);
|
||||
}
|
||||
|
||||
onDeletePartnerCard(): void {}
|
||||
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { CustomerSearchComponent } from './customer-search.component';
|
||||
import { CustomerResultsSideViewModule } from './results-side-view/results-side-view.module';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CustomerResultsMainViewModule } from './results-main-view/results-main-view.module';
|
||||
import { CustomerDetailsMainViewModule } from './details-main-view/details-main-view.module';
|
||||
import { CustomerHistoryMainViewModule } from './history-main-view/history-main-view.module';
|
||||
import { CustomerFilterMainViewModule } from './filter-main-view/filter-main-view.module';
|
||||
import { MainSideViewModule } from './main-side-view/main-side-view.module';
|
||||
import { OrderDetailsSideViewComponent } from './order-details-side-view/order-details-side-view.component';
|
||||
import { CustomerMainViewComponent } from './main-view/main-view.component';
|
||||
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SharedSplitscreenComponent,
|
||||
CustomerResultsSideViewModule,
|
||||
CustomerResultsMainViewModule,
|
||||
CustomerDetailsMainViewModule,
|
||||
CustomerHistoryMainViewModule,
|
||||
CustomerFilterMainViewModule,
|
||||
MainSideViewModule,
|
||||
OrderDetailsSideViewComponent,
|
||||
CustomerMainViewComponent,
|
||||
],
|
||||
exports: [CustomerSearchComponent],
|
||||
declarations: [CustomerSearchComponent],
|
||||
})
|
||||
export class CustomerSearchModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { CustomerSearchComponent } from './customer-search.component';
|
||||
import { CustomerResultsSideViewModule } from './results-side-view/results-side-view.module';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { CustomerResultsMainViewModule } from './results-main-view/results-main-view.module';
|
||||
import { CustomerDetailsMainViewModule } from './details-main-view/details-main-view.module';
|
||||
import { CustomerHistoryMainViewModule } from './history-main-view/history-main-view.module';
|
||||
import { CustomerFilterMainViewModule } from './filter-main-view/filter-main-view.module';
|
||||
import { MainSideViewModule } from './main-side-view/main-side-view.module';
|
||||
import { OrderDetailsSideViewComponent } from './order-details-side-view/order-details-side-view.component';
|
||||
import { CustomerMainViewComponent } from './main-view/main-view.component';
|
||||
import { SharedSplitscreenComponent } from '@shared/components/splitscreen';
|
||||
import {
|
||||
RewardSelectionService,
|
||||
RewardSelectionPopUpService,
|
||||
} from '@isa/checkout/shared/reward-selection-dialog';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
SharedSplitscreenComponent,
|
||||
CustomerResultsSideViewModule,
|
||||
CustomerResultsMainViewModule,
|
||||
CustomerDetailsMainViewModule,
|
||||
CustomerHistoryMainViewModule,
|
||||
CustomerFilterMainViewModule,
|
||||
MainSideViewModule,
|
||||
OrderDetailsSideViewComponent,
|
||||
CustomerMainViewComponent,
|
||||
],
|
||||
exports: [CustomerSearchComponent],
|
||||
declarations: [CustomerSearchComponent],
|
||||
providers: [
|
||||
RewardSelectionService,
|
||||
RewardSelectionPopUpService,
|
||||
],
|
||||
})
|
||||
export class CustomerSearchModule {}
|
||||
|
||||
@@ -1,218 +1,245 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, Host, inject } from '@angular/core';
|
||||
import { CustomerSearchStore } from '../../store';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { debounceTime, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { Observable, Subject, combineLatest } from 'rxjs';
|
||||
import { AssignedPayerDTO, CustomerDTO, ListResponseArgsOfAssignedPayerDTO } from '@generated/swagger/crm-api';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { CustomerPipesModule } from '@shared/pipes/customer';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CustomerDetailsViewMainComponent } from '../details-main-view.component';
|
||||
import { PayerDTO } from '@generated/swagger/checkout-api';
|
||||
|
||||
interface DetailsMainViewBillingAddressesComponentState {
|
||||
assignedPayers: AssignedPayerDTO[];
|
||||
selectedPayer: AssignedPayerDTO;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-details-main-view-billing-addresses',
|
||||
templateUrl: 'details-main-view-billing-addresses.component.html',
|
||||
styleUrls: ['details-main-view-billing-addresses.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-details-main-view-billing-addresses' },
|
||||
imports: [AsyncPipe, CustomerPipesModule, RouterLink],
|
||||
})
|
||||
export class DetailsMainViewBillingAddressesComponent
|
||||
extends ComponentStore<DetailsMainViewBillingAddressesComponentState>
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private _host = inject(CustomerDetailsViewMainComponent, { host: true });
|
||||
private _store = inject(CustomerSearchStore);
|
||||
private _customerService = inject(CrmCustomerService);
|
||||
private _modal = inject(UiModalService);
|
||||
private _navigation = inject(CustomerSearchNavigation);
|
||||
|
||||
assignedPayers$ = this.select((state) => state.assignedPayers);
|
||||
|
||||
selectedPayer$ = this.select((state) => state.selectedPayer);
|
||||
|
||||
isNotBusinessKonto$ = this._store.isBusinessKonto$.pipe(map((isBusinessKonto) => !isBusinessKonto));
|
||||
|
||||
showCustomerAddress$ = combineLatest([
|
||||
this._store.isBusinessKonto$,
|
||||
this._store.isMitarbeiter$,
|
||||
this._store.isKundenkarte$,
|
||||
]).pipe(map(([isBusinessKonto, isMitarbeiter, isKundenkarte]) => isBusinessKonto || isMitarbeiter || isKundenkarte));
|
||||
|
||||
get showCustomerAddress() {
|
||||
return this._store.isBusinessKonto || this._store.isMitarbeiter;
|
||||
}
|
||||
|
||||
canAddNewAddress$ = combineLatest([
|
||||
this._store.isOnlinekonto$,
|
||||
this._store.isOnlineKontoMitKundenkarte$,
|
||||
this._store.isKundenkarte$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isOnlinekonto, isOnlineKontoMitKundenkarte, isKundenkarte]) =>
|
||||
isOnlinekonto || isOnlineKontoMitKundenkarte || isKundenkarte,
|
||||
),
|
||||
);
|
||||
|
||||
canEditAddress$ = combineLatest([this._store.isKundenkarte$]).pipe(map(([isKundenkarte]) => isKundenkarte));
|
||||
|
||||
customer$ = this._store.customer$;
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
editRoute$ = combineLatest([this._store.processId$, this._store.customerId$, this._store.isBusinessKonto$]).pipe(
|
||||
map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b })),
|
||||
);
|
||||
|
||||
addBillingAddressRoute$ = combineLatest([
|
||||
this.canAddNewAddress$,
|
||||
this._store.processId$,
|
||||
this._store.customerId$,
|
||||
]).pipe(
|
||||
map(([canAddNewAddress, processId, customerId]) =>
|
||||
canAddNewAddress ? this._navigation.addBillingAddressRoute({ processId, customerId }) : undefined,
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
assignedPayers: [],
|
||||
selectedPayer: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
editRoute(payerId: number) {
|
||||
return this._navigation.editBillingAddressRoute({
|
||||
customerId: this._store.customerId,
|
||||
payerId,
|
||||
processId: this._store.processId,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
combineLatest([this._store.customerId$, this._store.isMitarbeiter$])
|
||||
.pipe(takeUntil(this._onDestroy$), debounceTime(250))
|
||||
.subscribe(([customerId, isMitarbeiter]) => {
|
||||
this.resetStore();
|
||||
// #4715 Hier erfolgt ein Check auf Mitarbeiter, da Mitarbeiter keine zusätzlichen Rechnungsadressen haben sollen
|
||||
if (customerId && !isMitarbeiter) {
|
||||
this.loadAssignedPayers(customerId);
|
||||
}
|
||||
});
|
||||
|
||||
combineLatest([this.selectedPayer$, this._store.customer$])
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(([selectedPayer, customer]) => {
|
||||
if (selectedPayer) {
|
||||
this._host.setPayer(this._createPayerFromCrmPayerDTO(selectedPayer));
|
||||
} else if (this.showCustomerAddress) {
|
||||
this._host.setPayer(this._createPayerFormCustomer(customer));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_createPayerFromCrmPayerDTO(assignedPayer: AssignedPayerDTO): PayerDTO {
|
||||
const payer = assignedPayer.payer.data;
|
||||
return {
|
||||
reference: { id: payer.id },
|
||||
payerType: payer.payerType as any,
|
||||
payerNumber: payer.payerNumber,
|
||||
payerStatus: payer.payerStatus,
|
||||
gender: payer.gender,
|
||||
title: payer.title,
|
||||
firstName: payer.firstName,
|
||||
lastName: payer.lastName,
|
||||
communicationDetails: payer.communicationDetails ? { ...payer.communicationDetails } : undefined,
|
||||
organisation: payer.organisation ? { ...payer.organisation } : undefined,
|
||||
address: payer.address ? { ...payer.address } : undefined,
|
||||
source: payer.id,
|
||||
};
|
||||
}
|
||||
|
||||
_createPayerFormCustomer(customer: CustomerDTO): PayerDTO {
|
||||
return {
|
||||
reference: { id: customer.id },
|
||||
payerType: customer.customerType as any,
|
||||
payerNumber: customer.customerNumber,
|
||||
payerStatus: 0,
|
||||
gender: customer.gender,
|
||||
title: customer.title,
|
||||
firstName: customer.firstName,
|
||||
lastName: customer.lastName,
|
||||
communicationDetails: customer.communicationDetails ? { ...customer.communicationDetails } : undefined,
|
||||
organisation: customer.organisation ? { ...customer.organisation } : undefined,
|
||||
address: customer.address ? { ...customer.address } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
loadAssignedPayers = this.effect((customerId$: Observable<number>) =>
|
||||
customerId$.pipe(
|
||||
switchMap((customerId) =>
|
||||
this._customerService
|
||||
.getAssignedPayers({ customerId })
|
||||
.pipe(tapResponse(this.handleLoadAssignedPayersResponse, this.handleLoadAssignedPayersError)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
handleLoadAssignedPayersResponse = (response: ListResponseArgsOfAssignedPayerDTO) => {
|
||||
const selectedPayer = response.result.reduce<AssignedPayerDTO>((prev, curr) => {
|
||||
if (!prev) {
|
||||
return curr;
|
||||
}
|
||||
|
||||
const prevDate = new Date(prev?.isDefault ?? 0);
|
||||
const currDate = new Date(curr?.isDefault ?? 0);
|
||||
|
||||
if (prevDate > currDate) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return curr;
|
||||
}, undefined);
|
||||
|
||||
this.patchState({
|
||||
assignedPayers: response.result,
|
||||
selectedPayer,
|
||||
});
|
||||
};
|
||||
|
||||
handleLoadAssignedPayersError = (err: any) => {
|
||||
this._modal.error('Laden der Rechnungsadressen fehlgeschlagen', err);
|
||||
};
|
||||
|
||||
resetStore() {
|
||||
this.patchState({
|
||||
assignedPayers: [],
|
||||
selectedPayer: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
selectPayer(payer: AssignedPayerDTO) {
|
||||
this.patchState({
|
||||
selectedPayer: payer,
|
||||
});
|
||||
}
|
||||
|
||||
selectCustomerAddress() {
|
||||
this.patchState({
|
||||
selectedPayer: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CustomerSearchStore } from '../../store';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { debounceTime, map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { Observable, Subject, combineLatest } from 'rxjs';
|
||||
import {
|
||||
AssignedPayerDTO,
|
||||
ListResponseArgsOfAssignedPayerDTO,
|
||||
} from '@generated/swagger/crm-api';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { CustomerPipesModule } from '@shared/pipes/customer';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CustomerDetailsViewMainComponent } from '../details-main-view.component';
|
||||
import {
|
||||
AssignedPayer,
|
||||
CrmTabMetadataService,
|
||||
Customer,
|
||||
} from '@isa/crm/data-access';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { CustomerAdapter } from '@isa/checkout/data-access';
|
||||
|
||||
interface DetailsMainViewBillingAddressesComponentState {
|
||||
assignedPayers: AssignedPayerDTO[];
|
||||
selectedPayer: AssignedPayerDTO;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-details-main-view-billing-addresses',
|
||||
templateUrl: 'details-main-view-billing-addresses.component.html',
|
||||
styleUrls: ['details-main-view-billing-addresses.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-details-main-view-billing-addresses' },
|
||||
imports: [AsyncPipe, CustomerPipesModule, RouterLink],
|
||||
})
|
||||
export class DetailsMainViewBillingAddressesComponent
|
||||
extends ComponentStore<DetailsMainViewBillingAddressesComponentState>
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
tabId = injectTabId();
|
||||
crmTabMetadataService = inject(CrmTabMetadataService);
|
||||
|
||||
private _host = inject(CustomerDetailsViewMainComponent, { host: true });
|
||||
private _store = inject(CustomerSearchStore);
|
||||
private _customerService = inject(CrmCustomerService);
|
||||
private _modal = inject(UiModalService);
|
||||
private _navigation = inject(CustomerSearchNavigation);
|
||||
|
||||
assignedPayers$ = this.select((state) => state.assignedPayers);
|
||||
|
||||
selectedPayer$ = this.select((state) => state.selectedPayer);
|
||||
|
||||
isNotBusinessKonto$ = this._store.isBusinessKonto$.pipe(
|
||||
map((isBusinessKonto) => !isBusinessKonto),
|
||||
);
|
||||
|
||||
showCustomerAddress$ = combineLatest([
|
||||
this._store.isBusinessKonto$,
|
||||
this._store.isMitarbeiter$,
|
||||
this._store.isKundenkarte$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isBusinessKonto, isMitarbeiter, isKundenkarte]) =>
|
||||
isBusinessKonto || isMitarbeiter || isKundenkarte,
|
||||
),
|
||||
);
|
||||
|
||||
get showCustomerAddress() {
|
||||
return this._store.isBusinessKonto || this._store.isMitarbeiter;
|
||||
}
|
||||
|
||||
canAddNewAddress$ = combineLatest([
|
||||
this._store.isOnlinekonto$,
|
||||
this._store.isOnlineKontoMitKundenkarte$,
|
||||
this._store.isKundenkarte$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isOnlinekonto, isOnlineKontoMitKundenkarte, isKundenkarte]) =>
|
||||
isOnlinekonto || isOnlineKontoMitKundenkarte || isKundenkarte,
|
||||
),
|
||||
);
|
||||
|
||||
canEditAddress$ = combineLatest([this._store.isKundenkarte$]).pipe(
|
||||
map(([isKundenkarte]) => isKundenkarte),
|
||||
);
|
||||
|
||||
customer$ = this._store.customer$;
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
editRoute$ = combineLatest([
|
||||
this._store.processId$,
|
||||
this._store.customerId$,
|
||||
this._store.isBusinessKonto$,
|
||||
]).pipe(
|
||||
map(([processId, customerId, isB2b]) =>
|
||||
this._navigation.editRoute({ processId, customerId, isB2b }),
|
||||
),
|
||||
);
|
||||
|
||||
addBillingAddressRoute$ = combineLatest([
|
||||
this.canAddNewAddress$,
|
||||
this._store.processId$,
|
||||
this._store.customerId$,
|
||||
]).pipe(
|
||||
map(([canAddNewAddress, processId, customerId]) =>
|
||||
canAddNewAddress
|
||||
? this._navigation.addBillingAddressRoute({ processId, customerId })
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
assignedPayers: [],
|
||||
selectedPayer: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
editRoute(payerId: number) {
|
||||
return this._navigation.editBillingAddressRoute({
|
||||
customerId: this._store.customerId,
|
||||
payerId,
|
||||
processId: this._store.processId,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
combineLatest([this._store.customerId$, this._store.isMitarbeiter$])
|
||||
.pipe(takeUntil(this._onDestroy$), debounceTime(250))
|
||||
.subscribe(([customerId, isMitarbeiter]) => {
|
||||
this.resetStore();
|
||||
// #4715 Hier erfolgt ein Check auf Mitarbeiter, da Mitarbeiter keine zusätzlichen Rechnungsadressen haben sollen
|
||||
if (customerId && !isMitarbeiter) {
|
||||
this.loadAssignedPayers(customerId);
|
||||
}
|
||||
});
|
||||
|
||||
combineLatest([this.selectedPayer$, this._store.customer$])
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(([selectedPayer, customer]) => {
|
||||
if (selectedPayer) {
|
||||
const payer = CustomerAdapter.toPayerFromAssignedPayer(
|
||||
selectedPayer as AssignedPayer,
|
||||
);
|
||||
if (payer) {
|
||||
this._host.setPayer(payer);
|
||||
this.crmTabMetadataService.setSelectedPayerId(
|
||||
this.tabId(),
|
||||
selectedPayer?.payer?.id,
|
||||
);
|
||||
}
|
||||
} else if (this.showCustomerAddress) {
|
||||
this._host.setPayer(
|
||||
CustomerAdapter.toPayerFromCustomer(
|
||||
customer as unknown as Customer,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
loadAssignedPayers = this.effect((customerId$: Observable<number>) =>
|
||||
customerId$.pipe(
|
||||
switchMap((customerId) =>
|
||||
this._customerService
|
||||
.getAssignedPayers({ customerId })
|
||||
.pipe(
|
||||
tapResponse(
|
||||
this.handleLoadAssignedPayersResponse,
|
||||
this.handleLoadAssignedPayersError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
handleLoadAssignedPayersResponse = (
|
||||
response: ListResponseArgsOfAssignedPayerDTO,
|
||||
) => {
|
||||
const selectedPayer = response.result.reduce<AssignedPayerDTO>(
|
||||
(prev, curr) => {
|
||||
if (!prev) {
|
||||
return curr;
|
||||
}
|
||||
|
||||
const prevDate = new Date(prev?.isDefault ?? 0);
|
||||
const currDate = new Date(curr?.isDefault ?? 0);
|
||||
|
||||
if (prevDate > currDate) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return curr;
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
this.patchState({
|
||||
assignedPayers: response.result,
|
||||
selectedPayer,
|
||||
});
|
||||
};
|
||||
|
||||
handleLoadAssignedPayersError = (err: unknown) => {
|
||||
this._modal.error(
|
||||
'Laden der Rechnungsadressen fehlgeschlagen',
|
||||
err as Error,
|
||||
);
|
||||
};
|
||||
|
||||
resetStore() {
|
||||
this.patchState({
|
||||
assignedPayers: [],
|
||||
selectedPayer: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
selectPayer(payer: AssignedPayerDTO) {
|
||||
this.patchState({
|
||||
selectedPayer: payer,
|
||||
});
|
||||
}
|
||||
|
||||
selectCustomerAddress() {
|
||||
this.patchState({
|
||||
selectedPayer: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,217 +1,267 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, Host, inject } from '@angular/core';
|
||||
import { CustomerSearchStore } from '../../store';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { Observable, Subject, combineLatest } from 'rxjs';
|
||||
import { CustomerDTO, ListResponseArgsOfAssignedPayerDTO, ShippingAddressDTO } from '@generated/swagger/crm-api';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { CustomerPipesModule } from '@shared/pipes/customer';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CustomerDetailsViewMainComponent } from '../details-main-view.component';
|
||||
|
||||
interface DetailsMainViewDeliveryAddressesComponentState {
|
||||
shippingAddresses: ShippingAddressDTO[];
|
||||
selectedShippingAddress: ShippingAddressDTO;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-details-main-view-delivery-addresses',
|
||||
templateUrl: 'details-main-view-delivery-addresses.component.html',
|
||||
styleUrls: ['details-main-view-delivery-addresses.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-details-main-view-delivery-addresses' },
|
||||
imports: [AsyncPipe, CustomerPipesModule, RouterLink],
|
||||
})
|
||||
export class DetailsMainViewDeliveryAddressesComponent
|
||||
extends ComponentStore<DetailsMainViewDeliveryAddressesComponentState>
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
private _host = inject(CustomerDetailsViewMainComponent, { host: true });
|
||||
private _store = inject(CustomerSearchStore);
|
||||
private _customerService = inject(CrmCustomerService);
|
||||
private _modal = inject(UiModalService);
|
||||
private _navigation = inject(CustomerSearchNavigation);
|
||||
|
||||
shippingAddresses$ = this.select((state) => state.shippingAddresses);
|
||||
|
||||
selectedShippingAddress$ = this.select((state) => state.selectedShippingAddress);
|
||||
|
||||
get selectedShippingAddress() {
|
||||
return this.get((s) => s.selectedShippingAddress);
|
||||
}
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
showCustomerAddress$ = combineLatest([
|
||||
this._store.isBusinessKonto$,
|
||||
this._store.isMitarbeiter$,
|
||||
this._store.isKundenkarte$,
|
||||
]).pipe(map(([isBusinessKonto, isMitarbeiter, isKundenkarte]) => isBusinessKonto || isMitarbeiter || isKundenkarte));
|
||||
|
||||
get showCustomerAddress() {
|
||||
return this._store.isBusinessKonto || this._store.isMitarbeiter;
|
||||
}
|
||||
|
||||
customer$ = this._store.customer$;
|
||||
|
||||
canAddNewAddress$ = combineLatest([
|
||||
this._store.isOnlinekonto$,
|
||||
this._store.isOnlineKontoMitKundenkarte$,
|
||||
this._store.isKundenkarte$,
|
||||
this._store.isBusinessKonto$,
|
||||
this._store.isMitarbeiter$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isOnlinekonto, isOnlineKontoMitKundenkarte, isKundenkarte, isBusinessKonto, isMitarbeiter]) =>
|
||||
isOnlinekonto || isOnlineKontoMitKundenkarte || isKundenkarte || isBusinessKonto || isMitarbeiter,
|
||||
),
|
||||
);
|
||||
|
||||
editRoute$ = combineLatest([this._store.processId$, this._store.customerId$, this._store.isBusinessKonto$]).pipe(
|
||||
map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b })),
|
||||
);
|
||||
|
||||
addShippingAddressRoute$ = combineLatest([
|
||||
this.canAddNewAddress$,
|
||||
this._store.processId$,
|
||||
this._store.customerId$,
|
||||
]).pipe(
|
||||
map(([canAddNewAddress, processId, customerId]) =>
|
||||
canAddNewAddress ? this._navigation.addShippingAddressRoute({ processId, customerId }) : undefined,
|
||||
),
|
||||
);
|
||||
|
||||
editShippingAddressRoute$ = (shippingAddressId: number) =>
|
||||
combineLatest([this.canEditAddress$, this._store.processId$, this._store.customerId$]).pipe(
|
||||
map(([canEditAddress, processId, customerId]) => {
|
||||
if (canEditAddress) {
|
||||
return this._navigation.editShippingAddressRoute({ processId, customerId, shippingAddressId });
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
);
|
||||
|
||||
canEditAddress$ = combineLatest([
|
||||
this._store.isKundenkarte$,
|
||||
this._store.isBusinessKonto$,
|
||||
this._store.isMitarbeiter$,
|
||||
]).pipe(map(([isKundenkarte, isBusinessKonto, isMitarbeiter]) => isKundenkarte || isBusinessKonto || isMitarbeiter));
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
shippingAddresses: [],
|
||||
selectedShippingAddress: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._store.customerId$.pipe(takeUntil(this._onDestroy$)).subscribe((customerId) => {
|
||||
this.resetStore();
|
||||
if (customerId) {
|
||||
this.loadShippingAddresses(customerId);
|
||||
}
|
||||
});
|
||||
|
||||
combineLatest([this.selectedShippingAddress$, this._store.customer$])
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(([selectedShippingAddress, customer]) => {
|
||||
if (selectedShippingAddress) {
|
||||
this._host.setShippingAddress(this._createShippingAddressFromShippingAddress(selectedShippingAddress));
|
||||
} else if (this.showCustomerAddress) {
|
||||
this._host.setShippingAddress(this._createShippingAddressFromCustomer(customer));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
_createShippingAddressFromCustomer(customer: CustomerDTO) {
|
||||
return {
|
||||
reference: { id: customer.id },
|
||||
gender: customer?.gender,
|
||||
title: customer?.title,
|
||||
firstName: customer?.firstName,
|
||||
lastName: customer?.lastName,
|
||||
communicationDetails: customer?.communicationDetails ? { ...customer?.communicationDetails } : undefined,
|
||||
organisation: customer?.organisation ? { ...customer?.organisation } : undefined,
|
||||
address: customer?.address ? { ...customer?.address } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
_createShippingAddressFromShippingAddress(address: ShippingAddressDTO) {
|
||||
return {
|
||||
reference: { id: address.id },
|
||||
gender: address.gender,
|
||||
title: address.title,
|
||||
firstName: address.firstName,
|
||||
lastName: address.lastName,
|
||||
communicationDetails: address.communicationDetails ? { ...address.communicationDetails } : undefined,
|
||||
organisation: address.organisation ? { ...address.organisation } : undefined,
|
||||
address: address.address ? { ...address.address } : undefined,
|
||||
source: address.id,
|
||||
};
|
||||
}
|
||||
|
||||
loadShippingAddresses = this.effect((customerId$: Observable<number>) =>
|
||||
customerId$.pipe(
|
||||
switchMap((customerId) =>
|
||||
this._customerService
|
||||
.getShippingAddresses({ customerId })
|
||||
.pipe(tapResponse(this.handleLoadShippingAddressesResponse, this.handleLoadAssignedPayersError)),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
handleLoadShippingAddressesResponse = (response: ListResponseArgsOfAssignedPayerDTO) => {
|
||||
const selectedShippingAddress = response.result.reduce<ShippingAddressDTO>((prev, curr) => {
|
||||
if (!this.showCustomerAddress && !prev) {
|
||||
return curr;
|
||||
}
|
||||
|
||||
const prevDate = new Date(prev?.isDefault ?? 0);
|
||||
const currDate = new Date(curr?.isDefault ?? 0);
|
||||
|
||||
if (prevDate > currDate) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return curr;
|
||||
}, undefined);
|
||||
|
||||
this.patchState({
|
||||
shippingAddresses: response.result,
|
||||
selectedShippingAddress,
|
||||
});
|
||||
};
|
||||
|
||||
handleLoadAssignedPayersError = (err: any) => {
|
||||
this._modal.error('Laden der Lieferadressen fehlgeschlagen', err);
|
||||
};
|
||||
|
||||
resetStore() {
|
||||
this.patchState({
|
||||
shippingAddresses: [],
|
||||
selectedShippingAddress: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
selectShippingAddress(shippingAddress: ShippingAddressDTO) {
|
||||
this.patchState({
|
||||
selectedShippingAddress: shippingAddress,
|
||||
});
|
||||
}
|
||||
|
||||
selectCustomerAddress() {
|
||||
this.patchState({
|
||||
selectedShippingAddress: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CustomerSearchStore } from '../../store';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { map, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { Observable, Subject, combineLatest } from 'rxjs';
|
||||
import {
|
||||
ListResponseArgsOfAssignedPayerDTO,
|
||||
ShippingAddressDTO,
|
||||
} from '@generated/swagger/crm-api';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import { CustomerPipesModule } from '@shared/pipes/customer';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import { UiModalService } from '@ui/modal';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { CustomerDetailsViewMainComponent } from '../details-main-view.component';
|
||||
import { CrmTabMetadataService, Customer } from '@isa/crm/data-access';
|
||||
import { injectTabId } from '@isa/core/tabs';
|
||||
import { ShippingAddressAdapter } from '@isa/checkout/data-access';
|
||||
|
||||
interface DetailsMainViewDeliveryAddressesComponentState {
|
||||
shippingAddresses: ShippingAddressDTO[];
|
||||
selectedShippingAddress: ShippingAddressDTO;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'page-details-main-view-delivery-addresses',
|
||||
templateUrl: 'details-main-view-delivery-addresses.component.html',
|
||||
styleUrls: ['details-main-view-delivery-addresses.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-details-main-view-delivery-addresses' },
|
||||
imports: [AsyncPipe, CustomerPipesModule, RouterLink],
|
||||
})
|
||||
export class DetailsMainViewDeliveryAddressesComponent
|
||||
extends ComponentStore<DetailsMainViewDeliveryAddressesComponentState>
|
||||
implements OnInit, OnDestroy
|
||||
{
|
||||
tabId = injectTabId();
|
||||
crmTabMetadataService = inject(CrmTabMetadataService);
|
||||
|
||||
private _host = inject(CustomerDetailsViewMainComponent, { host: true });
|
||||
private _store = inject(CustomerSearchStore);
|
||||
private _customerService = inject(CrmCustomerService);
|
||||
private _modal = inject(UiModalService);
|
||||
private _navigation = inject(CustomerSearchNavigation);
|
||||
|
||||
shippingAddresses$ = this.select((state) => state.shippingAddresses);
|
||||
|
||||
selectedShippingAddress$ = this.select(
|
||||
(state) => state.selectedShippingAddress,
|
||||
);
|
||||
|
||||
get selectedShippingAddress() {
|
||||
return this.get((s) => s.selectedShippingAddress);
|
||||
}
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
showCustomerAddress$ = combineLatest([
|
||||
this._store.isBusinessKonto$,
|
||||
this._store.isMitarbeiter$,
|
||||
this._store.isKundenkarte$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isBusinessKonto, isMitarbeiter, isKundenkarte]) =>
|
||||
isBusinessKonto || isMitarbeiter || isKundenkarte,
|
||||
),
|
||||
);
|
||||
|
||||
get showCustomerAddress() {
|
||||
return this._store.isBusinessKonto || this._store.isMitarbeiter;
|
||||
}
|
||||
|
||||
customer$ = this._store.customer$;
|
||||
|
||||
canAddNewAddress$ = combineLatest([
|
||||
this._store.isOnlinekonto$,
|
||||
this._store.isOnlineKontoMitKundenkarte$,
|
||||
this._store.isKundenkarte$,
|
||||
this._store.isBusinessKonto$,
|
||||
this._store.isMitarbeiter$,
|
||||
]).pipe(
|
||||
map(
|
||||
([
|
||||
isOnlinekonto,
|
||||
isOnlineKontoMitKundenkarte,
|
||||
isKundenkarte,
|
||||
isBusinessKonto,
|
||||
isMitarbeiter,
|
||||
]) =>
|
||||
isOnlinekonto ||
|
||||
isOnlineKontoMitKundenkarte ||
|
||||
isKundenkarte ||
|
||||
isBusinessKonto ||
|
||||
isMitarbeiter,
|
||||
),
|
||||
);
|
||||
|
||||
editRoute$ = combineLatest([
|
||||
this._store.processId$,
|
||||
this._store.customerId$,
|
||||
this._store.isBusinessKonto$,
|
||||
]).pipe(
|
||||
map(([processId, customerId, isB2b]) =>
|
||||
this._navigation.editRoute({ processId, customerId, isB2b }),
|
||||
),
|
||||
);
|
||||
|
||||
addShippingAddressRoute$ = combineLatest([
|
||||
this.canAddNewAddress$,
|
||||
this._store.processId$,
|
||||
this._store.customerId$,
|
||||
]).pipe(
|
||||
map(([canAddNewAddress, processId, customerId]) =>
|
||||
canAddNewAddress
|
||||
? this._navigation.addShippingAddressRoute({ processId, customerId })
|
||||
: undefined,
|
||||
),
|
||||
);
|
||||
|
||||
editShippingAddressRoute$ = (shippingAddressId: number) =>
|
||||
combineLatest([
|
||||
this.canEditAddress$,
|
||||
this._store.processId$,
|
||||
this._store.customerId$,
|
||||
]).pipe(
|
||||
map(([canEditAddress, processId, customerId]) => {
|
||||
if (canEditAddress) {
|
||||
return this._navigation.editShippingAddressRoute({
|
||||
processId,
|
||||
customerId,
|
||||
shippingAddressId,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}),
|
||||
);
|
||||
|
||||
canEditAddress$ = combineLatest([
|
||||
this._store.isKundenkarte$,
|
||||
this._store.isBusinessKonto$,
|
||||
this._store.isMitarbeiter$,
|
||||
]).pipe(
|
||||
map(
|
||||
([isKundenkarte, isBusinessKonto, isMitarbeiter]) =>
|
||||
isKundenkarte || isBusinessKonto || isMitarbeiter,
|
||||
),
|
||||
);
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
shippingAddresses: [],
|
||||
selectedShippingAddress: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this._store.customerId$
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe((customerId) => {
|
||||
this.resetStore();
|
||||
if (customerId) {
|
||||
this.loadShippingAddresses(customerId);
|
||||
}
|
||||
});
|
||||
|
||||
combineLatest([this.selectedShippingAddress$, this._store.customer$])
|
||||
.pipe(takeUntil(this._onDestroy$))
|
||||
.subscribe(([selectedShippingAddress, customer]) => {
|
||||
if (selectedShippingAddress) {
|
||||
this._host.setShippingAddress(
|
||||
ShippingAddressAdapter.fromCrmShippingAddress(
|
||||
selectedShippingAddress,
|
||||
),
|
||||
);
|
||||
this.crmTabMetadataService.setSelectedShippingAddressId(
|
||||
this.tabId(),
|
||||
selectedShippingAddress?.id,
|
||||
);
|
||||
} else if (this.showCustomerAddress) {
|
||||
this._host.setShippingAddress(
|
||||
ShippingAddressAdapter.fromCustomer(
|
||||
customer as unknown as Customer,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._onDestroy$.next();
|
||||
this._onDestroy$.complete();
|
||||
}
|
||||
|
||||
loadShippingAddresses = this.effect((customerId$: Observable<number>) =>
|
||||
customerId$.pipe(
|
||||
switchMap((customerId) =>
|
||||
this._customerService
|
||||
.getShippingAddresses({ customerId })
|
||||
.pipe(
|
||||
tapResponse(
|
||||
this.handleLoadShippingAddressesResponse,
|
||||
this.handleLoadAssignedPayersError,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
handleLoadShippingAddressesResponse = (
|
||||
response: ListResponseArgsOfAssignedPayerDTO,
|
||||
) => {
|
||||
const selectedShippingAddress = response.result.reduce<ShippingAddressDTO>(
|
||||
(prev, curr) => {
|
||||
if (!this.showCustomerAddress && !prev) {
|
||||
return curr;
|
||||
}
|
||||
|
||||
const prevDate = new Date(prev?.isDefault ?? 0);
|
||||
const currDate = new Date(curr?.isDefault ?? 0);
|
||||
|
||||
if (prevDate > currDate) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return curr;
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
|
||||
this.patchState({
|
||||
shippingAddresses: response.result,
|
||||
selectedShippingAddress,
|
||||
});
|
||||
};
|
||||
|
||||
handleLoadAssignedPayersError = (err: unknown) => {
|
||||
this._modal.error('Laden der Lieferadressen fehlgeschlagen', err as Error);
|
||||
};
|
||||
|
||||
resetStore() {
|
||||
this.patchState({
|
||||
shippingAddresses: [],
|
||||
selectedShippingAddress: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
selectShippingAddress(shippingAddress: ShippingAddressDTO) {
|
||||
this.patchState({
|
||||
selectedShippingAddress: shippingAddress,
|
||||
});
|
||||
}
|
||||
|
||||
selectCustomerAddress() {
|
||||
this.patchState({
|
||||
selectedShippingAddress: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,185 +1,198 @@
|
||||
<shared-loader [loading]="fetching$ | async" background="true" spinnerSize="32">
|
||||
<div class="overflow-scroll max-h-[calc(100vh-15rem)]">
|
||||
<div class="customer-details-header grid grid-flow-row pb-6">
|
||||
<div class="customer-details-header-actions flex flex-row justify-end pt-4 px-4">
|
||||
<page-customer-menu
|
||||
[customerId]="customerId$ | async"
|
||||
[processId]="processId$ | async"
|
||||
[hasCustomerCard]="hasKundenkarte$ | async"
|
||||
[showCustomerDetails]="false"
|
||||
></page-customer-menu>
|
||||
</div>
|
||||
<div class="customer-details-header-body text-center -mt-3">
|
||||
<h1 class="text-[1.625rem] font-bold">
|
||||
{{ (isBusinessKonto$ | async) ? 'Firmendetails' : 'Kundendetails' }}
|
||||
</h1>
|
||||
<p>Sind Ihre Kundendaten korrekt?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content h-14">
|
||||
<div class="pl-4 font-bold grid grid-flow-col justify-start items-center gap-2">
|
||||
<shared-icon [icon]="customerType$ | async"></shared-icon>
|
||||
<span>
|
||||
{{ customerType$ | async }}
|
||||
</span>
|
||||
</div>
|
||||
@if (showEditButton$ | async) {
|
||||
@if (editRoute$ | async; as editRoute) {
|
||||
<a
|
||||
[routerLink]="editRoute.path"
|
||||
[queryParams]="editRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
class="btn btn-label font-bold text-brand"
|
||||
>
|
||||
Bearbeiten
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3">
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Erstellungsdatum</div>
|
||||
@if (created$ | async; as created) {
|
||||
<div class="data-value">
|
||||
{{ created | date: 'dd.MM.yyyy' }} | {{ created | date: 'HH:mm' }} Uhr
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Kundennummer</div>
|
||||
<div class="data-value">{{ customerNumber$ | async }}</div>
|
||||
</div>
|
||||
@if (customerNumberDig$ | async; as customerNumberDig) {
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Kundennummer-DIG</div>
|
||||
<div class="data-value">{{ customerNumberDig }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (customerNumberBeeline$ | async; as customerNumberBeeline) {
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Kundennummer-BEELINE</div>
|
||||
<div class="data-value">{{ customerNumberBeeline }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (isBusinessKonto$ | async) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Firmenname</div>
|
||||
<div class="data-value">{{ organisationName$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Abteilung</div>
|
||||
<div class="data-value">{{ department$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">USt-ID</div>
|
||||
<div class="data-value">{{ vatId$ | async }}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Anrede</div>
|
||||
<div class="data-value">{{ gender$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Titel</div>
|
||||
<div class="data-value">{{ title$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Nachname</div>
|
||||
<div class="data-value">{{ lastName$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Vorname</div>
|
||||
<div class="data-value">{{ firstName$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">E-Mail</div>
|
||||
<div class="data-value">{{ email$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Straße</div>
|
||||
<div class="data-value">{{ street$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Hausnr.</div>
|
||||
<div class="data-value">{{ streetNumber$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">PLZ</div>
|
||||
<div class="data-value">{{ zipCode$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Ort</div>
|
||||
<div class="data-value">{{ city$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Adresszusatz</div>
|
||||
<div class="data-value">{{ info$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Land</div>
|
||||
@if (country$ | async; as country) {
|
||||
<div class="data-value">{{ country | country }}</div>
|
||||
}
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Festnetznr.</div>
|
||||
<div class="data-value">{{ landline$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Mobilnr.</div>
|
||||
<div class="data-value">{{ mobile$ | async }}</div>
|
||||
</div>
|
||||
|
||||
@if (!(isBusinessKonto$ | async)) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Geburtstag</div>
|
||||
<div class="data-value">{{ dateOfBirth$ | async | date: 'dd.MM.yyyy' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (!(isBusinessKonto$ | async) && (organisationName$ | async)) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Firmenname</div>
|
||||
<div class="data-value">{{ organisationName$ | async }}</div>
|
||||
</div>
|
||||
@if (!(isOnlineOrCustomerCardUser$ | async)) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Abteilung</div>
|
||||
<div class="data-value">{{ department$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">USt-ID</div>
|
||||
<div class="data-value">{{ vatId$ | async }}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<page-details-main-view-billing-addresses></page-details-main-view-billing-addresses>
|
||||
<page-details-main-view-delivery-addresses></page-details-main-view-delivery-addresses>
|
||||
<div class="h-24"></div>
|
||||
</div>
|
||||
</shared-loader>
|
||||
|
||||
@if (shoppingCartHasNoItems$ | async) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="continue()"
|
||||
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
|
||||
[disabled]="showLoader$ | async"
|
||||
>
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zur Artikelsuche</shared-loader>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (shoppingCartHasItems$ | async) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="continue()"
|
||||
class="text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch"
|
||||
[disabled]="showLoader$ | async"
|
||||
>
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">Weiter zum Warenkorb</shared-loader>
|
||||
</button>
|
||||
}
|
||||
<shared-loader [loading]="fetching$ | async" background="true" spinnerSize="32">
|
||||
<div class="overflow-scroll max-h-[calc(100vh-15rem)]">
|
||||
<div class="customer-details-header grid grid-flow-row pb-6">
|
||||
<div
|
||||
class="customer-details-header-actions flex flex-row justify-end pt-4 px-4"
|
||||
>
|
||||
<page-customer-menu
|
||||
[customerId]="customerId$ | async"
|
||||
[processId]="processId$ | async"
|
||||
[hasCustomerCard]="hasKundenkarte$ | async"
|
||||
[showCustomerDetails]="false"
|
||||
></page-customer-menu>
|
||||
</div>
|
||||
<div class="customer-details-header-body text-center -mt-3">
|
||||
<h1 class="text-[1.625rem] font-bold">
|
||||
{{ (isBusinessKonto$ | async) ? 'Firmendetails' : 'Kundendetails' }}
|
||||
</h1>
|
||||
<p>Sind Ihre Kundendaten korrekt?</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="customer-details-customer-type flex flex-row justify-between items-center bg-surface-2 text-surface-2-content h-14"
|
||||
>
|
||||
<div
|
||||
class="pl-4 font-bold grid grid-flow-col justify-start items-center gap-2"
|
||||
>
|
||||
<shared-icon [icon]="customerType$ | async"></shared-icon>
|
||||
<span>
|
||||
{{ customerType$ | async }}
|
||||
</span>
|
||||
</div>
|
||||
@if (showEditButton$ | async) {
|
||||
@if (editRoute$ | async; as editRoute) {
|
||||
<a
|
||||
[routerLink]="editRoute.path"
|
||||
[queryParams]="editRoute.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
class="btn btn-label font-bold text-brand"
|
||||
>
|
||||
Bearbeiten
|
||||
</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="customer-details-customer-main-data px-5 py-3 grid grid-flow-row gap-3"
|
||||
>
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Erstellungsdatum</div>
|
||||
@if (created$ | async; as created) {
|
||||
<div class="data-value">
|
||||
{{ created | date: 'dd.MM.yyyy' }} |
|
||||
{{ created | date: 'HH:mm' }} Uhr
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Kundennummer</div>
|
||||
<div class="data-value">{{ customerNumber$ | async }}</div>
|
||||
</div>
|
||||
@if (customerNumberDig$ | async; as customerNumberDig) {
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Kundennummer-DIG</div>
|
||||
<div class="data-value">{{ customerNumberDig }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (customerNumberBeeline$ | async; as customerNumberBeeline) {
|
||||
<div class="flex flex-row">
|
||||
<div class="data-label">Kundennummer-BEELINE</div>
|
||||
<div class="data-value">{{ customerNumberBeeline }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (isBusinessKonto$ | async) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Firmenname</div>
|
||||
<div class="data-value">{{ organisationName$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Abteilung</div>
|
||||
<div class="data-value">{{ department$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">USt-ID</div>
|
||||
<div class="data-value">{{ vatId$ | async }}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Anrede</div>
|
||||
<div class="data-value">{{ gender$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Titel</div>
|
||||
<div class="data-value">{{ title$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Nachname</div>
|
||||
<div class="data-value">{{ lastName$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Vorname</div>
|
||||
<div class="data-value">{{ firstName$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">E-Mail</div>
|
||||
<div class="data-value">{{ email$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Straße</div>
|
||||
<div class="data-value">{{ street$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Hausnr.</div>
|
||||
<div class="data-value">{{ streetNumber$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">PLZ</div>
|
||||
<div class="data-value">{{ zipCode$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Ort</div>
|
||||
<div class="data-value">{{ city$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Adresszusatz</div>
|
||||
<div class="data-value">{{ info$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Land</div>
|
||||
@if (country$ | async; as country) {
|
||||
<div class="data-value">{{ country | country }}</div>
|
||||
}
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Festnetznr.</div>
|
||||
<div class="data-value">{{ landline$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Mobilnr.</div>
|
||||
<div class="data-value">{{ mobile$ | async }}</div>
|
||||
</div>
|
||||
|
||||
@if (!(isBusinessKonto$ | async)) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Geburtstag</div>
|
||||
<div class="data-value">
|
||||
{{ dateOfBirth$ | async | date: 'dd.MM.yyyy' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (!(isBusinessKonto$ | async) && (organisationName$ | async)) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Firmenname</div>
|
||||
<div class="data-value">{{ organisationName$ | async }}</div>
|
||||
</div>
|
||||
@if (!(isOnlineOrCustomerCardUser$ | async)) {
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">Abteilung</div>
|
||||
<div class="data-value">{{ department$ | async }}</div>
|
||||
</div>
|
||||
<div class="customer-details-customer-main-row">
|
||||
<div class="data-label">USt-ID</div>
|
||||
<div class="data-value">{{ vatId$ | async }}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<page-details-main-view-billing-addresses></page-details-main-view-billing-addresses>
|
||||
<page-details-main-view-delivery-addresses></page-details-main-view-delivery-addresses>
|
||||
<div class="h-24"></div>
|
||||
</div>
|
||||
</shared-loader>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="continue()"
|
||||
[class]="
|
||||
'text-white text-lg bg-brand rounded-full px-5 py-3 absolute top-[calc(100vh-19.375rem)] left-1/2 -translate-x-1/2 font-bold disabled:bg-inactive-branch' +
|
||||
(hasReturnUrl() ? ' w-60' : '')
|
||||
"
|
||||
[disabled]="
|
||||
hasReturnUrl()
|
||||
? !(hasKundenkarte$ | async) || (showLoader$ | async)
|
||||
: (showLoader$ | async)
|
||||
"
|
||||
>
|
||||
<shared-loader [loading]="showLoader$ | async" spinnerSize="32">
|
||||
@if (hasReturnUrl()) {
|
||||
Auswählen
|
||||
} @else if (shoppingCartHasItems$ | async) {
|
||||
Weiter zum Warenkorb
|
||||
} @else {
|
||||
Weiter zur Artikelsuche
|
||||
}
|
||||
</shared-loader>
|
||||
</button>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
||||
class="justify-self-center"
|
||||
[cardDetails]="karte"
|
||||
[isCustomerCard]="true"
|
||||
[customerId]="customerId$ | async"
|
||||
></page-customer-kundenkarte>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, inject } from '@angular/core';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
OnInit,
|
||||
OnDestroy,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { CustomerSearchStore } from '../store';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { map, takeUntil, tap } from 'rxjs/operators';
|
||||
@@ -21,6 +27,7 @@ import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { CustomerOrderItemListItemComponent } from './order-item-list-item/order-item-list-item.component';
|
||||
import { groupBy } from '@ui/common';
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-order-details-main-view',
|
||||
@@ -52,38 +59,58 @@ export class OrderDetailsMainViewComponent implements OnInit, OnDestroy {
|
||||
private _navigation = inject(CustomerSearchNavigation);
|
||||
private _env = inject(EnvironmentService);
|
||||
|
||||
orderId$ = this._activateRoute.params.pipe(map((params) => Number(params.orderId)));
|
||||
orderId$ = this._activateRoute.params.pipe(
|
||||
map((params) => Number(params.orderId)),
|
||||
);
|
||||
|
||||
order$ = this._store.order$;
|
||||
|
||||
orderTargetBranch$ = this.order$.pipe(map((order) => order?.targetBranch?.id));
|
||||
orderTargetBranch$ = this.order$.pipe(
|
||||
map((order) => order?.targetBranch?.id),
|
||||
);
|
||||
|
||||
orderShippingTarget$ = this.order$.pipe(map((order) => order?.shipping?.data));
|
||||
orderShippingTarget$ = this.order$.pipe(
|
||||
map((order) => order?.shipping?.data),
|
||||
);
|
||||
|
||||
customerId$ = this._activateRoute.params.pipe(map((params) => Number(params.customerId)));
|
||||
customerId$ = this._activateRoute.params.pipe(
|
||||
map((params) => Number(params.customerId)),
|
||||
);
|
||||
|
||||
customer$ = this._store.customer$;
|
||||
|
||||
accountType$ = this.customer$.pipe(
|
||||
map((customer) => customer?.features?.find((feature) => feature.group === 'd-customertype')),
|
||||
map((customer) => getEnabledCustomerFeature(customer?.features)),
|
||||
);
|
||||
|
||||
accountTypeKey$ = this.accountType$.pipe(map((accountType) => accountType?.key));
|
||||
accountTypeKey$ = this.accountType$.pipe(
|
||||
map((accountType) => accountType?.key),
|
||||
);
|
||||
|
||||
accountTypeDescription$ = this.accountType$.pipe(map((accountType) => accountType?.description));
|
||||
accountTypeDescription$ = this.accountType$.pipe(
|
||||
map((accountType) => accountType?.description),
|
||||
);
|
||||
|
||||
orderItemId$ = this._activateRoute.params.pipe(map((params) => Number(params.orderItemId)));
|
||||
orderItemId$ = this._activateRoute.params.pipe(
|
||||
map((params) => Number(params.orderItemId)),
|
||||
);
|
||||
|
||||
orderItems$ = this.order$.pipe(map((order) => order?.items?.map((i) => i?.data)));
|
||||
orderItems$ = this.order$.pipe(
|
||||
map((order) => order?.items?.map((i) => i?.data)),
|
||||
);
|
||||
|
||||
selectedOrderItem$ = this._store.selectedOrderItem$;
|
||||
|
||||
selectedOrderItemOrderType$ = this.selectedOrderItem$.pipe(map((orderItem) => orderItem?.features?.orderType));
|
||||
selectedOrderItemOrderType$ = this.selectedOrderItem$.pipe(
|
||||
map((orderItem) => orderItem?.features?.orderType),
|
||||
);
|
||||
|
||||
private _onDestroy = new Subject<void>();
|
||||
|
||||
ordersRoute$ = combineLatest([this.customerId$, this._store.processId$]).pipe(
|
||||
map(([customerId, processId]) => this._navigation.ordersRoute({ processId, customerId })),
|
||||
map(([customerId, processId]) =>
|
||||
this._navigation.ordersRoute({ processId, customerId }),
|
||||
),
|
||||
);
|
||||
|
||||
orderDetailsHistoryRoute$ = combineLatest([
|
||||
@@ -93,27 +120,38 @@ export class OrderDetailsMainViewComponent implements OnInit, OnDestroy {
|
||||
this.orderItemId$,
|
||||
]).pipe(
|
||||
map(([customerId, processId, orderId, orderItemId]) =>
|
||||
this._navigation.orderDetailsHistoryRoute({ processId, customerId, orderId, orderItemId }),
|
||||
this._navigation.orderDetailsHistoryRoute({
|
||||
processId,
|
||||
customerId,
|
||||
orderId,
|
||||
orderItemId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
groupedOrderItemsByOrderType$ = this.orderItems$.pipe(
|
||||
map((orderItems) => groupBy(orderItems, (orderItem) => orderItem?.features?.orderType)),
|
||||
map((orderItems) =>
|
||||
groupBy(orderItems, (orderItem) => orderItem?.features?.orderType),
|
||||
),
|
||||
tap((groupedOrderItems) => console.log(groupedOrderItems)),
|
||||
);
|
||||
|
||||
showSelectedItem$ = this._env.matchDesktopXLarge$;
|
||||
|
||||
showItemList$ = this.showSelectedItem$.pipe(map((showSelectedItem) => !showSelectedItem));
|
||||
showItemList$ = this.showSelectedItem$.pipe(
|
||||
map((showSelectedItem) => !showSelectedItem),
|
||||
);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.orderId$.pipe(takeUntil(this._onDestroy)).subscribe((orderId) => {
|
||||
this._store.selectOrder(orderId);
|
||||
});
|
||||
|
||||
this.customerId$.pipe(takeUntil(this._onDestroy)).subscribe((customerId) => {
|
||||
this._store.selectCustomer({ customerId });
|
||||
});
|
||||
this.customerId$
|
||||
.pipe(takeUntil(this._onDestroy))
|
||||
.subscribe((customerId) => {
|
||||
this._store.selectCustomer({ customerId });
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
|
||||
@@ -1,113 +1,151 @@
|
||||
<div class="page-customer-order-item-list-item__card-content grid grid-cols-[6rem_1fr] gap-4">
|
||||
<div>
|
||||
<img class="rounded shadow mx-auto w-[5.9rem]" [src]="orderItem?.product?.ean | productImage" [alt]="orderItem?.product?.name" />
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
<div class="grid grid-flow-col justify-between items-end">
|
||||
<span>{{ orderItem.product?.contributors }}</span>
|
||||
@if (orderDetailsHistoryRoute$ | async; as orderDetailsHistoryRoute) {
|
||||
<a
|
||||
[routerLink]="orderDetailsHistoryRoute.path"
|
||||
[queryParams]="orderDetailsHistoryRoute.urlTree.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
class="text-brand font-bold text-xl"
|
||||
>
|
||||
Historie
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="font-bold text-lg">
|
||||
{{ orderItem?.product?.name }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="isa-label">
|
||||
{{ processingStatus$ | async | orderItemProcessingStatus }}
|
||||
</span>
|
||||
</div>
|
||||
@if (orderItemSubsetItem$ | async; as orderItemSubsetItem) {
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
<div class="col-data">
|
||||
<div class="col-label">Menge</div>
|
||||
<div class="col-value">{{ orderItem?.quantity?.quantity }}x</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Format</div>
|
||||
<div class="col-value grid-flow-col grid gap-3 items-center justify-start">
|
||||
<shared-icon [icon]="orderItem?.product?.format"></shared-icon>
|
||||
<span>{{ orderItem?.product?.formatDetail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">ISBN/EAN</div>
|
||||
<div class="col-value">{{ orderItem?.product?.ean }}</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Preis</div>
|
||||
<div class="col-value">{{ orderItem?.unitPrice?.value?.value | currency: orderItem?.unitPrice?.value?.currency : 'code' }}</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">MwSt</div>
|
||||
<div class="col-value">{{ orderItem?.unitPrice?.vat?.inPercent }}%</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="col-data">
|
||||
<div class="col-label">Lieferant</div>
|
||||
<div class="col-value">
|
||||
{{ orderItemSubsetItem?.supplier?.data?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Meldenummer</div>
|
||||
<div class="col-value">{{ orderItemSubsetItem?.ssc }} - {{ orderItemSubsetItem?.sscText }}</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Vsl. Lieferdatum</div>
|
||||
<div class="col-value">
|
||||
{{ orderItemSubsetItem?.estimatedShippingDate | date: 'dd.MM.yyyy' }}
|
||||
</div>
|
||||
</div>
|
||||
@if (orderItemSubsetItem?.preferredPickUpDate) {
|
||||
<div class="col-data">
|
||||
<div class="col-label">Zurücklegen bis</div>
|
||||
<div class="col-value">
|
||||
{{ orderItemSubsetItem?.preferredPickUpDate | date: 'dd.MM.yyyy' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<hr />
|
||||
@if (orderItemSubsetItem?.compartmentCode) {
|
||||
<div class="col-data">
|
||||
<div class="col-label">Abholfachnummer</div>
|
||||
<div class="col-value">
|
||||
<span>{{ orderItemSubsetItem?.compartmentCode }}</span>
|
||||
@if (orderItemSubsetItem?.compartmentInfo) {
|
||||
<span>_{{ orderItemSubsetItem?.compartmentInfo }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="col-data">
|
||||
<div class="col-label">Vormerker</div>
|
||||
<div class="col-value">{{ isPrebooked$ | async }}</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="col-data">
|
||||
<div class="col-label">Zahlungsweg</div>
|
||||
<div class="col-value">-</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Zahlungsart</div>
|
||||
<div class="col-value">
|
||||
{{ orderPaymentType$ | async | paymentType }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Anmerkung</div>
|
||||
<div class="col-value">
|
||||
{{ orderItemSubsetItem?.specialComment || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="page-customer-order-item-list-item__card-content grid grid-cols-[6rem_1fr] gap-4"
|
||||
>
|
||||
<div class="flex flex-col gap-2 justify-start items-center">
|
||||
@let ean = orderItem?.product?.ean;
|
||||
@let name = orderItem?.product?.name;
|
||||
@if (ean && name) {
|
||||
<img
|
||||
class="rounded shadow mx-auto w-[5.9rem]"
|
||||
[src]="ean | productImage"
|
||||
[alt]="name"
|
||||
/>
|
||||
}
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
|
||||
Prämie
|
||||
</ui-label>
|
||||
}
|
||||
</div>
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
<div class="grid grid-flow-col justify-between items-end">
|
||||
<span>{{ orderItem.product?.contributors }}</span>
|
||||
@if (orderDetailsHistoryRoute$ | async; as orderDetailsHistoryRoute) {
|
||||
<a
|
||||
[routerLink]="orderDetailsHistoryRoute.path"
|
||||
[queryParams]="orderDetailsHistoryRoute.urlTree.queryParams"
|
||||
[queryParamsHandling]="'merge'"
|
||||
class="text-brand font-bold text-xl"
|
||||
>
|
||||
Historie
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
<div class="font-bold text-lg">
|
||||
{{ orderItem?.product?.name }}
|
||||
</div>
|
||||
<div>
|
||||
<span class="isa-label">
|
||||
{{ processingStatus$ | async | orderItemProcessingStatus }}
|
||||
</span>
|
||||
</div>
|
||||
@if (orderItemSubsetItem$ | async; as orderItemSubsetItem) {
|
||||
<div class="grid grid-flow-row gap-2">
|
||||
<div class="col-data">
|
||||
<div class="col-label">Menge</div>
|
||||
<div class="col-value">{{ orderItem?.quantity?.quantity }}x</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Format</div>
|
||||
<div
|
||||
class="col-value grid-flow-col grid gap-3 items-center justify-start"
|
||||
>
|
||||
@let format = orderItem?.product?.format;
|
||||
@if (format) {
|
||||
<shared-icon [icon]="orderItem?.product?.format"></shared-icon>
|
||||
}
|
||||
<span>{{ orderItem?.product?.formatDetail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">ISBN/EAN</div>
|
||||
<div class="col-value">{{ orderItem?.product?.ean }}</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<div class="col-label">Prämie</div>
|
||||
<div class="col-value">{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte</div>
|
||||
} @else {
|
||||
<div class="col-label">Preis</div>
|
||||
<div class="col-value">
|
||||
{{
|
||||
orderItem?.unitPrice?.value?.value
|
||||
| currency: orderItem?.unitPrice?.value?.currency : 'code'
|
||||
}}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">MwSt</div>
|
||||
<div class="col-value">
|
||||
{{ orderItem?.unitPrice?.vat?.inPercent }}%
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="col-data">
|
||||
<div class="col-label">Lieferant</div>
|
||||
<div class="col-value">
|
||||
{{ orderItemSubsetItem?.supplier?.data?.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Meldenummer</div>
|
||||
<div class="col-value">
|
||||
{{ orderItemSubsetItem?.ssc }} - {{ orderItemSubsetItem?.sscText }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Vsl. Lieferdatum</div>
|
||||
<div class="col-value">
|
||||
{{
|
||||
orderItemSubsetItem?.estimatedShippingDate | date: 'dd.MM.yyyy'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
@if (orderItemSubsetItem?.preferredPickUpDate) {
|
||||
<div class="col-data">
|
||||
<div class="col-label">Zurücklegen bis</div>
|
||||
<div class="col-value">
|
||||
{{
|
||||
orderItemSubsetItem?.preferredPickUpDate | date: 'dd.MM.yyyy'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<hr />
|
||||
@if (orderItemSubsetItem?.compartmentCode) {
|
||||
<div class="col-data">
|
||||
<div class="col-label">Abholfachnummer</div>
|
||||
<div class="col-value">
|
||||
<span>{{ orderItemSubsetItem?.compartmentCode }}</span>
|
||||
@if (orderItemSubsetItem?.compartmentInfo) {
|
||||
<span>_{{ orderItemSubsetItem?.compartmentInfo }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="col-data">
|
||||
<div class="col-label">Vormerker</div>
|
||||
<div class="col-value">{{ isPrebooked$ | async }}</div>
|
||||
</div>
|
||||
<hr />
|
||||
<div class="col-data">
|
||||
<div class="col-label">Zahlungsweg</div>
|
||||
<div class="col-value">-</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Zahlungsart</div>
|
||||
<div class="col-value">
|
||||
{{ orderPaymentType$ | async | paymentType }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-data">
|
||||
<div class="col-label">Anmerkung</div>
|
||||
<div class="col-value">
|
||||
{{ orderItemSubsetItem?.specialComment || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,88 +1,136 @@
|
||||
import { AsyncPipe, CurrencyPipe, DatePipe } from '@angular/common';
|
||||
import { Component, ChangeDetectionStrategy, Input, OnDestroy, OnInit, inject } from '@angular/core';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { ProductImagePipe } from '@cdn/product-image';
|
||||
import { OrderItemProcessingStatusPipe } from '@shared/pipes/order';
|
||||
import { OrderItemDTO } from '@generated/swagger/oms-api';
|
||||
import { BehaviorSubject, Subject, combineLatest } from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { CustomerSearchStore } from '../../store';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { PaymentTypePipe } from '@shared/pipes/customer';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-order-item-list-item',
|
||||
templateUrl: 'order-item-list-item.component.html',
|
||||
styleUrls: ['order-item-list-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-customer-order-item-list-item' },
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
ProductImagePipe,
|
||||
CurrencyPipe,
|
||||
RouterLink,
|
||||
PaymentTypePipe,
|
||||
OrderItemProcessingStatusPipe
|
||||
],
|
||||
})
|
||||
export class CustomerOrderItemListItemComponent implements OnInit, OnDestroy {
|
||||
private _activatedRoute = inject(ActivatedRoute);
|
||||
private _store = inject(CustomerSearchStore);
|
||||
private _navigation = inject(CustomerSearchNavigation);
|
||||
|
||||
private _onDestroy = new Subject<void>();
|
||||
|
||||
private _orderItemSub = new BehaviorSubject<OrderItemDTO>(undefined);
|
||||
|
||||
@Input()
|
||||
get orderItem() {
|
||||
return this._orderItemSub.getValue();
|
||||
}
|
||||
|
||||
set orderItem(value: OrderItemDTO) {
|
||||
this._orderItemSub.next(value);
|
||||
}
|
||||
orderId$ = this._activatedRoute.params.pipe(map((params) => Number(params.orderId)));
|
||||
|
||||
order$ = this._store.order$;
|
||||
|
||||
orderPaymentType$ = this.order$.pipe(map((order) => order?.paymentType));
|
||||
|
||||
customerId$ = this._activatedRoute.params.pipe(map((params) => Number(params.customerId)));
|
||||
|
||||
orderItemOrderType$ = this._orderItemSub.pipe(map((orderItem) => orderItem?.features?.orderType));
|
||||
|
||||
orderItemSubsetItem$ = this._orderItemSub.pipe(map((orderItem) => orderItem?.subsetItems?.[0]?.data));
|
||||
|
||||
orderDetailsHistoryRoute$ = combineLatest([
|
||||
this.customerId$,
|
||||
this._store.processId$,
|
||||
this.orderId$,
|
||||
this._orderItemSub,
|
||||
]).pipe(
|
||||
map(([customerId, processId, orderId, orderItem]) =>
|
||||
this._navigation.orderDetailsHistoryRoute({ processId, customerId, orderId, orderItemId: orderItem?.id }),
|
||||
),
|
||||
);
|
||||
|
||||
isPrebooked$ = this.orderItemSubsetItem$.pipe(map((subsetItem) => (subsetItem?.isPrebooked ? 'Ja' : 'Nein')));
|
||||
|
||||
processingStatus$ = this.orderItemSubsetItem$.pipe(map((subsetItem) => subsetItem?.processingStatus));
|
||||
|
||||
ngOnInit() {
|
||||
this.customerId$.pipe(takeUntil(this._onDestroy)).subscribe((customerId) => {
|
||||
this._store.selectCustomer({ customerId });
|
||||
});
|
||||
|
||||
this.orderId$.pipe(takeUntil(this._onDestroy)).subscribe((orderId) => {
|
||||
this._store.selectOrder(+orderId);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._onDestroy.next();
|
||||
this._onDestroy.complete();
|
||||
this._orderItemSub.complete();
|
||||
}
|
||||
}
|
||||
import {
|
||||
AsyncPipe,
|
||||
CurrencyPipe,
|
||||
DatePipe,
|
||||
DecimalPipe,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, RouterLink } from '@angular/router';
|
||||
import { ProductImagePipe } from '@cdn/product-image';
|
||||
import { OrderItemProcessingStatusPipe } from '@shared/pipes/order';
|
||||
import { OrderItemDTO } from '@generated/swagger/oms-api';
|
||||
import { BehaviorSubject, Subject, combineLatest } from 'rxjs';
|
||||
import { map, takeUntil } from 'rxjs/operators';
|
||||
import { CustomerSearchStore } from '../../store';
|
||||
import { CustomerSearchNavigation } from '@shared/services/navigation';
|
||||
import { PaymentTypePipe } from '@shared/pipes/customer';
|
||||
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
|
||||
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
|
||||
import { IconComponent } from '@shared/components/icon';
|
||||
|
||||
@Component({
|
||||
selector: 'page-customer-order-item-list-item',
|
||||
templateUrl: 'order-item-list-item.component.html',
|
||||
styleUrls: ['order-item-list-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
host: { class: 'page-customer-order-item-list-item' },
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
ProductImagePipe,
|
||||
CurrencyPipe,
|
||||
RouterLink,
|
||||
PaymentTypePipe,
|
||||
OrderItemProcessingStatusPipe,
|
||||
LabelComponent,
|
||||
IconComponent,
|
||||
DecimalPipe,
|
||||
],
|
||||
})
|
||||
export class CustomerOrderItemListItemComponent implements OnInit, OnDestroy {
|
||||
private _activatedRoute = inject(ActivatedRoute);
|
||||
private _store = inject(CustomerSearchStore);
|
||||
private _navigation = inject(CustomerSearchNavigation);
|
||||
|
||||
private _onDestroy = new Subject<void>();
|
||||
|
||||
private _orderItemSub = new BehaviorSubject<OrderItemDTO>(undefined);
|
||||
|
||||
@Input()
|
||||
get orderItem() {
|
||||
return this._orderItemSub.getValue();
|
||||
}
|
||||
|
||||
set orderItem(value: OrderItemDTO) {
|
||||
this._orderItemSub.next(value);
|
||||
}
|
||||
orderId$ = this._activatedRoute.params.pipe(
|
||||
map((params) => Number(params.orderId)),
|
||||
);
|
||||
|
||||
order$ = this._store.order$;
|
||||
|
||||
orderPaymentType$ = this.order$.pipe(map((order) => order?.paymentType));
|
||||
|
||||
customerId$ = this._activatedRoute.params.pipe(
|
||||
map((params) => Number(params.customerId)),
|
||||
);
|
||||
|
||||
orderItemOrderType$ = this._orderItemSub.pipe(
|
||||
map((orderItem) => orderItem?.features?.orderType),
|
||||
);
|
||||
|
||||
orderItemSubsetItem$ = this._orderItemSub.pipe(
|
||||
map((orderItem) => orderItem?.subsetItems?.[0]?.data),
|
||||
);
|
||||
|
||||
orderDetailsHistoryRoute$ = combineLatest([
|
||||
this.customerId$,
|
||||
this._store.processId$,
|
||||
this.orderId$,
|
||||
this._orderItemSub,
|
||||
]).pipe(
|
||||
map(([customerId, processId, orderId, orderItem]) =>
|
||||
this._navigation.orderDetailsHistoryRoute({
|
||||
processId,
|
||||
customerId,
|
||||
orderId,
|
||||
orderItemId: orderItem?.id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
isPrebooked$ = this.orderItemSubsetItem$.pipe(
|
||||
map((subsetItem) => (subsetItem?.isPrebooked ? 'Ja' : 'Nein')),
|
||||
);
|
||||
|
||||
processingStatus$ = this.orderItemSubsetItem$.pipe(
|
||||
map((subsetItem) => subsetItem?.processingStatus),
|
||||
);
|
||||
|
||||
hasRewardPoints$ = this._orderItemSub.pipe(
|
||||
map((orderItem) => getOrderItemRewardFeature(orderItem) !== undefined),
|
||||
);
|
||||
|
||||
rewardPoints$ = this._orderItemSub.pipe(
|
||||
map((orderItem) => getOrderItemRewardFeature(orderItem)),
|
||||
);
|
||||
|
||||
Labeltype = Labeltype;
|
||||
LabelPriority = LabelPriority;
|
||||
|
||||
ngOnInit() {
|
||||
this.customerId$
|
||||
.pipe(takeUntil(this._onDestroy))
|
||||
.subscribe((customerId) => {
|
||||
this._store.selectCustomer({ customerId });
|
||||
});
|
||||
|
||||
this.orderId$.pipe(takeUntil(this._onDestroy)).subscribe((orderId) => {
|
||||
this._store.selectOrder(+orderId);
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._onDestroy.next();
|
||||
this._onDestroy.complete();
|
||||
this._orderItemSub.complete();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
<ng-container *ngIf="orderItem$ | async; let orderItem">
|
||||
<div class="grid grid-flow-row gap-px-2">
|
||||
<div class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t">
|
||||
<div class="grid grid-flow-col gap-[0.4375rem] items-center" *ngIf="fetchingCustomerDone$ | async; else featureLoading">
|
||||
<ng-container *ngIf="features$ | async; let features">
|
||||
<shared-icon *ngIf="features?.length > 0" [size]="24" icon="person"></shared-icon>
|
||||
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2" *ngFor="let feature of features">
|
||||
<div
|
||||
class="bg-[#F5F7FA] flex flex-row justify-between items-center p-4 rounded-t"
|
||||
>
|
||||
<div
|
||||
class="grid grid-flow-col gap-[0.4375rem] items-center"
|
||||
*ngIf="fetchingCustomerDone$ | async; else featureLoading"
|
||||
>
|
||||
<ng-container *ngIf="customerFeature$ | async; let feature">
|
||||
<shared-icon
|
||||
*ngIf="!!feature"
|
||||
[size]="24"
|
||||
icon="person"
|
||||
></shared-icon>
|
||||
<div class="grid grid-flow-col gap-2 items-center font-bold text-p2">
|
||||
{{ feature?.description }}
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -27,30 +36,69 @@
|
||||
<shared-skeleton-loader class="w-64 h-6"></shared-skeleton-loader>
|
||||
</ng-template>
|
||||
|
||||
<div class="page-pickup-shelf-details-header__details bg-white px-4 pt-4 pb-5">
|
||||
<div class="flex flex-row items-center" [class.mb-8]="!orderItem?.features?.paid && !isKulturpass">
|
||||
<page-pickup-shelf-details-header-nav-menu class="mr-2" [customer]="customer$ | async"></page-pickup-shelf-details-header-nav-menu>
|
||||
<h2 class="page-pickup-shelf-details-header__details-header items-center">
|
||||
<div
|
||||
class="page-pickup-shelf-details-header__details bg-white px-4 pt-4 pb-5"
|
||||
>
|
||||
<div
|
||||
class="flex flex-row items-center"
|
||||
[class.mb-8]="!orderItem?.features?.paid && !isKulturpass"
|
||||
>
|
||||
<page-pickup-shelf-details-header-nav-menu
|
||||
class="mr-2"
|
||||
[customer]="customer$ | async"
|
||||
></page-pickup-shelf-details-header-nav-menu>
|
||||
<h2
|
||||
class="page-pickup-shelf-details-header__details-header items-center"
|
||||
>
|
||||
<div class="text-h2">
|
||||
{{ orderItem?.organisation }}
|
||||
<ng-container *ngIf="!!orderItem?.organisation && (!!orderItem?.firstName || !!orderItem?.lastName)">-</ng-container>
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!!orderItem?.organisation &&
|
||||
(!!orderItem?.firstName || !!orderItem?.lastName)
|
||||
"
|
||||
>-</ng-container
|
||||
>
|
||||
{{ orderItem?.lastName }}
|
||||
{{ orderItem?.firstName }}
|
||||
</div>
|
||||
<div class="page-pickup-shelf-details-header__header-compartment text-h3">
|
||||
{{ orderItem?.compartmentCode }}{{ orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo }}
|
||||
<div
|
||||
class="page-pickup-shelf-details-header__header-compartment text-h3"
|
||||
>
|
||||
{{ orderItem?.compartmentCode
|
||||
}}{{
|
||||
orderItem?.compartmentInfo && '_' + orderItem?.compartmentInfo
|
||||
}}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="page-pickup-shelf-details-header__paid-marker mt-[0.375rem]" *ngIf="orderItem?.features?.paid && !isKulturpass">
|
||||
<div class="font-bold flex flex-row items-center justify-center text-p2 text-[#26830C]">
|
||||
<shared-icon class="flex items-center justify-center mr-[0.375rem]" [size]="24" icon="credit-card"></shared-icon>
|
||||
<div
|
||||
class="page-pickup-shelf-details-header__paid-marker mt-[0.375rem]"
|
||||
*ngIf="orderItem?.features?.paid && !isKulturpass"
|
||||
>
|
||||
<div
|
||||
class="font-bold flex flex-row items-center justify-center text-p2 text-[#26830C]"
|
||||
>
|
||||
<shared-icon
|
||||
class="flex items-center justify-center mr-[0.375rem]"
|
||||
[size]="24"
|
||||
icon="credit-card"
|
||||
></shared-icon>
|
||||
{{ orderItem?.features?.paid }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-pickup-shelf-details-header__paid-marker mt-[0.375rem] text-[#26830C]" *ngIf="isKulturpass">
|
||||
<svg class="fill-current mr-2" xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22">
|
||||
<div
|
||||
class="page-pickup-shelf-details-header__paid-marker mt-[0.375rem] text-[#26830C]"
|
||||
*ngIf="isKulturpass"
|
||||
>
|
||||
<svg
|
||||
class="fill-current mr-2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="22"
|
||||
viewBox="0 -960 960 960"
|
||||
width="22"
|
||||
>
|
||||
<path
|
||||
d="M880-740v520q0 24-18 42t-42 18H140q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h680q24 0 42 18t18 42ZM140-631h680v-109H140v109Zm0 129v282h680v-282H140Zm0 282v-520 520Z"
|
||||
/>
|
||||
@@ -59,21 +107,42 @@
|
||||
</div>
|
||||
|
||||
<div class="page-pickup-shelf-details-header__details-wrapper -mt-3">
|
||||
<div class="flex flex-row page-pickup-shelf-details-header__buyer-number" data-detail-id="Kundennummer">
|
||||
<div
|
||||
class="flex flex-row page-pickup-shelf-details-header__buyer-number"
|
||||
data-detail-id="Kundennummer"
|
||||
>
|
||||
<div class="min-w-[9rem]">Kundennummer</div>
|
||||
<div class="flex flex-row font-bold">{{ orderItem?.buyerNumber }}</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{ orderItem?.buyerNumber }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row page-pickup-shelf-details-header__order-number" data-detail-id="VorgangId">
|
||||
<div
|
||||
class="flex flex-row page-pickup-shelf-details-header__order-number"
|
||||
data-detail-id="VorgangId"
|
||||
>
|
||||
<div class="min-w-[9rem]">Vorgang-ID</div>
|
||||
<div class="flex flex-row font-bold">{{ orderItem?.orderNumber }}</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{ orderItem?.orderNumber }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row page-pickup-shelf-details-header__order-date" data-detail-id="Bestelldatum">
|
||||
<div
|
||||
class="flex flex-row page-pickup-shelf-details-header__order-date"
|
||||
data-detail-id="Bestelldatum"
|
||||
>
|
||||
<div class="min-w-[9rem]">Bestelldatum</div>
|
||||
<div class="flex flex-row font-bold">{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{ orderItem?.orderDate | date: 'dd.MM.yy | HH:mm' }} Uhr
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row page-pickup-shelf-details-header__processing-status justify-between" data-detail-id="Status">
|
||||
<div
|
||||
class="flex flex-row page-pickup-shelf-details-header__processing-status justify-between"
|
||||
data-detail-id="Status"
|
||||
>
|
||||
<div class="min-w-[9rem]">Status</div>
|
||||
<div *ngIf="!(changeStatusLoader$ | async)" class="flex flex-row font-bold -mr-[0.125rem]">
|
||||
<div
|
||||
*ngIf="!(changeStatusLoader$ | async)"
|
||||
class="flex flex-row font-bold -mr-[0.125rem]"
|
||||
>
|
||||
<span *ngIf="!(canEditStatus$ | async)">
|
||||
{{ orderItem?.processingStatus | processingStatus }}
|
||||
</span>
|
||||
@@ -97,7 +166,12 @@
|
||||
icon="arrow-drop-down"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-dropdown #statusDropdown yPosition="below" xPosition="after" [xOffset]="8">
|
||||
<ui-dropdown
|
||||
#statusDropdown
|
||||
yPosition="below"
|
||||
xPosition="after"
|
||||
[xOffset]="8"
|
||||
>
|
||||
<button
|
||||
uiDropdownItem
|
||||
*ngFor="let action of statusActions$ | async"
|
||||
@@ -111,12 +185,22 @@
|
||||
</ui-dropdown>
|
||||
</ng-container>
|
||||
</div>
|
||||
<ui-spinner *ngIf="changeStatusLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
|
||||
<ui-spinner
|
||||
*ngIf="changeStatusLoader$ | async; let loader"
|
||||
class="flex flex-row font-bold loader"
|
||||
[show]="loader"
|
||||
></ui-spinner>
|
||||
</div>
|
||||
<div class="flex flex-row page-pickup-shelf-details-header__order-source" data-detail-id="Bestellkanal">
|
||||
<div
|
||||
class="flex flex-row page-pickup-shelf-details-header__order-source"
|
||||
data-detail-id="Bestellkanal"
|
||||
>
|
||||
<div class="min-w-[9rem]">Bestellkanal</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
<shared-skeleton-loader class="w-32" *ngIf="fetchingOrder$ | async; else orderSourceTmpl"></shared-skeleton-loader>
|
||||
<shared-skeleton-loader
|
||||
class="w-32"
|
||||
*ngIf="fetchingOrder$ | async; else orderSourceTmpl"
|
||||
></shared-skeleton-loader>
|
||||
<ng-template #orderSourceTmpl>
|
||||
{{ order()?.features?.orderSource }}
|
||||
</ng-template>
|
||||
@@ -142,19 +226,30 @@
|
||||
|
||||
<ng-template #changeDate>
|
||||
<div class="min-w-[9rem]">Geändert</div>
|
||||
<div class="flex flex-row font-bold">{{ orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
{{
|
||||
orderItem?.processingStatusDate | date: 'dd.MM.yy | HH:mm'
|
||||
}}
|
||||
Uhr
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex flex-row page-pickup-shelf-details-header__pick-up justify-between"
|
||||
data-detail-id="Wunschdatum"
|
||||
*ngIf="orderItem.orderType === 1 && (orderItem.processingStatus === 16 || orderItem.processingStatus === 8192)"
|
||||
*ngIf="
|
||||
orderItem.orderType === 1 &&
|
||||
(orderItem.processingStatus === 16 ||
|
||||
orderItem.processingStatus === 8192)
|
||||
"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="preferredPickUpDate"></ng-container>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col page-pickup-shelf-details-header__dig-and-notification">
|
||||
<div
|
||||
class="flex flex-col page-pickup-shelf-details-header__dig-and-notification"
|
||||
>
|
||||
<div
|
||||
*ngIf="orderItem.orderType === 1"
|
||||
class="flex flex-row page-pickup-shelf-details-header__notification"
|
||||
@@ -162,9 +257,14 @@
|
||||
>
|
||||
<div class="min-w-[9rem]">Benachrichtigung</div>
|
||||
<div class="flex flex-row font-bold">
|
||||
<shared-skeleton-loader class="w-32" *ngIf="fetchingOrder$ | async; else notificationsChannelTpl"></shared-skeleton-loader>
|
||||
<shared-skeleton-loader
|
||||
class="w-32"
|
||||
*ngIf="fetchingOrder$ | async; else notificationsChannelTpl"
|
||||
></shared-skeleton-loader>
|
||||
<ng-template #notificationsChannelTpl>
|
||||
{{ (notificationsChannel$ | async | notificationsChannel) || '-' }}
|
||||
{{
|
||||
(notificationsChannel$ | async | notificationsChannel) || '-'
|
||||
}}
|
||||
</ng-template>
|
||||
</div>
|
||||
</div>
|
||||
@@ -175,11 +275,17 @@
|
||||
|
||||
<ng-template #abholfrist>
|
||||
<div class="min-w-[9rem]">Abholfrist</div>
|
||||
<div *ngIf="!(orderItemSubsetLoading$ | async); else featureLoading" class="flex flex-row font-bold">
|
||||
<div
|
||||
*ngIf="!(orderItemSubsetLoading$ | async); else featureLoading"
|
||||
class="flex flex-row font-bold"
|
||||
>
|
||||
<button
|
||||
[uiOverlayTrigger]="deadlineDatepicker"
|
||||
#deadlineDatepickerTrigger="uiOverlayTrigger"
|
||||
[disabled]="!isKulturpass && (!!orderItem?.features?.paid || (changeDateDisabled$ | async))"
|
||||
[disabled]="
|
||||
!isKulturpass &&
|
||||
(!!orderItem?.features?.paid || (changeDateDisabled$ | async))
|
||||
"
|
||||
class="cta-pickup-deadline"
|
||||
matomoClickCategory="pickup-shelf-details-header"
|
||||
matomoClickAction="click"
|
||||
@@ -188,7 +294,11 @@
|
||||
<strong class="border-r border-[#AEB7C1] pr-4">
|
||||
{{ orderItem?.pickUpDeadline | date: 'dd.MM.yy' }}
|
||||
</strong>
|
||||
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
|
||||
<shared-icon
|
||||
class="text-[#596470] ml-4"
|
||||
[size]="24"
|
||||
icon="isa-calendar"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-datepicker
|
||||
#deadlineDatepicker
|
||||
@@ -205,23 +315,40 @@
|
||||
|
||||
<ng-template #preferredPickUpDate>
|
||||
<div class="min-w-[9rem]">Zurücklegen bis</div>
|
||||
<div *ngIf="!(orderItemSubsetLoading$ | async); else featureLoading" class="flex flex-row font-bold">
|
||||
<div
|
||||
*ngIf="!(orderItemSubsetLoading$ | async); else featureLoading"
|
||||
class="flex flex-row font-bold"
|
||||
>
|
||||
<button
|
||||
[uiOverlayTrigger]="preferredPickUpDatePicker"
|
||||
#preferredPickUpDatePickerTrigger="uiOverlayTrigger"
|
||||
[disabled]="(!isKulturpass && !!orderItem?.features?.paid) || (changeDateDisabled$ | async)"
|
||||
[disabled]="
|
||||
(!isKulturpass && !!orderItem?.features?.paid) ||
|
||||
(changeDateDisabled$ | async)
|
||||
"
|
||||
class="cta-pickup-preferred"
|
||||
matomoClickCategory="pickup-shelf-details-header"
|
||||
matomoClickAction="click"
|
||||
matomoClickName="pickup-preferred"
|
||||
>
|
||||
<strong class="border-r border-[#AEB7C1] pr-4" *ngIf="findLatestPreferredPickUpDate$ | async; let pickUpDate; else: selectTemplate">
|
||||
<strong
|
||||
class="border-r border-[#AEB7C1] pr-4"
|
||||
*ngIf="
|
||||
findLatestPreferredPickUpDate$ | async;
|
||||
let pickUpDate;
|
||||
else: selectTemplate
|
||||
"
|
||||
>
|
||||
{{ pickUpDate | date: 'dd.MM.yy' }}
|
||||
</strong>
|
||||
<ng-template #selectTemplate>
|
||||
<strong class="border-r border-[#AEB7C1] pr-4">Auswählen</strong>
|
||||
</ng-template>
|
||||
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
|
||||
<shared-icon
|
||||
class="text-[#596470] ml-4"
|
||||
[size]="24"
|
||||
icon="isa-calendar"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-datepicker
|
||||
#preferredPickUpDatePicker
|
||||
@@ -238,7 +365,10 @@
|
||||
|
||||
<ng-template #vslLieferdatum>
|
||||
<div class="min-w-[9rem]">vsl. Lieferdatum</div>
|
||||
<div *ngIf="!(orderItemSubsetLoading$ | async); else featureLoading" class="flex flex-row font-bold">
|
||||
<div
|
||||
*ngIf="!(orderItemSubsetLoading$ | async); else featureLoading"
|
||||
class="flex flex-row font-bold"
|
||||
>
|
||||
<button
|
||||
class="cta-datepicker"
|
||||
[disabled]="changeDateDisabled$ | async"
|
||||
@@ -251,7 +381,11 @@
|
||||
<span class="border-r border-[#AEB7C1] pr-4">
|
||||
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
|
||||
</span>
|
||||
<shared-icon class="text-[#596470] ml-4" [size]="24" icon="isa-calendar"></shared-icon>
|
||||
<shared-icon
|
||||
class="text-[#596470] ml-4"
|
||||
[size]="24"
|
||||
icon="isa-calendar"
|
||||
></shared-icon>
|
||||
</button>
|
||||
<ui-datepicker
|
||||
#uiDatepicker
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import { AsyncPipe, DatePipe, NgFor, NgIf, NgSwitch, NgSwitchCase, NgTemplateOutlet } from '@angular/common';
|
||||
import {
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
NgFor,
|
||||
NgIf,
|
||||
NgSwitch,
|
||||
NgSwitchCase,
|
||||
NgTemplateOutlet,
|
||||
} from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
@@ -28,6 +36,7 @@ import { PickUpShelfDetailsHeaderNavMenuComponent } from '../pickup-shelf-detail
|
||||
import { SkeletonLoaderComponent } from '@shared/components/loader';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatomoModule } from 'ngx-matomo-client';
|
||||
import { getEnabledCustomerFeature } from '@isa/crm/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'page-pickup-shelf-details-header',
|
||||
@@ -65,7 +74,10 @@ export class PickUpShelfDetailsHeaderComponent {
|
||||
handleAction = new EventEmitter<KeyValueDTOOfStringAndString>();
|
||||
|
||||
@Output()
|
||||
updateDate = new EventEmitter<{ date: Date; type?: 'delivery' | 'pickup' | 'preferred' }>();
|
||||
updateDate = new EventEmitter<{
|
||||
date: Date;
|
||||
type?: 'delivery' | 'pickup' | 'preferred';
|
||||
}>();
|
||||
|
||||
orderItemSubsetLoading$ = this._store.orderItemSubsetLoading$;
|
||||
|
||||
@@ -83,12 +95,19 @@ export class PickUpShelfDetailsHeaderComponent {
|
||||
?.reduce((acc, item) => {
|
||||
return [...acc, ...item.data.subsetItems];
|
||||
}, [])
|
||||
?.filter((a) => !!a.data.preferredPickUpDate && selectedOrderItemIds.find((id) => id === a.data.id));
|
||||
?.filter(
|
||||
(a) =>
|
||||
!!a.data.preferredPickUpDate &&
|
||||
selectedOrderItemIds.find((id) => id === a.data.id),
|
||||
);
|
||||
|
||||
if (subsetItems?.length > 0) {
|
||||
latestDate = new Date(
|
||||
subsetItems?.reduce((a, b) => {
|
||||
return new Date(a.data.preferredPickUpDate) > new Date(b.data.preferredPickUpDate) ? a : b;
|
||||
return new Date(a.data.preferredPickUpDate) >
|
||||
new Date(b.data.preferredPickUpDate)
|
||||
? a
|
||||
: b;
|
||||
})?.data?.preferredPickUpDate,
|
||||
);
|
||||
}
|
||||
@@ -108,14 +127,24 @@ export class PickUpShelfDetailsHeaderComponent {
|
||||
return this.order()?.features?.orderSource === 'KulturPass';
|
||||
}
|
||||
|
||||
minDateDatepicker = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -1);
|
||||
minDateDatepicker = this.dateAdapter.addCalendarDays(
|
||||
this.dateAdapter.today(),
|
||||
-1,
|
||||
);
|
||||
today = this.dateAdapter.today();
|
||||
|
||||
// Daten die im Header Angezeigt werden sollen
|
||||
orderItem$ = combineLatest([
|
||||
this._store.selectedOrderItem$, // Wenn man im Abholfach ist muss das ausgewählte OrderItem genommen werden
|
||||
this._store.selectedOrderItems$.pipe(map((orderItems) => orderItems?.find((_) => true))), // Wenn man in der Warenausgabe ist muss man das erste OrderItem nehmen
|
||||
]).pipe(map(([selectedOrderItem, selectedOrderItems]) => selectedOrderItem || selectedOrderItems));
|
||||
this._store.selectedOrderItems$.pipe(
|
||||
map((orderItems) => orderItems?.find((_) => true)),
|
||||
), // Wenn man in der Warenausgabe ist muss man das erste OrderItem nehmen
|
||||
]).pipe(
|
||||
map(
|
||||
([selectedOrderItem, selectedOrderItems]) =>
|
||||
selectedOrderItem || selectedOrderItems,
|
||||
),
|
||||
);
|
||||
|
||||
changeDateLoader$ = new BehaviorSubject<boolean>(false);
|
||||
changePreferredDateLoader$ = new BehaviorSubject<boolean>(false);
|
||||
@@ -124,28 +153,41 @@ export class PickUpShelfDetailsHeaderComponent {
|
||||
|
||||
changeDateDisabled$ = this.changeStatusDisabled$;
|
||||
|
||||
fetchingCustomerDone$ = this._store.fetchingCustomer$.pipe(map((fetchingCustomer) => !fetchingCustomer));
|
||||
fetchingCustomerDone$ = this._store.fetchingCustomer$.pipe(
|
||||
map((fetchingCustomer) => !fetchingCustomer),
|
||||
);
|
||||
|
||||
customer$ = this._store.customer$;
|
||||
|
||||
features$ = this.customer$.pipe(
|
||||
map((customer) => customer?.features || []),
|
||||
map((features) => features.filter((f) => f.enabled && !!f.description)),
|
||||
customerFeature$ = this.customer$.pipe(
|
||||
map((customer) => getEnabledCustomerFeature(customer?.features)),
|
||||
shareReplay(),
|
||||
);
|
||||
|
||||
statusActions$ = this.orderItem$.pipe(
|
||||
map((orderItem) => orderItem?.actions?.filter((action) => action.enabled === false)),
|
||||
map((orderItem) =>
|
||||
orderItem?.actions?.filter((action) => action.enabled === false),
|
||||
),
|
||||
);
|
||||
|
||||
crudaUpdate$ = this.orderItem$.pipe(map((orederItem) => !!(orederItem?.cruda & 4)));
|
||||
crudaUpdate$ = this.orderItem$.pipe(
|
||||
map((orederItem) => !!(orederItem?.cruda & 4)),
|
||||
);
|
||||
|
||||
editButtonDisabled$ = combineLatest([this.changeStatusLoader$, this.crudaUpdate$]).pipe(
|
||||
map(([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate),
|
||||
editButtonDisabled$ = combineLatest([
|
||||
this.changeStatusLoader$,
|
||||
this.crudaUpdate$,
|
||||
]).pipe(
|
||||
map(
|
||||
([changeStatusLoader, crudaUpdate]) => changeStatusLoader || !crudaUpdate,
|
||||
),
|
||||
);
|
||||
|
||||
canEditStatus$ = combineLatest([this.statusActions$, this.crudaUpdate$]).pipe(
|
||||
map(([statusActions, crudaUpdate]) => statusActions?.length > 0 && crudaUpdate),
|
||||
map(
|
||||
([statusActions, crudaUpdate]) =>
|
||||
statusActions?.length > 0 && crudaUpdate,
|
||||
),
|
||||
);
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -40,6 +40,11 @@
|
||||
[src]="orderItem.product?.ean | productImage"
|
||||
[alt]="orderItem.product?.name"
|
||||
/>
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
|
||||
Prämie
|
||||
</ui-label>
|
||||
}
|
||||
</div>
|
||||
<div class="page-pickup-shelf-details-item__details">
|
||||
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
|
||||
@@ -117,10 +122,15 @@
|
||||
<div class="value">{{ orderItem.product?.ean }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (orderItem.price !== undefined) {
|
||||
@if (orderItem.price !== undefined || (hasRewardPoints$ | async)) {
|
||||
<div class="detail">
|
||||
<div class="label">Preis</div>
|
||||
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
|
||||
@if (hasRewardPoints$ | async) {
|
||||
<div class="label">Prämie</div>
|
||||
<div class="value">{{ rewardPoints$ | async | number: '1.0-0' }} Lesepunkte</div>
|
||||
} @else {
|
||||
<div class="label">Preis</div>
|
||||
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!!orderItem.retailPrice?.vat?.inPercent) {
|
||||
|
||||
@@ -21,6 +21,8 @@ button {
|
||||
}
|
||||
|
||||
.page-pickup-shelf-details-item__thumbnail {
|
||||
@apply flex flex-col items-center gap-2;
|
||||
|
||||
img {
|
||||
@apply rounded shadow-cta w-[3.625rem] max-h-[5.9375rem];
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { CdkTextareaAutosize, TextFieldModule } from '@angular/cdk/text-field';
|
||||
import { AsyncPipe, CurrencyPipe, DatePipe } from '@angular/common';
|
||||
import { AsyncPipe, CurrencyPipe, DatePipe, DecimalPipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnInit,
|
||||
Output,
|
||||
ViewChild,
|
||||
inject, OnDestroy,
|
||||
inject,
|
||||
OnDestroy,
|
||||
} from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule, UntypedFormControl } from '@angular/forms';
|
||||
import { NavigateOnClickDirective, ProductImageModule } from '@cdn/product-image';
|
||||
import { DBHOrderItemListItemDTO, OrderDTO, ReceiptDTO } from '@generated/swagger/oms-api';
|
||||
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
|
||||
import { UiTooltipModule } from '@ui/tooltip';
|
||||
import { PickupShelfPaymentTypePipe } from '../pipes/payment-type.pipe';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
@@ -48,6 +50,7 @@ export interface PickUpShelfDetailsItemComponentState {
|
||||
ReactiveFormsModule,
|
||||
CurrencyPipe,
|
||||
DatePipe,
|
||||
DecimalPipe,
|
||||
AsyncPipe,
|
||||
ProductImageModule,
|
||||
TextFieldModule,
|
||||
@@ -56,12 +59,13 @@ export interface PickUpShelfDetailsItemComponentState {
|
||||
UiQuantityDropdownModule,
|
||||
NotificationTypePipe,
|
||||
NavigateOnClickDirective,
|
||||
MatomoModule
|
||||
MatomoModule,
|
||||
LabelComponent
|
||||
],
|
||||
})
|
||||
export class PickUpShelfDetailsItemComponent
|
||||
extends ComponentStore<PickUpShelfDetailsItemComponentState>
|
||||
implements OnInit, OnDestroy
|
||||
implements OnDestroy
|
||||
{
|
||||
private _store = inject(PickupShelfDetailsStore);
|
||||
|
||||
@@ -117,6 +121,22 @@ export class PickUpShelfDetailsItemComponent
|
||||
|
||||
hasSmsNotification$ = this.smsNotificationDates$.pipe(map((dates) => dates?.length > 0));
|
||||
|
||||
/**
|
||||
* Observable that indicates whether the order item has reward points (Lesepunkte).
|
||||
* Returns true if the item has a 'praemie' feature.
|
||||
*/
|
||||
hasRewardPoints$ = this.orderItem$.pipe(
|
||||
map((orderItem) => getOrderItemRewardFeature(orderItem) !== undefined),
|
||||
);
|
||||
|
||||
/**
|
||||
* Observable that emits the reward points (Lesepunkte) value for the order item.
|
||||
* Returns the parsed numeric value from the 'praemie' feature, or undefined if not present.
|
||||
*/
|
||||
rewardPoints$ = this.orderItem$.pipe(
|
||||
map((orderItem) => getOrderItemRewardFeature(orderItem)),
|
||||
);
|
||||
|
||||
canChangeQuantity$ = combineLatest([this.orderItem$, this._store.fetchPartial$]).pipe(
|
||||
map(([item, partialPickup]) => ([16, 8192].includes(item?.processingStatus) || partialPickup) && item.quantity > 1),
|
||||
);
|
||||
@@ -167,12 +187,12 @@ export class PickUpShelfDetailsItemComponent
|
||||
return this._store.receipts;
|
||||
}
|
||||
|
||||
readonly receipts$ = this._store.receipts$;
|
||||
|
||||
set receipts(receipts: ReceiptDTO[]) {
|
||||
this._store.updateReceipts(receipts);
|
||||
}
|
||||
|
||||
readonly receipts$ = this._store.receipts$;
|
||||
|
||||
readonly receiptCount$ = this.receipts$.pipe(map((receipts) => receipts?.length));
|
||||
|
||||
specialCommentControl = new UntypedFormControl();
|
||||
@@ -181,7 +201,11 @@ export class PickUpShelfDetailsItemComponent
|
||||
|
||||
private _onDestroy$ = new Subject<void>();
|
||||
|
||||
expanded: boolean = false;
|
||||
expanded = false;
|
||||
|
||||
// Expose to template
|
||||
Labeltype = Labeltype;
|
||||
LabelPriority = LabelPriority;
|
||||
|
||||
constructor(private _cdr: ChangeDetectorRef) {
|
||||
super({
|
||||
@@ -189,8 +213,6 @@ export class PickUpShelfDetailsItemComponent
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {}
|
||||
|
||||
ngOnDestroy() {
|
||||
// Remove Prev OrderItem from selected list
|
||||
this._store.selectOrderItem(this.orderItem, false);
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
|
||||
.page-pickup-shelf-list-item__item-thumbnail {
|
||||
grid-area: thumbnail;
|
||||
@apply flex flex-col items-center gap-2;
|
||||
}
|
||||
|
||||
.page-pickup-shelf-list-item__item-image {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[class.page-pickup-shelf-list-item__item-grid-container-main]="primaryOutletActive"
|
||||
[class.page-pickup-shelf-list-item__item-grid-container-secondary]="primaryOutletActive && isItemSelectable === undefined"
|
||||
>
|
||||
<div class="page-pickup-shelf-list-item__item-thumbnail text-center w-[3.125rem] h-[4.9375rem]">
|
||||
<div class="page-pickup-shelf-list-item__item-thumbnail text-center">
|
||||
@if (item?.product?.ean | productImage; as productImage) {
|
||||
<img
|
||||
class="page-pickup-shelf-list-item__item-image w-[3.125rem] max-h-[4.9375rem]"
|
||||
@@ -20,6 +20,11 @@
|
||||
[alt]="item?.product?.name"
|
||||
/>
|
||||
}
|
||||
@if (hasRewardPoints) {
|
||||
<ui-label [type]="Labeltype.Tag" [priority]="LabelPriority.High">
|
||||
Prämie
|
||||
</ui-label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -5,6 +5,8 @@ import { NavigateOnClickDirective, ProductImageModule } from '@cdn/product-image
|
||||
import { EnvironmentService } from '@core/environment';
|
||||
import { IconModule } from '@shared/components/icon';
|
||||
import { DBHOrderItemListItemDTO } from '@generated/swagger/oms-api';
|
||||
import { getOrderItemRewardFeature } from '@isa/oms/data-access';
|
||||
import { LabelComponent, Labeltype, LabelPriority } from '@isa/ui/label';
|
||||
import { UiCommonModule } from '@ui/common';
|
||||
import { PickupShelfProcessingStatusPipe } from '../pipes/processing-status.pipe';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
@@ -29,7 +31,8 @@ import { MatomoModule } from 'ngx-matomo-client';
|
||||
UiCommonModule,
|
||||
PickupShelfProcessingStatusPipe,
|
||||
NavigateOnClickDirective,
|
||||
MatomoModule
|
||||
MatomoModule,
|
||||
LabelComponent
|
||||
],
|
||||
providers: [PickupShelfProcessingStatusPipe],
|
||||
})
|
||||
@@ -77,12 +80,24 @@ export class PickUpShelfListItemComponent {
|
||||
return { 'background-color': this._processingStatusPipe.transform(this.item?.processingStatus, true) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the order item has reward points (Lesepunkte).
|
||||
* Returns true if the item has a 'praemie' feature.
|
||||
*/
|
||||
get hasRewardPoints() {
|
||||
return getOrderItemRewardFeature(this.item) !== undefined;
|
||||
}
|
||||
|
||||
selected$ = this.store.selectedListItems$.pipe(
|
||||
map((selectedListItems) =>
|
||||
selectedListItems?.find((item) => item?.orderItemSubsetId === this.item?.orderItemSubsetId),
|
||||
),
|
||||
);
|
||||
|
||||
// Expose to template
|
||||
Labeltype = Labeltype;
|
||||
LabelPriority = LabelPriority;
|
||||
|
||||
constructor(
|
||||
private _elRef: ElementRef,
|
||||
private _environment: EnvironmentService,
|
||||
|
||||
@@ -1,275 +1,328 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import { OpenStreetMap, OpenStreetMapParams, PlaceDto } from '@external/openstreetmap';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import { BranchDTO, BranchType } from '@generated/swagger/checkout-api';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { geoDistance, GeoLocation } from '@utils/common';
|
||||
import { debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
export interface BranchSelectorState {
|
||||
query: string;
|
||||
fetching: boolean;
|
||||
branches: BranchDTO[];
|
||||
filteredBranches: BranchDTO[];
|
||||
selectedBranch?: BranchDTO;
|
||||
online?: boolean;
|
||||
orderingEnabled?: boolean;
|
||||
shippingEnabled?: boolean;
|
||||
filterCurrentBranch?: boolean;
|
||||
currentBranchNumber?: string;
|
||||
orderBy?: 'name' | 'distance';
|
||||
branchType?: number;
|
||||
}
|
||||
|
||||
function branchSorterFn(a: BranchDTO, b: BranchDTO, userBranch: BranchDTO) {
|
||||
return (
|
||||
geoDistance(userBranch?.address?.geoLocation, a?.address?.geoLocation) -
|
||||
geoDistance(userBranch?.address?.geoLocation, b?.address?.geoLocation)
|
||||
);
|
||||
}
|
||||
|
||||
function selectBranches(state: BranchSelectorState) {
|
||||
if (!state?.branches) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let branches = state.branches;
|
||||
|
||||
if (typeof state.online === 'boolean') {
|
||||
branches = branches.filter((branch) => !!branch?.isOnline === state.online);
|
||||
}
|
||||
|
||||
if (typeof state.orderingEnabled === 'boolean') {
|
||||
branches = branches.filter((branch) => !!branch?.isOrderingEnabled === state.orderingEnabled);
|
||||
}
|
||||
|
||||
if (typeof state.shippingEnabled === 'boolean') {
|
||||
branches = branches.filter((branch) => !!branch?.isShippingEnabled === state.shippingEnabled);
|
||||
}
|
||||
|
||||
if (typeof state.filterCurrentBranch === 'boolean' && typeof state.currentBranchNumber === 'string') {
|
||||
branches = branches.filter((branch) => branch?.branchNumber !== state.currentBranchNumber);
|
||||
}
|
||||
|
||||
if (typeof state.orderBy === 'string' && typeof state.currentBranchNumber === 'string') {
|
||||
switch (state.orderBy) {
|
||||
case 'name':
|
||||
branches?.sort((branchA, branchB) => branchA?.name?.localeCompare(branchB?.name));
|
||||
break;
|
||||
case 'distance':
|
||||
const currentBranch = state.branches?.find((b) => b?.branchNumber === state.currentBranchNumber);
|
||||
branches?.sort((a: BranchDTO, b: BranchDTO) => branchSorterFn(a, b, currentBranch));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof state.branchType === 'number') {
|
||||
branches = branches.filter((branch) => branch?.branchType === state.branchType);
|
||||
}
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BranchSelectorStore extends ComponentStore<BranchSelectorState> {
|
||||
get query() {
|
||||
return this.get((s) => s.query);
|
||||
}
|
||||
|
||||
readonly query$ = this.select((s) => s.query);
|
||||
|
||||
get fetching() {
|
||||
return this.get((s) => s.fetching);
|
||||
}
|
||||
|
||||
readonly fetching$ = this.select((s) => s.fetching);
|
||||
|
||||
get branches() {
|
||||
return this.get(selectBranches);
|
||||
}
|
||||
|
||||
readonly branches$ = this.select(selectBranches);
|
||||
|
||||
get filteredBranches() {
|
||||
return this.get((s) => s.filteredBranches);
|
||||
}
|
||||
|
||||
readonly filteredBranches$ = this.select((s) => s.filteredBranches);
|
||||
|
||||
get selectedBranch() {
|
||||
return this.get((s) => s.selectedBranch);
|
||||
}
|
||||
|
||||
readonly selectedBranch$ = this.select((s) => s.selectedBranch);
|
||||
|
||||
constructor(
|
||||
private _availabilityService: DomainAvailabilityService,
|
||||
private _uiModal: UiModalService,
|
||||
private _openStreetMap: OpenStreetMap,
|
||||
auth: AuthService,
|
||||
) {
|
||||
super({
|
||||
query: '',
|
||||
fetching: false,
|
||||
filteredBranches: [],
|
||||
branches: [],
|
||||
online: true,
|
||||
orderingEnabled: true,
|
||||
shippingEnabled: true,
|
||||
filterCurrentBranch: undefined,
|
||||
currentBranchNumber: auth.getClaimByKey('branch_no'),
|
||||
orderBy: 'name',
|
||||
branchType: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
loadBranches = this.effect(($) =>
|
||||
$.pipe(
|
||||
tap((_) => this.setFetching(true)),
|
||||
switchMap(() =>
|
||||
this._availabilityService.getBranches().pipe(
|
||||
withLatestFrom(this.selectedBranch$),
|
||||
tapResponse(
|
||||
([response, selectedBranch]) => this.loadBranchesResponseFn({ response, selectedBranch }),
|
||||
(error: Error) => this.loadBranchesErrorFn(error),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
perimeterSearch = this.effect(($) =>
|
||||
$.pipe(
|
||||
tap((_) => this.beforePerimeterSearch()),
|
||||
debounceTime(500),
|
||||
switchMap(() => {
|
||||
const queryToken = {
|
||||
country: 'Germany',
|
||||
postalcode: this.query,
|
||||
limit: 1,
|
||||
} as OpenStreetMapParams.Query;
|
||||
return this._openStreetMap.query(queryToken).pipe(
|
||||
withLatestFrom(this.branches$),
|
||||
tapResponse(
|
||||
([response, branches]) => this.perimeterSearchResponseFn({ response, branches }),
|
||||
(error: Error) => this.perimeterSearchErrorFn(error),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
beforePerimeterSearch = () => {
|
||||
this.setFilteredBranches([]);
|
||||
this.setFetching(true);
|
||||
};
|
||||
|
||||
perimeterSearchResponseFn = ({ response, branches }: { response: PlaceDto[]; branches: BranchDTO[] }) => {
|
||||
const place = response?.find((_) => true);
|
||||
const branch = this._findNearestBranchByPlace({ place, branches });
|
||||
const filteredBranches = [...branches]
|
||||
?.sort((a: BranchDTO, b: BranchDTO) => branchSorterFn(a, b, branch))
|
||||
?.slice(0, 10);
|
||||
this.setFilteredBranches(filteredBranches ?? []);
|
||||
};
|
||||
|
||||
perimeterSearchErrorFn = (error: Error) => {
|
||||
this.setFilteredBranches([]);
|
||||
console.error('OpenStreetMap Request Failed! ', error);
|
||||
};
|
||||
|
||||
loadBranchesResponseFn = ({ response, selectedBranch }: { response: BranchDTO[]; selectedBranch?: BranchDTO }) => {
|
||||
this.setBranches(response ?? []);
|
||||
if (selectedBranch) {
|
||||
this.setSelectedBranch(selectedBranch);
|
||||
}
|
||||
this.setFetching(false);
|
||||
};
|
||||
|
||||
loadBranchesErrorFn = (error: Error) => {
|
||||
this.setBranches([]);
|
||||
this._uiModal.open({
|
||||
title: 'Fehler beim Laden der Filialen',
|
||||
content: UiErrorModalComponent,
|
||||
data: error,
|
||||
config: { showScrollbarY: false },
|
||||
});
|
||||
};
|
||||
|
||||
setBranches(branches: BranchDTO[]) {
|
||||
this.patchState({ branches });
|
||||
}
|
||||
|
||||
setFilteredBranches(filteredBranches: BranchDTO[]) {
|
||||
this.patchState({ filteredBranches });
|
||||
}
|
||||
|
||||
setSelectedBranch(selectedBranch?: BranchDTO) {
|
||||
if (selectedBranch) {
|
||||
this.patchState({
|
||||
selectedBranch,
|
||||
query: this.formatBranch(selectedBranch),
|
||||
});
|
||||
} else {
|
||||
this.patchState({
|
||||
selectedBranch,
|
||||
query: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setQuery(query: string) {
|
||||
this.patchState({ query });
|
||||
}
|
||||
|
||||
setFetching(fetching: boolean) {
|
||||
this.patchState({ fetching });
|
||||
}
|
||||
|
||||
formatBranch(branch?: BranchDTO) {
|
||||
return branch ? (branch.key ? branch.key + ' - ' + branch.name : branch.name) : '';
|
||||
}
|
||||
|
||||
private _findNearestBranchByPlace({ place, branches }: { place: PlaceDto; branches: BranchDTO[] }): BranchDTO {
|
||||
const placeGeoLocation = { longitude: Number(place?.lon), latitude: Number(place?.lat) } as GeoLocation;
|
||||
return (
|
||||
branches?.reduce((a, b) =>
|
||||
geoDistance(placeGeoLocation, a.address.geoLocation) > geoDistance(placeGeoLocation, b.address.geoLocation)
|
||||
? b
|
||||
: a,
|
||||
) ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
getBranchById(id: number): BranchDTO {
|
||||
return this.branches.find((branch) => branch.id === id);
|
||||
}
|
||||
|
||||
setOnline(online: boolean) {
|
||||
this.patchState({ online });
|
||||
}
|
||||
|
||||
setOrderingEnabled(orderingEnabled: boolean) {
|
||||
this.patchState({ orderingEnabled });
|
||||
}
|
||||
|
||||
setShippingEnabled(shippingEnabled: boolean) {
|
||||
this.patchState({ shippingEnabled });
|
||||
}
|
||||
|
||||
setFilterCurrentBranch(filterCurrentBranch: boolean) {
|
||||
this.patchState({ filterCurrentBranch });
|
||||
}
|
||||
|
||||
setOrderBy(orderBy: 'name' | 'distance') {
|
||||
this.patchState({ orderBy });
|
||||
}
|
||||
|
||||
setBranchType(branchType: BranchType) {
|
||||
this.patchState({ branchType });
|
||||
}
|
||||
}
|
||||
import { Injectable } from '@angular/core';
|
||||
import { AuthService } from '@core/auth';
|
||||
import { DomainAvailabilityService } from '@domain/availability';
|
||||
import {
|
||||
OpenStreetMap,
|
||||
OpenStreetMapParams,
|
||||
PlaceDto,
|
||||
} from '@external/openstreetmap';
|
||||
import { ComponentStore } from '@ngrx/component-store';
|
||||
import { tapResponse } from '@ngrx/operators';
|
||||
|
||||
import { BranchDTO, BranchType } from '@generated/swagger/checkout-api';
|
||||
import { UiErrorModalComponent, UiModalService } from '@ui/modal';
|
||||
import { geoDistance, GeoLocation } from '@utils/common';
|
||||
import { debounceTime, switchMap, tap, withLatestFrom } from 'rxjs/operators';
|
||||
|
||||
export interface BranchSelectorState {
|
||||
query: string;
|
||||
fetching: boolean;
|
||||
branches: BranchDTO[];
|
||||
filteredBranches: BranchDTO[];
|
||||
selectedBranch?: BranchDTO;
|
||||
online?: boolean;
|
||||
orderingEnabled?: boolean;
|
||||
shippingEnabled?: boolean;
|
||||
filterCurrentBranch?: boolean;
|
||||
currentBranchNumber?: string;
|
||||
orderBy?: 'name' | 'distance';
|
||||
branchType?: number;
|
||||
}
|
||||
|
||||
function branchSorterFn(a: BranchDTO, b: BranchDTO, userBranch: BranchDTO) {
|
||||
return (
|
||||
geoDistance(userBranch?.address?.geoLocation, a?.address?.geoLocation) -
|
||||
geoDistance(userBranch?.address?.geoLocation, b?.address?.geoLocation)
|
||||
);
|
||||
}
|
||||
|
||||
function selectBranches(state: BranchSelectorState) {
|
||||
if (!state?.branches) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let branches = state.branches;
|
||||
|
||||
if (typeof state.online === 'boolean') {
|
||||
branches = branches.filter((branch) => !!branch?.isOnline === state.online);
|
||||
}
|
||||
|
||||
if (typeof state.orderingEnabled === 'boolean') {
|
||||
branches = branches.filter(
|
||||
(branch) => !!branch?.isOrderingEnabled === state.orderingEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof state.shippingEnabled === 'boolean') {
|
||||
branches = branches.filter(
|
||||
(branch) => !!branch?.isShippingEnabled === state.shippingEnabled,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof state.filterCurrentBranch === 'boolean' &&
|
||||
typeof state.currentBranchNumber === 'string'
|
||||
) {
|
||||
branches = branches.filter(
|
||||
(branch) => branch?.branchNumber !== state.currentBranchNumber,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
typeof state.orderBy === 'string' &&
|
||||
typeof state.currentBranchNumber === 'string'
|
||||
) {
|
||||
switch (state.orderBy) {
|
||||
case 'name':
|
||||
branches?.sort((branchA, branchB) =>
|
||||
branchA?.name?.localeCompare(branchB?.name),
|
||||
);
|
||||
break;
|
||||
case 'distance': {
|
||||
const currentBranch = state.branches?.find(
|
||||
(b) => b?.branchNumber === state.currentBranchNumber,
|
||||
);
|
||||
branches?.sort((a: BranchDTO, b: BranchDTO) =>
|
||||
branchSorterFn(a, b, currentBranch),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof state.branchType === 'number') {
|
||||
branches = branches.filter(
|
||||
(branch) => branch?.branchType === state.branchType,
|
||||
);
|
||||
}
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BranchSelectorStore extends ComponentStore<BranchSelectorState> {
|
||||
get query() {
|
||||
return this.get((s) => s.query);
|
||||
}
|
||||
|
||||
readonly query$ = this.select((s) => s.query);
|
||||
|
||||
get fetching() {
|
||||
return this.get((s) => s.fetching);
|
||||
}
|
||||
|
||||
readonly fetching$ = this.select((s) => s.fetching);
|
||||
|
||||
get branches() {
|
||||
return this.get(selectBranches);
|
||||
}
|
||||
|
||||
readonly branches$ = this.select(selectBranches);
|
||||
|
||||
get filteredBranches() {
|
||||
return this.get((s) => s.filteredBranches);
|
||||
}
|
||||
|
||||
readonly filteredBranches$ = this.select((s) => s.filteredBranches);
|
||||
|
||||
get selectedBranch() {
|
||||
return this.get((s) => s.selectedBranch);
|
||||
}
|
||||
|
||||
readonly selectedBranch$ = this.select((s) => s.selectedBranch);
|
||||
|
||||
constructor(
|
||||
private _availabilityService: DomainAvailabilityService,
|
||||
private _uiModal: UiModalService,
|
||||
private _openStreetMap: OpenStreetMap,
|
||||
auth: AuthService,
|
||||
) {
|
||||
super({
|
||||
query: '',
|
||||
fetching: false,
|
||||
filteredBranches: [],
|
||||
branches: [],
|
||||
online: true,
|
||||
orderingEnabled: true,
|
||||
shippingEnabled: true,
|
||||
filterCurrentBranch: undefined,
|
||||
currentBranchNumber: auth.getClaimByKey('branch_no'),
|
||||
orderBy: 'name',
|
||||
branchType: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
loadBranches = this.effect(($) =>
|
||||
$.pipe(
|
||||
tap(() => this.setFetching(true)),
|
||||
switchMap(() =>
|
||||
this._availabilityService.getBranches().pipe(
|
||||
withLatestFrom(this.selectedBranch$),
|
||||
tapResponse(
|
||||
([response, selectedBranch]) =>
|
||||
this.loadBranchesResponseFn({ response, selectedBranch }),
|
||||
(error: Error) => this.loadBranchesErrorFn(error),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
perimeterSearch = this.effect(($) =>
|
||||
$.pipe(
|
||||
tap(() => this.beforePerimeterSearch()),
|
||||
debounceTime(500),
|
||||
switchMap(() => {
|
||||
const queryToken = {
|
||||
country: 'Germany',
|
||||
zipCode: this.query,
|
||||
limit: 1,
|
||||
} as OpenStreetMapParams.Query;
|
||||
return this._openStreetMap.query(queryToken).pipe(
|
||||
withLatestFrom(this.branches$),
|
||||
tapResponse(
|
||||
([response, branches]) =>
|
||||
this.perimeterSearchResponseFn({ response, branches }),
|
||||
(error: Error) => this.perimeterSearchErrorFn(error),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
beforePerimeterSearch = () => {
|
||||
this.setFilteredBranches([]);
|
||||
this.setFetching(true);
|
||||
};
|
||||
|
||||
perimeterSearchResponseFn = ({
|
||||
response,
|
||||
branches,
|
||||
}: {
|
||||
response: PlaceDto[];
|
||||
branches: BranchDTO[];
|
||||
}) => {
|
||||
const place = response?.[0];
|
||||
const branch = this._findNearestBranchByPlace({ place, branches });
|
||||
const filteredBranches = [...branches]
|
||||
?.sort((a: BranchDTO, b: BranchDTO) => branchSorterFn(a, b, branch))
|
||||
?.slice(0, 10);
|
||||
this.setFilteredBranches(filteredBranches ?? []);
|
||||
};
|
||||
|
||||
perimeterSearchErrorFn = (error: Error) => {
|
||||
this.setFilteredBranches([]);
|
||||
console.error('OpenStreetMap Request Failed! ', error);
|
||||
};
|
||||
|
||||
loadBranchesResponseFn = ({
|
||||
response,
|
||||
selectedBranch,
|
||||
}: {
|
||||
response: BranchDTO[];
|
||||
selectedBranch?: BranchDTO;
|
||||
}) => {
|
||||
this.setBranches(response ?? []);
|
||||
if (selectedBranch) {
|
||||
this.setSelectedBranch(selectedBranch);
|
||||
}
|
||||
this.setFetching(false);
|
||||
};
|
||||
|
||||
loadBranchesErrorFn = (error: Error) => {
|
||||
this.setBranches([]);
|
||||
this._uiModal.open({
|
||||
title: 'Fehler beim Laden der Filialen',
|
||||
content: UiErrorModalComponent,
|
||||
data: error,
|
||||
config: { showScrollbarY: false },
|
||||
});
|
||||
};
|
||||
|
||||
setBranches(branches: BranchDTO[]) {
|
||||
this.patchState({ branches });
|
||||
}
|
||||
|
||||
setFilteredBranches(filteredBranches: BranchDTO[]) {
|
||||
this.patchState({ filteredBranches });
|
||||
}
|
||||
|
||||
setSelectedBranch(selectedBranch?: BranchDTO) {
|
||||
if (selectedBranch) {
|
||||
this.patchState({
|
||||
selectedBranch,
|
||||
query: this.formatBranch(selectedBranch),
|
||||
});
|
||||
} else {
|
||||
this.patchState({
|
||||
selectedBranch,
|
||||
query: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setQuery(query: string) {
|
||||
this.patchState({ query });
|
||||
}
|
||||
|
||||
setFetching(fetching: boolean) {
|
||||
this.patchState({ fetching });
|
||||
}
|
||||
|
||||
formatBranch(branch?: BranchDTO) {
|
||||
return branch
|
||||
? branch.key
|
||||
? branch.key + ' - ' + branch.name
|
||||
: branch.name
|
||||
: '';
|
||||
}
|
||||
|
||||
private _findNearestBranchByPlace({
|
||||
place,
|
||||
branches,
|
||||
}: {
|
||||
place: PlaceDto;
|
||||
branches: BranchDTO[];
|
||||
}): BranchDTO {
|
||||
const placeGeoLocation = {
|
||||
longitude: Number(place?.lon),
|
||||
latitude: Number(place?.lat),
|
||||
} as GeoLocation;
|
||||
return (
|
||||
branches?.reduce((a, b) =>
|
||||
geoDistance(placeGeoLocation, a.address.geoLocation) >
|
||||
geoDistance(placeGeoLocation, b.address.geoLocation)
|
||||
? b
|
||||
: a,
|
||||
) ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
getBranchById(id: number): BranchDTO {
|
||||
return this.branches.find((branch) => branch.id === id);
|
||||
}
|
||||
|
||||
setOnline(online: boolean) {
|
||||
this.patchState({ online });
|
||||
}
|
||||
|
||||
setOrderingEnabled(orderingEnabled: boolean) {
|
||||
this.patchState({ orderingEnabled });
|
||||
}
|
||||
|
||||
setShippingEnabled(shippingEnabled: boolean) {
|
||||
this.patchState({ shippingEnabled });
|
||||
}
|
||||
|
||||
setFilterCurrentBranch(filterCurrentBranch: boolean) {
|
||||
this.patchState({ filterCurrentBranch });
|
||||
}
|
||||
|
||||
setOrderBy(orderBy: 'name' | 'distance') {
|
||||
this.patchState({ orderBy });
|
||||
}
|
||||
|
||||
setBranchType(branchType: BranchType) {
|
||||
this.patchState({ branchType });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,73 +1,84 @@
|
||||
<div class="searchbox-input-wrapper">
|
||||
<div class="searchbox-hint-wrapper">
|
||||
<input
|
||||
id="searchbox"
|
||||
class="searchbox-input"
|
||||
autocomplete="off"
|
||||
#input
|
||||
type="text"
|
||||
[placeholder]="placeholder"
|
||||
[(ngModel)]="query"
|
||||
(ngModelChange)="setQuery($event, true, true)"
|
||||
(focus)="clearHint(); focused.emit(true)"
|
||||
(blur)="focused.emit(false)"
|
||||
(keyup)="onKeyup($event)"
|
||||
(keyup.enter)="
|
||||
tracker.trackEvent({ action: 'keyup enter', name: 'search' })
|
||||
"
|
||||
matomoTracker
|
||||
#tracker="matomo"
|
||||
matomoCategory="searchbox"
|
||||
/>
|
||||
@if (showHint) {
|
||||
<div class="searchbox-hint" (click)="focus()">
|
||||
{{ hint }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (input.value) {
|
||||
<button
|
||||
(click)="clear(); focus()"
|
||||
tabindex="-1"
|
||||
class="searchbox-clear-btn"
|
||||
type="button"
|
||||
>
|
||||
<shared-icon icon="close" [size]="32"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
@if (!loading) {
|
||||
@if (!showScannerButton) {
|
||||
<button
|
||||
tabindex="0"
|
||||
class="searchbox-search-btn"
|
||||
type="button"
|
||||
(click)="emitSearch()"
|
||||
[disabled]="completeValue !== query"
|
||||
matomoClickAction="click"
|
||||
matomoClickCategory="searchbox"
|
||||
matomoClickName="search"
|
||||
>
|
||||
<ui-icon icon="search" size="1.5rem"></ui-icon>
|
||||
</button>
|
||||
}
|
||||
@if (showScannerButton) {
|
||||
<button
|
||||
tabindex="0"
|
||||
class="searchbox-scan-btn"
|
||||
type="button"
|
||||
(click)="startScan()"
|
||||
matomoClickAction="open"
|
||||
matomoClickCategory="searchbox"
|
||||
matomoClickName="scanner"
|
||||
>
|
||||
<shared-icon icon="barcode-scan" [size]="32"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (loading) {
|
||||
<div class="searchbox-load-indicator">
|
||||
<ui-icon icon="spinner" size="32px"></ui-icon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ng-content select="ui-autocomplete"></ng-content>
|
||||
<div class="searchbox-input-wrapper" role="search">
|
||||
<div class="searchbox-hint-wrapper">
|
||||
<input
|
||||
id="searchbox"
|
||||
class="searchbox-input"
|
||||
autocomplete="off"
|
||||
#input
|
||||
type="text"
|
||||
[placeholder]="placeholder"
|
||||
[(ngModel)]="query"
|
||||
(ngModelChange)="setQuery($event, true, true)"
|
||||
(focus)="clearHint(); focused.emit(true)"
|
||||
(blur)="focused.emit(false)"
|
||||
(keyup)="onKeyup($event)"
|
||||
(keyup.enter)="
|
||||
tracker.trackEvent({ action: 'keyup enter', name: 'search' })
|
||||
"
|
||||
matomoTracker
|
||||
#tracker="matomo"
|
||||
matomoCategory="searchbox"
|
||||
aria-label="Search input"
|
||||
aria-autocomplete="list"
|
||||
[attr.aria-expanded]="autocomplete?.opend || null"
|
||||
[attr.aria-controls]="autocomplete?.opend ? 'searchbox-autocomplete' : null"
|
||||
[attr.aria-activedescendant]="autocomplete?.activeItem ? 'searchbox-item-' + autocomplete?.listKeyManager?.activeItemIndex : null"
|
||||
[attr.aria-busy]="loading || null"
|
||||
[attr.aria-describedby]="showHint ? 'searchbox-hint' : null"
|
||||
/>
|
||||
@if (showHint) {
|
||||
<div id="searchbox-hint" class="searchbox-hint" (click)="focus()" aria-hidden="true">
|
||||
{{ hint }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@if (input.value) {
|
||||
<button
|
||||
(click)="clear(); focus()"
|
||||
tabindex="-1"
|
||||
class="searchbox-clear-btn"
|
||||
type="button"
|
||||
aria-label="Clear"
|
||||
>
|
||||
<shared-icon icon="close" [size]="32" aria-hidden="true"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
@if (!loading) {
|
||||
@if (!showScannerButton) {
|
||||
<button
|
||||
tabindex="0"
|
||||
class="searchbox-search-btn"
|
||||
type="button"
|
||||
(click)="emitSearch()"
|
||||
[disabled]="completeValue !== query"
|
||||
[attr.aria-disabled]="completeValue !== query || null"
|
||||
matomoClickAction="click"
|
||||
matomoClickCategory="searchbox"
|
||||
matomoClickName="search"
|
||||
aria-label="Search"
|
||||
>
|
||||
<ui-icon icon="search" size="1.5rem" aria-hidden="true"></ui-icon>
|
||||
</button>
|
||||
}
|
||||
@if (showScannerButton) {
|
||||
<button
|
||||
tabindex="0"
|
||||
class="searchbox-scan-btn"
|
||||
type="button"
|
||||
(click)="startScan()"
|
||||
matomoClickAction="open"
|
||||
matomoClickCategory="searchbox"
|
||||
matomoClickName="scanner"
|
||||
aria-label="Scan barcode"
|
||||
>
|
||||
<shared-icon icon="barcode-scan" [size]="32" aria-hidden="true"></shared-icon>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
@if (loading) {
|
||||
<div class="searchbox-load-indicator" role="status" aria-live="polite" aria-label="Loading search results">
|
||||
<ui-icon icon="spinner" size="32px" aria-hidden="true"></ui-icon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ng-content select="ui-autocomplete"></ng-content>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { NavigationRoute } from './defs/navigation-route';
|
||||
import {
|
||||
encodeFormData,
|
||||
mapCustomerInfoDtoToCustomerCreateFormData,
|
||||
} from 'apps/isa-app/src/page/customer';
|
||||
} from '@page/customer';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class CustomerCreateNavigation {
|
||||
@@ -58,7 +58,7 @@ export class CustomerCreateNavigation {
|
||||
},
|
||||
];
|
||||
|
||||
let formData = params?.customerInfo
|
||||
const formData = params?.customerInfo
|
||||
? encodeFormData(
|
||||
mapCustomerInfoDtoToCustomerCreateFormData(params.customerInfo),
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<span class="truncate">
|
||||
{{ p.name }}
|
||||
</span>
|
||||
@if (p.type === 'cart') {
|
||||
@if (showCart() && p.type === 'cart') {
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-full px-3 h-[2.375rem] font-bold text-p1 flex flex-row items-center justify-between shopping-cart-count ml-4"
|
||||
@@ -21,9 +21,7 @@
|
||||
(click)="$event?.preventDefault(); $event?.stopPropagation()"
|
||||
>
|
||||
<shared-icon icon="shopping-cart-bold" [size]="22"></shared-icon>
|
||||
<span class="shopping-cart-count-label ml-2">{{
|
||||
cartItemCount$ | async
|
||||
}}</span>
|
||||
<span class="shopping-cart-count-label ml-2">{{ itemCount() }}</span>
|
||||
</button>
|
||||
}
|
||||
</a>
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
computed,
|
||||
input,
|
||||
effect,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { ApplicationProcess, ApplicationService } from '@core/application';
|
||||
@@ -25,8 +26,12 @@ import {
|
||||
combineLatest,
|
||||
isObservable,
|
||||
} from 'rxjs';
|
||||
import { map, switchMap, tap } from 'rxjs/operators';
|
||||
import { map, tap } from 'rxjs/operators';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import {
|
||||
CheckoutMetadataService,
|
||||
ShoppingCartResource,
|
||||
} from '@isa/checkout/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-process-bar-item',
|
||||
@@ -34,14 +39,33 @@ import { TabService } from '@isa/core/tabs';
|
||||
styleUrls: ['process-bar-item.component.css'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: false,
|
||||
providers: [ShoppingCartResource],
|
||||
})
|
||||
export class ShellProcessBarItemComponent
|
||||
implements OnInit, OnDestroy, OnChanges
|
||||
{
|
||||
#tabService = inject(TabService);
|
||||
#checkoutMetadataService = inject(CheckoutMetadataService);
|
||||
#shoppingCartResource = inject(ShoppingCartResource);
|
||||
|
||||
tab = computed(() => this.#tabService.entityMap()[this.process().id]);
|
||||
|
||||
shoppingCartId = computed(() => {
|
||||
return this.#checkoutMetadataService.getShoppingCartId(this.process().id);
|
||||
});
|
||||
|
||||
shoppingCartIdEffect = effect(() => {
|
||||
const shoppingCartId = this.shoppingCartId();
|
||||
untracked(() =>
|
||||
this.#shoppingCartResource.setShoppingCartId(shoppingCartId),
|
||||
);
|
||||
});
|
||||
|
||||
itemCount = computed(() => {
|
||||
const shoppingCart = this.#shoppingCartResource.resource.value();
|
||||
return shoppingCart?.items?.length ?? 0;
|
||||
});
|
||||
|
||||
private _process$ = new BehaviorSubject<ApplicationProcess>(undefined);
|
||||
|
||||
process$ = this._process$.asObservable();
|
||||
@@ -52,15 +76,7 @@ export class ShellProcessBarItemComponent
|
||||
closed = new EventEmitter();
|
||||
|
||||
showCart = computed(() => {
|
||||
const tab = this.tab();
|
||||
|
||||
const pdata = tab.metadata?.process_data as { count?: number };
|
||||
|
||||
if (!pdata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return 'count' in pdata;
|
||||
return true;
|
||||
});
|
||||
|
||||
currentLocationUrlTree = computed(() => {
|
||||
@@ -83,7 +99,7 @@ export class ShellProcessBarItemComponent
|
||||
|
||||
latestBreadcrumb$: Observable<Breadcrumb> = NEVER;
|
||||
|
||||
routerLink$: Observable<string[] | any[]> = NEVER;
|
||||
routerLink$: Observable<string[] | unknown[]> = NEVER;
|
||||
|
||||
queryParams$: Observable<object> = NEVER;
|
||||
|
||||
@@ -112,7 +128,6 @@ export class ShellProcessBarItemComponent
|
||||
this.initQueryParams$();
|
||||
this.initIsActive$();
|
||||
this.initShowCloseButton$();
|
||||
this.initCartItemCount$();
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
@@ -171,15 +186,6 @@ export class ShellProcessBarItemComponent
|
||||
}
|
||||
}
|
||||
|
||||
initCartItemCount$() {
|
||||
this.cartItemCount$ = this.process$.pipe(
|
||||
switchMap((process) =>
|
||||
this._checkout?.getShoppingCart({ processId: process?.id }),
|
||||
),
|
||||
map((cart) => cart?.items?.length ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this._process$.complete();
|
||||
}
|
||||
|
||||
@@ -1,72 +1,74 @@
|
||||
<div
|
||||
class="flex flex-row justify-start items-center h-full max-w-[1920px] desktop-xx-large:max-w-[2448px] relative"
|
||||
(mouseenter)="hovered = true"
|
||||
(mouseleave)="hovered = false"
|
||||
>
|
||||
@if (showScrollArrows) {
|
||||
<button
|
||||
class="scroll-button prev-button"
|
||||
[class.invisible]="!this.hovered || showArrowLeft"
|
||||
(click)="scrollLeft()"
|
||||
>
|
||||
<ui-icon icon="arrow_head" size="22px" rotate="180deg"></ui-icon>
|
||||
</button>
|
||||
}
|
||||
<div
|
||||
#processContainer
|
||||
class="grid grid-flow-col max-w-[calc(100vw-9.5rem)] overflow-x-scroll"
|
||||
(wheel)="onMouseWheel($event)"
|
||||
(scroll)="checkScrollArrowVisibility()"
|
||||
>
|
||||
@for (process of processes$ | async; track trackByFn($index, process)) {
|
||||
<shell-process-bar-item
|
||||
[process]="process"
|
||||
(closed)="checkScrollArrowVisibility()"
|
||||
></shell-process-bar-item>
|
||||
}
|
||||
</div>
|
||||
@if (showScrollArrows) {
|
||||
<button
|
||||
class="scroll-button next-button"
|
||||
[class.invisible]="!this.hovered || showArrowRight"
|
||||
(click)="scrollRight()"
|
||||
>
|
||||
<ui-icon icon="arrow_head" size="22px"></ui-icon>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="grid px-3 shell-process-bar__create-process-btn-desktop start-process-btn grid-flow-col items-center justify-center gap-[0.625rem] grow-0 shrink-0"
|
||||
(click)="createProcess('product')"
|
||||
type="button"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="createProcessButtonContent"></ng-container>
|
||||
</button>
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="!(processes$ | async)?.length"
|
||||
class="grow-0 shrink-0 px-3 mr-[.125rem] shell-process-bar__close-processes"
|
||||
(click)="closeAllProcesses()"
|
||||
>
|
||||
<div
|
||||
class="rounded border border-solid flex flex-row pl-3 pr-[0.625rem] py-[0.375rem]"
|
||||
[class.text-brand]="(processes$ | async)?.length"
|
||||
[class.border-brand]="(processes$ | async)?.length"
|
||||
[class.text-[#AEB7C1]]="!(processes$ | async)?.length"
|
||||
[class.border-[#AEB7C1]]="!(processes$ | async)?.length"
|
||||
>
|
||||
<span class="mr-1">{{ (processes$ | async)?.length }}</span>
|
||||
<shared-icon icon="close"></shared-icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-template #createProcessButtonContent>
|
||||
<div class="bg-brand text-white w-[2.375rem] h-[2.375rem] rounded-full grid items-center justify-center mx-auto mb-1">
|
||||
<shared-icon icon="add"></shared-icon>
|
||||
</div>
|
||||
@if (showStartProcessText$ | async) {
|
||||
<span class="text-brand create-process-btn-text">Vorgang starten</span>
|
||||
}
|
||||
</ng-template>
|
||||
<div
|
||||
class="flex flex-row justify-start items-center h-full max-w-[1920px] desktop-xx-large:max-w-[2448px] relative"
|
||||
(mouseenter)="hovered = true"
|
||||
(mouseleave)="hovered = false"
|
||||
>
|
||||
@if (showScrollArrows) {
|
||||
<button
|
||||
class="scroll-button prev-button"
|
||||
[class.invisible]="!this.hovered || showArrowLeft"
|
||||
(click)="scrollLeft()"
|
||||
>
|
||||
<ui-icon icon="arrow_head" size="22px" rotate="180deg"></ui-icon>
|
||||
</button>
|
||||
}
|
||||
<div
|
||||
#processContainer
|
||||
class="grid grid-flow-col max-w-[calc(100vw-9.5rem)] overflow-x-scroll"
|
||||
(wheel)="onMouseWheel($event)"
|
||||
(scroll)="checkScrollArrowVisibility()"
|
||||
>
|
||||
@for (process of processes$ | async; track process.id) {
|
||||
<shell-process-bar-item
|
||||
[process]="process"
|
||||
(closed)="checkScrollArrowVisibility()"
|
||||
></shell-process-bar-item>
|
||||
}
|
||||
</div>
|
||||
@if (showScrollArrows) {
|
||||
<button
|
||||
class="scroll-button next-button"
|
||||
[class.invisible]="!this.hovered || showArrowRight"
|
||||
(click)="scrollRight()"
|
||||
>
|
||||
<ui-icon icon="arrow_head" size="22px"></ui-icon>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="grid px-3 shell-process-bar__create-process-btn-desktop start-process-btn grid-flow-col items-center justify-center gap-[0.625rem] grow-0 shrink-0"
|
||||
(click)="createProcess('product')"
|
||||
type="button"
|
||||
>
|
||||
<ng-container *ngTemplateOutlet="createProcessButtonContent"></ng-container>
|
||||
</button>
|
||||
<div class="grow"></div>
|
||||
<button
|
||||
type="button"
|
||||
[disabled]="!(processes$ | async)?.length"
|
||||
class="grow-0 shrink-0 px-3 mr-[.125rem] shell-process-bar__close-processes"
|
||||
(click)="closeAllProcesses()"
|
||||
>
|
||||
<div
|
||||
class="rounded border border-solid flex flex-row pl-3 pr-[0.625rem] py-[0.375rem]"
|
||||
[class.text-brand]="(processes$ | async)?.length"
|
||||
[class.border-brand]="(processes$ | async)?.length"
|
||||
[class.text-[#AEB7C1]]="!(processes$ | async)?.length"
|
||||
[class.border-[#AEB7C1]]="!(processes$ | async)?.length"
|
||||
>
|
||||
<span class="mr-1">{{ (processes$ | async)?.length }}</span>
|
||||
<shared-icon icon="close"></shared-icon>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ng-template #createProcessButtonContent>
|
||||
<div
|
||||
class="bg-brand text-white w-[2.375rem] h-[2.375rem] rounded-full grid items-center justify-center mx-auto mb-1"
|
||||
>
|
||||
<shared-icon icon="add"></shared-icon>
|
||||
</div>
|
||||
@if (showStartProcessText$ | async) {
|
||||
<span class="text-brand create-process-btn-text">Vorgang starten</span>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { coerceArray } from '@angular/cdk/coercion';
|
||||
import {
|
||||
Component,
|
||||
ChangeDetectionStrategy,
|
||||
@@ -65,10 +64,15 @@ export class ShellProcessBarComponent implements OnInit {
|
||||
}
|
||||
|
||||
initProcesses$() {
|
||||
// TODO: Use implementation from develop
|
||||
this.processes$ = this.section$.pipe(
|
||||
switchMap((section) => this._app.getProcesses$(section)),
|
||||
// TODO: Nach Prämie release kann der Filter rausgenommen werden
|
||||
map((processes) =>
|
||||
processes.filter((process) => process.type === 'cart'),
|
||||
processes.filter(
|
||||
(process) =>
|
||||
process.type === 'cart' || process.type === 'cart-checkout',
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<span class="side-menu-group-item-label">Erfassen</span>
|
||||
</a>
|
||||
}
|
||||
<!-- @if (customerRewardRoute()) {
|
||||
@if (customerRewardRoute()) {
|
||||
<a
|
||||
class="side-menu-group-item"
|
||||
(click)="closeSideMenu(); focusSearchBox()"
|
||||
@@ -86,10 +86,18 @@
|
||||
sharedRegexRouterLinkActive="active"
|
||||
sharedRegexRouterLinkActiveTest="^\/\d*\/reward"
|
||||
>
|
||||
<span class="side-menu-group-item-icon"> </span>
|
||||
<span class="side-menu-group-item-icon">
|
||||
<shell-reward-shopping-cart-indicator />
|
||||
@if (hasShoppingCartItems()) {
|
||||
<span
|
||||
class="w-2 h-2 bg-isa-accent-red rounded-full"
|
||||
data-what="open-reward-tasks-indicator"
|
||||
></span>
|
||||
}
|
||||
</span>
|
||||
<span class="side-menu-group-item-label">Prämienshop</span>
|
||||
</a>
|
||||
} -->
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ import { TabService } from '@isa/core/tabs';
|
||||
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
||||
import { isaNavigationRemission2, isaNavigationReturn } from '@isa/icons';
|
||||
import z from 'zod';
|
||||
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
|
||||
|
||||
@Component({
|
||||
selector: 'shell-side-menu',
|
||||
@@ -71,6 +72,7 @@ export class ShellSideMenuComponent {
|
||||
#cdr = inject(ChangeDetectorRef);
|
||||
#document = inject(DOCUMENT);
|
||||
tabService = inject(TabService);
|
||||
#shoppingCartResource = inject(SelectedRewardShoppingCartResource);
|
||||
|
||||
staticTabIds = Object.values(
|
||||
this.#config.get('process.ids', z.record(z.coerce.number())),
|
||||
@@ -151,6 +153,10 @@ export class ShellSideMenuComponent {
|
||||
return this.#router.createUrlTree(['/', tabId || this.nextId(), routeName]);
|
||||
});
|
||||
|
||||
hasShoppingCartItems = computed(() => {
|
||||
return this.#shoppingCartResource.resource.value()?.items?.length > 0;
|
||||
});
|
||||
|
||||
pickUpShelfOutRoutePath$ = this.getLastActivatedCustomerProcessId$().pipe(
|
||||
map((processId) => {
|
||||
if (processId) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { ShellSideMenuComponent } from './side-menu.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [ShellSideMenuComponent],
|
||||
exports: [ShellSideMenuComponent],
|
||||
})
|
||||
export class ShellSideMenuModule {}
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { ShellSideMenuComponent } from './side-menu.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [ShellSideMenuComponent],
|
||||
exports: [ShellSideMenuComponent],
|
||||
})
|
||||
export class ShellSideMenuModule {}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
@layer components {
|
||||
@import "../../../libs/ui/buttons/src/buttons.scss";
|
||||
@import "../../../libs/ui/bullet-list/src/bullet-list.scss";
|
||||
@import "../../../libs/ui/carousel/src/lib/_carousel.scss";
|
||||
@import "../../../libs/ui/datepicker/src/datepicker.scss";
|
||||
@import "../../../libs/ui/dialog/src/dialog.scss";
|
||||
@import "../../../libs/ui/input-controls/src/input-controls.scss";
|
||||
@@ -19,6 +20,7 @@
|
||||
@import "../../../libs/ui/skeleton-loader/src/skeleton-loader.scss";
|
||||
@import "../../../libs/ui/tooltip/src/tooltip.scss";
|
||||
@import "../../../libs/ui/label/src/label.scss";
|
||||
@import "../../../libs/ui/switch/src/switch.scss";
|
||||
|
||||
.input-control {
|
||||
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
applicationConfig,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
|
||||
import { DestinationInfoComponent } from '@isa/checkout/shared/product-info';
|
||||
import { ShippingTarget } from '@isa/checkout/data-access';
|
||||
|
||||
const meta: Meta<DestinationInfoComponent> = {
|
||||
title: 'checkout/shared/product-info/DestinationInfoComponent',
|
||||
component: DestinationInfoComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [provideHttpClient()],
|
||||
}),
|
||||
moduleMetadata({
|
||||
imports: [],
|
||||
providers: [],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<DestinationInfoComponent>;
|
||||
|
||||
export const Delivery: Story = {
|
||||
args: {
|
||||
underline: true,
|
||||
shoppingCartItem: {
|
||||
availability: {
|
||||
estimatedDelivery: {
|
||||
start: '2024-06-10T00:00:00+02:00',
|
||||
stop: '2024-06-12T00:00:00+02:00',
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
data: {
|
||||
target: ShippingTarget.Delivery,
|
||||
},
|
||||
},
|
||||
features: {
|
||||
orderType: 'Versand',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Pickup: Story = {
|
||||
args: {
|
||||
shoppingCartItem: {
|
||||
availability: {
|
||||
estimatedDelivery: {
|
||||
start: '2024-06-10T00:00:00+02:00',
|
||||
stop: '2024-06-12T00:00:00+02:00',
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
data: {
|
||||
target: ShippingTarget.Branch,
|
||||
targetBranch: {
|
||||
data: {
|
||||
name: 'Musterfiliale',
|
||||
address: {
|
||||
street: 'Musterstraße',
|
||||
streetNumber: '1',
|
||||
zipCode: '12345',
|
||||
city: 'Musterstadt',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
features: {
|
||||
orderType: 'Abholung',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const InStore: Story = {
|
||||
args: {
|
||||
shoppingCartItem: {
|
||||
availability: {
|
||||
estimatedDelivery: {
|
||||
start: '2024-06-10T00:00:00+02:00',
|
||||
stop: '2024-06-12T00:00:00+02:00',
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
data: {
|
||||
target: ShippingTarget.Branch,
|
||||
targetBranch: {
|
||||
data: {
|
||||
name: 'Musterfiliale',
|
||||
address: {
|
||||
street: 'Musterstraße',
|
||||
streetNumber: '1',
|
||||
zipCode: '12345',
|
||||
city: 'Musterstadt',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
features: {
|
||||
orderType: 'Rücklage',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
applicationConfig,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { ProductInfoRedemptionComponent } from '@isa/checkout/shared/product-info';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
const meta: Meta<ProductInfoRedemptionComponent> = {
|
||||
title: 'checkout/shared/product-info/ProductInfoRedemption',
|
||||
component: ProductInfoRedemptionComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [
|
||||
provideRouter([
|
||||
{ path: ':ean', component: ProductInfoRedemptionComponent },
|
||||
]),
|
||||
],
|
||||
}),
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductInfoRedemptionComponent>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
item: {
|
||||
product: {
|
||||
ean: '9783498007706',
|
||||
name: 'Die Assistentin',
|
||||
contributors: 'Wahl, Caroline',
|
||||
format: 'TB',
|
||||
formatDetail: 'Taschenbuch (Kartoniert)',
|
||||
manufacturer: 'Test Manufacturer',
|
||||
publicationDate: '2023-01-01',
|
||||
},
|
||||
redemptionPoints: 100,
|
||||
},
|
||||
orientation: 'vertical',
|
||||
},
|
||||
argTypes: {
|
||||
item: { control: 'object' },
|
||||
orientation: {
|
||||
control: { type: 'radio' },
|
||||
options: ['horizontal', 'vertical'],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
applicationConfig,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
import { ProductInfoComponent } from '@isa/checkout/shared/product-info';
|
||||
import { provideProductImageUrl } from '@isa/shared/product-image';
|
||||
import { provideProductRouterLinkBuilder } from '@isa/shared/product-router-link';
|
||||
import { provideRouter } from '@angular/router';
|
||||
|
||||
const mockProduct = {
|
||||
ean: '9783498007706',
|
||||
name: 'Die Assistentin',
|
||||
contributors: 'Wahl, Caroline',
|
||||
};
|
||||
|
||||
const meta: Meta<ProductInfoComponent> = {
|
||||
title: 'checkout/shared/product-info/ProductInfo',
|
||||
component: ProductInfoComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [
|
||||
provideRouter([{ path: ':ean', component: ProductInfoComponent }]),
|
||||
],
|
||||
}),
|
||||
moduleMetadata({
|
||||
providers: [
|
||||
provideProductImageUrl('https://produktbilder-test.paragon-data.net'),
|
||||
provideProductRouterLinkBuilder((ean: string) => ean),
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<ProductInfoComponent>;
|
||||
|
||||
export const BasicWithoutContent: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
argTypes: {
|
||||
item: { control: 'object' },
|
||||
nameSize: {
|
||||
control: { type: 'radio' },
|
||||
options: ['small', 'medium', 'large'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SmallNameSize: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'small',
|
||||
},
|
||||
};
|
||||
|
||||
export const MediumNameSize: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
};
|
||||
|
||||
export const LargeNameSize: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'large',
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLesepunkte: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<checkout-product-info [item]="item" [nameSize]="nameSize">
|
||||
<div class="isa-text-body-2-regular">
|
||||
<span class="isa-text-body-2-bold">150</span> Lesepunkte
|
||||
</div>
|
||||
</checkout-product-info>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithManufacturer: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<checkout-product-info [item]="item" [nameSize]="nameSize">
|
||||
<div class="isa-text-body-2-regular text-neutral-600">
|
||||
Rowohlt Taschenbuch
|
||||
</div>
|
||||
</checkout-product-info>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export const WithMultipleRows: Story = {
|
||||
args: {
|
||||
item: mockProduct,
|
||||
nameSize: 'medium',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `
|
||||
<checkout-product-info [item]="item" [nameSize]="nameSize">
|
||||
<div class="isa-text-body-2-regular">
|
||||
<span class="isa-text-body-2-bold">150</span> Lesepunkte
|
||||
</div>
|
||||
<div class="isa-text-body-2-regular text-neutral-600">
|
||||
Rowohlt Taschenbuch
|
||||
</div>
|
||||
<div class="isa-text-body-2-regular text-neutral-600">
|
||||
Erschienen: 01. Januar 2023
|
||||
</div>
|
||||
</checkout-product-info>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
applicationConfig,
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
} from '@storybook/angular';
|
||||
|
||||
import { StockInfoComponent } from '@isa/checkout/shared/product-info';
|
||||
import { RemissionStockService } from '@isa/remission/data-access';
|
||||
import { StockInfoDTO } from '@generated/swagger/inventory-api';
|
||||
|
||||
const meta: Meta<StockInfoComponent> = {
|
||||
title: 'checkout/shared/product-info/StockInfoComponent',
|
||||
component: StockInfoComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [],
|
||||
}),
|
||||
moduleMetadata({
|
||||
imports: [],
|
||||
providers: [
|
||||
{
|
||||
provide: RemissionStockService,
|
||||
useValue: {
|
||||
fetchStock: async (
|
||||
params: { itemIds: number[]; assignedStockId?: number },
|
||||
abortSignal?: AbortSignal,
|
||||
) => {
|
||||
const result: StockInfoDTO = {
|
||||
itemId: params.itemIds[0],
|
||||
stockId: params.assignedStockId,
|
||||
inStock: 14,
|
||||
};
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return [result];
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<StockInfoComponent>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
item: {
|
||||
id: 123456,
|
||||
catalogAvailability: {
|
||||
ssc: '999',
|
||||
sscText: 'Lieferbar in 1-3 Werktagen',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
183
apps/isa-app/stories/shared/address/address.component.stories.ts
Normal file
183
apps/isa-app/stories/shared/address/address.component.stories.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
applicationConfig,
|
||||
argsToTemplate,
|
||||
} from '@storybook/angular';
|
||||
|
||||
import { AddressComponent, Address } from '@isa/shared/address';
|
||||
import { CountryResource } from '@isa/crm/data-access';
|
||||
|
||||
const meta: Meta<AddressComponent> = {
|
||||
title: 'shared/address/AddressComponent',
|
||||
component: AddressComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [
|
||||
{
|
||||
provide: CountryResource,
|
||||
useValue: {
|
||||
resource: {
|
||||
value: () => [
|
||||
{ isO3166_A_3: 'DEU', name: 'Germany' },
|
||||
{ isO3166_A_3: 'FRA', name: 'France' },
|
||||
{ isO3166_A_3: 'AUT', name: 'Austria' },
|
||||
{ isO3166_A_3: 'USA', name: 'United States' },
|
||||
{ isO3166_A_3: 'CHE', name: 'Switzerland' },
|
||||
{ isO3166_A_3: 'ITA', name: 'Italy' },
|
||||
{ isO3166_A_3: 'ESP', name: 'Spain' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
address: {
|
||||
control: 'object',
|
||||
description: 'The address object to display',
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<shared-address ${argsToTemplate(args)}></shared-address>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<AddressComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
address: {
|
||||
careOf: 'John Doe',
|
||||
street: 'Hauptstraße',
|
||||
streetNumber: '42',
|
||||
apartment: 'Apt 3B',
|
||||
info: 'Building A, 3rd Floor',
|
||||
zipCode: '10115',
|
||||
city: 'Berlin',
|
||||
country: 'DEU',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GermanAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Maximilianstraße',
|
||||
streetNumber: '15',
|
||||
zipCode: '80539',
|
||||
city: 'München',
|
||||
country: 'DEU',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FrenchAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Rue de la Paix',
|
||||
streetNumber: '25',
|
||||
zipCode: '75002',
|
||||
city: 'Paris',
|
||||
country: 'FRA',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AustrianAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Stephansplatz',
|
||||
streetNumber: '1',
|
||||
zipCode: '1010',
|
||||
city: 'Wien',
|
||||
country: 'AUT',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SwissAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Bahnhofstrasse',
|
||||
streetNumber: '50',
|
||||
zipCode: '8001',
|
||||
city: 'Zürich',
|
||||
country: 'CHE',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCareOf: Story = {
|
||||
args: {
|
||||
address: {
|
||||
careOf: 'Maria Schmidt',
|
||||
street: 'Berliner Straße',
|
||||
streetNumber: '100',
|
||||
zipCode: '60311',
|
||||
city: 'Frankfurt am Main',
|
||||
country: 'DEU',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithApartment: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Lindenallee',
|
||||
streetNumber: '23',
|
||||
apartment: 'Wohnung 5A',
|
||||
zipCode: '50668',
|
||||
city: 'Köln',
|
||||
country: 'DEU',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAdditionalInfo: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Industriestraße',
|
||||
streetNumber: '7',
|
||||
info: 'Hintereingang, 2. Stock rechts',
|
||||
zipCode: '70565',
|
||||
city: 'Stuttgart',
|
||||
country: 'DEU',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const MinimalAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Dorfstraße',
|
||||
city: 'Neustadt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CompleteInternational: Story = {
|
||||
args: {
|
||||
address: {
|
||||
careOf: 'Jane Smith',
|
||||
street: 'Fifth Avenue',
|
||||
streetNumber: '350',
|
||||
apartment: 'Suite 2000',
|
||||
info: 'Empire State Building',
|
||||
zipCode: '10118',
|
||||
city: 'New York',
|
||||
state: 'NY',
|
||||
country: 'USA',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyAddress: Story = {
|
||||
args: {
|
||||
address: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
applicationConfig,
|
||||
argsToTemplate,
|
||||
} from '@storybook/angular';
|
||||
|
||||
import { InlineAddressComponent, Address } from '@isa/shared/address';
|
||||
import { CountryResource } from '@isa/crm/data-access';
|
||||
|
||||
const meta: Meta<InlineAddressComponent> = {
|
||||
title: 'shared/address/InlineAddressComponent',
|
||||
component: InlineAddressComponent,
|
||||
decorators: [
|
||||
applicationConfig({
|
||||
providers: [
|
||||
{
|
||||
provide: CountryResource,
|
||||
useValue: {
|
||||
resource: {
|
||||
value: () => [
|
||||
{ isO3166_A_3: 'DEU', name: 'Germany' },
|
||||
{ isO3166_A_3: 'FRA', name: 'France' },
|
||||
{ isO3166_A_3: 'AUT', name: 'Austria' },
|
||||
{ isO3166_A_3: 'USA', name: 'United States' },
|
||||
{ isO3166_A_3: 'CHE', name: 'Switzerland' },
|
||||
{ isO3166_A_3: 'ITA', name: 'Italy' },
|
||||
{ isO3166_A_3: 'ESP', name: 'Spain' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
address: {
|
||||
control: 'object',
|
||||
description: 'The address object to display in inline format',
|
||||
},
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<shared-inline-address ${argsToTemplate(args)}></shared-inline-address>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<InlineAddressComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Hauptstraße',
|
||||
streetNumber: '42',
|
||||
zipCode: '10115',
|
||||
city: 'Berlin',
|
||||
country: 'DEU',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const GermanAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Maximilianstraße',
|
||||
streetNumber: '15',
|
||||
zipCode: '80539',
|
||||
city: 'München',
|
||||
country: 'DEU',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FrenchAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Rue de la Paix',
|
||||
streetNumber: '25',
|
||||
zipCode: '75002',
|
||||
city: 'Paris',
|
||||
country: 'FRA',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const AustrianAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Stephansplatz',
|
||||
streetNumber: '1',
|
||||
zipCode: '1010',
|
||||
city: 'Wien',
|
||||
country: 'AUT',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SwissAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Bahnhofstrasse',
|
||||
streetNumber: '50',
|
||||
zipCode: '8001',
|
||||
city: 'Zürich',
|
||||
country: 'CHE',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const USAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Fifth Avenue',
|
||||
streetNumber: '350',
|
||||
zipCode: '10118',
|
||||
city: 'New York',
|
||||
country: 'USA',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const ShortAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Dorfstraße',
|
||||
streetNumber: '5',
|
||||
city: 'Neustadt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const StreetOnly: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Hauptstraße',
|
||||
streetNumber: '10',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CityOnly: Story = {
|
||||
args: {
|
||||
address: {
|
||||
zipCode: '12345',
|
||||
city: 'Beispielstadt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoCountry: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Teststraße',
|
||||
streetNumber: '99',
|
||||
zipCode: '54321',
|
||||
city: 'Musterstadt',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCountryLookup: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Via Roma',
|
||||
streetNumber: '10',
|
||||
zipCode: '00100',
|
||||
city: 'Roma',
|
||||
country: 'ITA',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const SpanishAddress: Story = {
|
||||
args: {
|
||||
address: {
|
||||
street: 'Calle Mayor',
|
||||
streetNumber: '1',
|
||||
zipCode: '28013',
|
||||
city: 'Madrid',
|
||||
country: 'ESP',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyAddress: Story = {
|
||||
args: {
|
||||
address: {},
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Meta, argsToTemplate } from '@storybook/angular';
|
||||
import { ProductFormatIconGroup } from '@isa/icons';
|
||||
import { ProductFormatIconComponent } from '@isa/shared/product-foramt';
|
||||
import { ProductFormatIconComponent } from '@isa/shared/product-format';
|
||||
|
||||
type ProductFormatInputs = {
|
||||
format: string;
|
||||
|
||||
@@ -1,48 +1,57 @@
|
||||
import { argsToTemplate, Meta } from '@storybook/angular';
|
||||
import { ProductFormatIconGroup } from '@isa/icons';
|
||||
import { ProductFormatComponent } from '@isa/shared/product-foramt';
|
||||
|
||||
type ProductFormatInputs = {
|
||||
format: string;
|
||||
formatDetail: string;
|
||||
};
|
||||
|
||||
const options = Object.keys(ProductFormatIconGroup).map((key) =>
|
||||
key.toUpperCase(),
|
||||
);
|
||||
|
||||
const meta: Meta<ProductFormatInputs> = {
|
||||
title: 'shared/product-format/ProductFormat',
|
||||
component: ProductFormatComponent,
|
||||
argTypes: {
|
||||
format: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options,
|
||||
description: 'The product format to display the icon for.',
|
||||
defaultValue: options[0],
|
||||
},
|
||||
formatDetail: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
description: 'The detail text for the product format.',
|
||||
defaultValue: 'Default Format Detail',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
format: options[0], // Default value for the product format
|
||||
formatDetail: 'Default Format Detail', // Default value for the format detail
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<shared-product-format ${argsToTemplate(args)}></shared-product-format>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = typeof meta;
|
||||
|
||||
export const Default: Story = {};
|
||||
import { argsToTemplate, Meta } from '@storybook/angular';
|
||||
import { ProductFormatIconGroup } from '@isa/icons';
|
||||
import { ProductFormatComponent } from '@isa/shared/product-format';
|
||||
|
||||
type ProductFormatInputs = {
|
||||
format: string;
|
||||
formatDetail: string;
|
||||
formatDetailsBold: boolean;
|
||||
};
|
||||
|
||||
const options = Object.keys(ProductFormatIconGroup).map((key) =>
|
||||
key.toUpperCase(),
|
||||
);
|
||||
|
||||
const meta: Meta<ProductFormatInputs> = {
|
||||
title: 'shared/product-format/ProductFormat',
|
||||
component: ProductFormatComponent,
|
||||
argTypes: {
|
||||
format: {
|
||||
control: {
|
||||
type: 'select',
|
||||
},
|
||||
options,
|
||||
description: 'The product format to display the icon for.',
|
||||
defaultValue: options[0],
|
||||
},
|
||||
formatDetail: {
|
||||
control: {
|
||||
type: 'text',
|
||||
},
|
||||
description: 'The detail text for the product format.',
|
||||
defaultValue: 'Default Format Detail',
|
||||
},
|
||||
formatDetailsBold: {
|
||||
control: {
|
||||
type: 'boolean',
|
||||
},
|
||||
description: 'Whether the format detail text should be bold.',
|
||||
defaultValue: false,
|
||||
},
|
||||
},
|
||||
args: {
|
||||
format: options[0], // Default value for the product format
|
||||
formatDetail: 'Default Format Detail', // Default value for the format detail
|
||||
formatDetailsBold: false, // Default value for the format details bold
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<shared-product-format ${argsToTemplate(args)}></shared-product-format>`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = typeof meta;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import {
|
||||
argsToTemplate,
|
||||
moduleMetadata,
|
||||
type Meta,
|
||||
type StoryObj,
|
||||
} from '@storybook/angular';
|
||||
import { QuantityControlComponent } from '@isa/shared/quantity-control';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
interface QuantityControlStoryProps {
|
||||
value: number;
|
||||
disabled: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
presetLimit?: number;
|
||||
}
|
||||
|
||||
const meta: Meta<QuantityControlStoryProps> = {
|
||||
component: QuantityControlComponent,
|
||||
title: 'shared/quantity-control/QuantityControl',
|
||||
argTypes: {
|
||||
value: {
|
||||
control: { type: 'number', min: 0, max: 99 },
|
||||
description: 'The quantity value',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disables the control when true',
|
||||
},
|
||||
min: {
|
||||
control: { type: 'number', min: 0, max: 10 },
|
||||
description: 'Minimum selectable value',
|
||||
},
|
||||
max: {
|
||||
control: { type: 'number', min: 1, max: 99 },
|
||||
description: 'Maximum selectable value (e.g., stock available)',
|
||||
},
|
||||
presetLimit: {
|
||||
control: { type: 'number', min: 1, max: 99 },
|
||||
description: 'Number of preset options before requiring Edit',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
value: 1,
|
||||
disabled: false,
|
||||
min: 1,
|
||||
max: undefined,
|
||||
presetLimit: 10,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<shared-quantity-control ${argsToTemplate(args)} />`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<QuantityControlStoryProps>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
value: 1,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithCustomValue: Story = {
|
||||
args: {
|
||||
value: 5,
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const HighStock: Story = {
|
||||
args: {
|
||||
value: 15,
|
||||
disabled: false,
|
||||
min: 1,
|
||||
max: 50,
|
||||
presetLimit: 20, // Shows 1-20, Edit for 21-50
|
||||
},
|
||||
};
|
||||
|
||||
export const LimitedStock: Story = {
|
||||
args: {
|
||||
value: 3,
|
||||
disabled: false,
|
||||
min: 1,
|
||||
max: 5,
|
||||
presetLimit: 10, // Shows 1-5 (capped at max), no Edit
|
||||
},
|
||||
};
|
||||
|
||||
export const ExactStock: Story = {
|
||||
args: {
|
||||
value: 1,
|
||||
disabled: false,
|
||||
min: 1,
|
||||
max: 10,
|
||||
presetLimit: 10, // Shows 1-10, no Edit (max=10 == presetLimit)
|
||||
},
|
||||
};
|
||||
|
||||
export const StartFromZero: Story = {
|
||||
args: {
|
||||
value: 0,
|
||||
disabled: false,
|
||||
min: 0,
|
||||
max: undefined,
|
||||
presetLimit: 10, // Shows 0-9, Edit for unlimited
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
value: 3,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const InFormContext: Story = {
|
||||
args: {
|
||||
value: 2,
|
||||
disabled: false,
|
||||
},
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [ReactiveFormsModule],
|
||||
}),
|
||||
],
|
||||
render: (args) => ({
|
||||
props: {
|
||||
...args,
|
||||
quantityControl: new FormControl(args.value),
|
||||
},
|
||||
template: `
|
||||
<div style="display: flex; flex-direction: column; gap: 1rem;">
|
||||
<shared-quantity-control [formControl]="quantityControl" />
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #f5f5f5; border-radius: 4px;">
|
||||
<strong>Form Value:</strong> {{ quantityControl.value }}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
141
apps/isa-app/stories/ui/carousel/carousel.stories.ts
Normal file
141
apps/isa-app/stories/ui/carousel/carousel.stories.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { type Meta, type StoryObj, moduleMetadata } from '@storybook/angular';
|
||||
import { CarouselComponent } from '@isa/ui/carousel';
|
||||
import { QuoteCardComponent } from './quote-card.component';
|
||||
|
||||
// Collection of developer/inspirational quotes
|
||||
const quotes = [
|
||||
{ id: 1, text: 'Code is like humor. When you have to explain it, it\'s bad.', author: 'Cory House' },
|
||||
{ id: 2, text: 'First, solve the problem. Then, write the code.', author: 'John Johnson' },
|
||||
{ id: 3, text: 'Simplicity is the soul of efficiency.', author: 'Austin Freeman' },
|
||||
{ id: 4, text: 'Make it work, make it right, make it fast.', author: 'Kent Beck' },
|
||||
{ id: 5, text: 'Clean code always looks like it was written by someone who cares.', author: 'Robert C. Martin' },
|
||||
{ id: 6, text: 'Any fool can write code that a computer can understand. Good programmers write code that humans can understand.', author: 'Martin Fowler' },
|
||||
{ id: 7, text: 'Experience is the name everyone gives to their mistakes.', author: 'Oscar Wilde' },
|
||||
{ id: 8, text: 'In order to be irreplaceable, one must always be different.', author: 'Coco Chanel' },
|
||||
{ id: 9, text: 'The best way to predict the future is to invent it.', author: 'Alan Kay' },
|
||||
{ id: 10, text: 'Programs must be written for people to read, and only incidentally for machines to execute.', author: 'Harold Abelson' },
|
||||
{ id: 11, text: 'Testing leads to failure, and failure leads to understanding.', author: 'Burt Rutan' },
|
||||
{ id: 12, text: 'It\'s not a bug – it\'s an undocumented feature.', author: 'Anonymous' },
|
||||
{ id: 13, text: 'Software is a great combination between artistry and engineering.', author: 'Bill Gates' },
|
||||
{ id: 14, text: 'Talk is cheap. Show me the code.', author: 'Linus Torvalds' },
|
||||
{ id: 15, text: 'Sometimes it pays to stay in bed on Monday, rather than spending the rest of the week debugging Monday\'s code.', author: 'Dan Salomon' },
|
||||
];
|
||||
|
||||
// Helper function to generate a specified number of quotes
|
||||
function generateQuotes(count: number) {
|
||||
const result = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(quotes[i % quotes.length]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
interface CarouselStoryProps {
|
||||
gap: string;
|
||||
arrowAutoHide: boolean;
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
const meta: Meta<CarouselStoryProps> = {
|
||||
component: CarouselComponent,
|
||||
title: 'ui/carousel/Carousel',
|
||||
decorators: [
|
||||
moduleMetadata({
|
||||
imports: [QuoteCardComponent],
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
gap: {
|
||||
control: 'text',
|
||||
description: 'CSS gap value for spacing between carousel items',
|
||||
},
|
||||
arrowAutoHide: {
|
||||
control: 'boolean',
|
||||
description: 'Whether to auto-hide arrows until carousel is hovered or focused',
|
||||
},
|
||||
itemCount: {
|
||||
control: { type: 'number', min: 3, max: 20 },
|
||||
description: 'Number of quote cards to render',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
gap: '1rem',
|
||||
arrowAutoHide: true,
|
||||
itemCount: 6,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: {
|
||||
...args,
|
||||
quotes: generateQuotes(args.itemCount),
|
||||
},
|
||||
template: `
|
||||
<div style="padding: 2.5rem; background: #f5f5f5;">
|
||||
<ui-carousel
|
||||
[gap]="gap"
|
||||
[arrowAutoHide]="arrowAutoHide"
|
||||
>
|
||||
@for (quote of quotes; track quote.id) {
|
||||
<quote-card [quote]="quote.text" [author]="quote.author"></quote-card>
|
||||
}
|
||||
</ui-carousel>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<CarouselStoryProps>;
|
||||
|
||||
/**
|
||||
* Default carousel with 6 quote cards.
|
||||
* Demonstrates basic horizontal scrolling with auto-hide arrows.
|
||||
*/
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
itemCount: 6,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Carousel with many items (15 cards).
|
||||
* Shows behavior with extensive scrolling and tests navigation performance.
|
||||
*/
|
||||
export const ManyItems: Story = {
|
||||
args: {
|
||||
itemCount: 15,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Carousel with few items (3 cards).
|
||||
* Demonstrates behavior when content might not overflow.
|
||||
* Arrows should disable if not scrollable.
|
||||
*/
|
||||
export const FewItems: Story = {
|
||||
args: {
|
||||
itemCount: 3,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Carousel with persistent arrows (no auto-hide).
|
||||
* Arrows are always visible when content is scrollable.
|
||||
*/
|
||||
export const AlwaysShowArrows: Story = {
|
||||
args: {
|
||||
itemCount: 8,
|
||||
arrowAutoHide: false,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Carousel with custom gap spacing (2rem).
|
||||
* Demonstrates configurable spacing between items.
|
||||
*/
|
||||
export const CustomGap: Story = {
|
||||
args: {
|
||||
itemCount: 6,
|
||||
gap: '2rem',
|
||||
},
|
||||
};
|
||||
63
apps/isa-app/stories/ui/carousel/quote-card.component.ts
Normal file
63
apps/isa-app/stories/ui/carousel/quote-card.component.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Component, input, ChangeDetectionStrategy } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Quote card component for Storybook carousel examples.
|
||||
* Displays a quote with author in a styled card matching the Figma design.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'quote-card',
|
||||
template: `
|
||||
<div class="quote-card">
|
||||
<div class="quote-card__content">
|
||||
<p class="quote-card__quote isa-text-body-1-regular text-isa-neutral-900">
|
||||
"{{ quote() }}"
|
||||
</p>
|
||||
@if (author()) {
|
||||
<p class="quote-card__author isa-text-body-2-bold text-isa-neutral-700">
|
||||
— {{ author() }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.quote-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
padding: 20px 22px;
|
||||
min-width: 334px;
|
||||
max-width: 334px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.quote-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.quote-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quote-card__quote {
|
||||
font-style: italic;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quote-card__author {
|
||||
margin: 0;
|
||||
font-style: normal;
|
||||
}
|
||||
`],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
})
|
||||
export class QuoteCardComponent {
|
||||
quote = input.required<string>();
|
||||
author = input<string>('');
|
||||
}
|
||||
104
apps/isa-app/stories/ui/switch/ui-icon-switch.stories.ts
Normal file
104
apps/isa-app/stories/ui/switch/ui-icon-switch.stories.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { argsToTemplate, type Meta, type StoryObj } from '@storybook/angular';
|
||||
import { IconSwitchComponent, IconSwitchColor } from '@isa/ui/switch';
|
||||
import { provideIcons } from '@ng-icons/core';
|
||||
import { isaFiliale, IsaIcons, isaNavigationDashboard } from '@isa/icons';
|
||||
|
||||
type IconSwitchComponentInputs = {
|
||||
icon: string;
|
||||
checked: boolean;
|
||||
color: IconSwitchColor;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
const meta: Meta<IconSwitchComponentInputs> = {
|
||||
component: IconSwitchComponent,
|
||||
title: 'ui/switch/IconSwitch',
|
||||
decorators: [
|
||||
(story) => ({
|
||||
...story(),
|
||||
applicationConfig: {
|
||||
providers: [provideIcons(IsaIcons)],
|
||||
},
|
||||
}),
|
||||
],
|
||||
argTypes: {
|
||||
icon: {
|
||||
control: { type: 'select' },
|
||||
options: Object.keys(IsaIcons),
|
||||
description: 'The name of the icon to display in the switch',
|
||||
},
|
||||
checked: {
|
||||
control: 'boolean',
|
||||
description: 'Whether the switch is checked (on) or not (off)',
|
||||
},
|
||||
color: {
|
||||
control: { type: 'select' },
|
||||
options: Object.values(IconSwitchColor),
|
||||
description: 'Determines the switch color theme',
|
||||
},
|
||||
disabled: {
|
||||
control: 'boolean',
|
||||
description: 'Disables the switch when true',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
icon: 'isaFiliale',
|
||||
checked: false,
|
||||
color: 'primary',
|
||||
disabled: false,
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
template: `<ui-icon-switch ${argsToTemplate(args)}></ui-icon-switch>`,
|
||||
}),
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<IconSwitchComponent>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Enabled: Story = {
|
||||
args: {
|
||||
checked: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'The switch in its enabled/checked state with the primary color theme.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
checked: false,
|
||||
disabled: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
'The switch in a disabled state. User interactions are prevented.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const EnabledAndDisabled: Story = {
|
||||
args: {
|
||||
checked: true,
|
||||
disabled: true,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'The switch in both enabled and disabled states simultaneously.',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -11,11 +11,17 @@ const meta: Meta<TooltipDirective> = {
|
||||
control: 'multi-select',
|
||||
options: ['click', 'hover', 'focus'],
|
||||
},
|
||||
variant: {
|
||||
control: { type: 'select' },
|
||||
options: ['default', 'warning'],
|
||||
description: 'Determines the visual variant of the tooltip',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
title: 'Tooltip Title',
|
||||
content: 'This is the tooltip content.',
|
||||
triggerOn: ['click', 'hover', 'focus'],
|
||||
variant: 'default',
|
||||
},
|
||||
render: (args) => ({
|
||||
props: args,
|
||||
@@ -37,3 +43,12 @@ export const Default: Story = {
|
||||
triggerOn: ['hover', 'click'],
|
||||
},
|
||||
};
|
||||
|
||||
export const Warning: Story = {
|
||||
args: {
|
||||
title: 'Warning Tooltip',
|
||||
content: 'This is a warning message.',
|
||||
triggerOn: ['hover', 'click'],
|
||||
variant: 'warning',
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user