Compare commits

..

1 Commits

Author SHA1 Message Date
Andreas Schickinger
1aa9ffec5d #4082 Sidenav Klickbereich erhöht 2023-06-09 10:58:31 +02:00
646 changed files with 3595 additions and 21076 deletions

View File

@@ -11,9 +11,13 @@ export class DevScanAdapter implements ScanAdapter {
constructor(private _modal: UiModalService, private _environmentService: EnvironmentService) {}
async init(): Promise<boolean> {
return new Promise((resolve, reject) => {
resolve(isDevMode());
});
if (this._environmentService.isTablet()) {
return new Promise((resolve, reject) => {
resolve(isDevMode());
});
}
return false;
}
scan(): Observable<string> {

View File

@@ -32,32 +32,22 @@ export class ScanAdapterService {
return Object.values(this._readyAdapters).some((ready) => ready);
}
scan(ops: { use?: string; include?: string[]; exclude?: string[] } = {}): Observable<string> {
scan(ops: { use?: string; include?: string[]; exclude?: string[] } = { exclude: ['Dev'] }): Observable<string> {
let adapter: ScanAdapter;
if (ops.use == undefined) {
const adapterOrder = ['Native', 'Scandit', 'Dev'];
for (const name of adapterOrder) {
adapter = this.getAdapter(name);
if (adapter) {
break;
}
}
// get the first adapter that is ready to use
// adapter = this.scanAdapters
// .filter((adapter) => {
// if (ops.include?.length) {
// return ops.include.includes(adapter.name);
// } else if (ops.exclude?.length) {
// return !ops.exclude.includes(adapter.name);
// } else {
// return true;
// }
// })
// .find((adapter) => this._readyAdapters[adapter.name]);
adapter = this.scanAdapters
.filter((adapter) => {
if (ops.include?.length) {
return ops.include.includes(adapter.name);
} else if (ops.exclude?.length) {
return !ops.exclude.includes(adapter.name);
} else {
return true;
}
})
.find((adapter) => this._readyAdapters[adapter.name]);
} else {
adapter = this.getAdapter(ops.use);
}

View File

@@ -2,8 +2,8 @@ import { NgModule } from '@angular/core';
import { ProductImagePipe } from './product-image.pipe';
@NgModule({
declarations: [],
imports: [ProductImagePipe],
declarations: [ProductImagePipe],
imports: [],
exports: [ProductImagePipe],
})
export class ProductImageModule {}

View File

@@ -3,8 +3,6 @@ import { ProductImageService } from './product-image.service';
@Pipe({
name: 'productImage',
standalone: true,
pure: true,
})
export class ProductImagePipe implements PipeTransform {
constructor(private imageService: ProductImageService) {}

View File

@@ -138,6 +138,6 @@ export class BreadcrumbService {
getLatestBreadcrumbForSection(section: 'customer' | 'branch', predicate: (crumb: Breadcrumb) => boolean = (_) => true) {
return this.store
.select(selectors.selectBreadcrumbsBySection, { section })
.pipe(map((crumbs) => crumbs.sort((a, b) => b.timestamp - a.timestamp).find((f) => predicate(f))));
.pipe(map((crumbs) => crumbs.sort((a, b) => b.changed - a.changed).find((f) => predicate(f))));
}
}

View File

@@ -1,6 +1,4 @@
import { CommandService } from './command.service';
export abstract class ActionHandler<T = any> {
constructor(readonly action: string) {}
abstract handler(data: T, service?: CommandService): Promise<T>;
abstract handler(data: T): Promise<T>;
}

View File

@@ -1,12 +1,8 @@
import { ModuleWithProviders, NgModule, Provider, Type } from '@angular/core';
import { ModuleWithProviders, NgModule, Type } from '@angular/core';
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[] {
return [CommandService, actionHandlers.map((handler) => ({ provide: FEATURE_ACTION_HANDLERS, useClass: handler, multi: true }))];
}
@NgModule({})
export class CoreCommandModule {
static forRoot(actionHandlers: Type<ActionHandler>[]): ModuleWithProviders<CoreCommandModule> {

View File

@@ -16,7 +16,7 @@ export class CommandService {
throw new Error('Action Handler does not exist');
}
data = await handler.handler(data, this);
data = await handler.handler(data);
}
return data;
}

View File

@@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { Platform } from '@angular/cdk/platform';
import { NativeContainerService } from 'native-container';
import { BreakpointObserver } from '@angular/cdk/layout';
import { shareReplay } from 'rxjs/operators';
const MATCH_TABLET = '(max-width: 1024px)';
@@ -24,19 +23,19 @@ export class EnvironmentService {
return this._breakpointObserver.isMatched(MATCH_TABLET);
}
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET).pipe(shareReplay());
matchTablet$ = this._breakpointObserver.observe(MATCH_TABLET);
matchDesktopSmall(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP_SMALL);
}
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL).pipe(shareReplay());
matchDesktopSmall$ = this._breakpointObserver.observe(MATCH_DESKTOP_SMALL);
matchDesktop(): boolean {
return this._breakpointObserver.isMatched(MATCH_DESKTOP);
}
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP).pipe(shareReplay());
matchDesktop$ = this._breakpointObserver.observe(MATCH_DESKTOP);
/**
* @deprecated Use `matchDesktopSmall` or 'matchDesktop' instead.

View File

@@ -145,7 +145,6 @@ export class DomainAvailabilityService {
);
}
@memorize({ ttl: 10000 })
getTakeAwayAvailability({
item,
quantity,
@@ -155,7 +154,6 @@ export class DomainAvailabilityService {
quantity: number;
branch?: BranchDTO;
}): Observable<AvailabilityDTO> {
console.log('getTakeAwayAvailability', item, quantity, branch);
const request = !!branch ? this.getStockByBranch(branch.id) : this.getDefaultStock();
return request.pipe(
switchMap((s) =>
@@ -542,7 +540,6 @@ export class DomainAvailabilityService {
return preferred.map((p) => {
return [
{
orderDeadline: p?.orderDeadline,
availabilityType: p?.status,
ssc: p?.ssc,
sscText: p?.sscText,
@@ -564,6 +561,7 @@ export class DomainAvailabilityService {
private _mapToShippingAvailability(availabilities: SwaggerAvailabilityDTO[]) {
const preferred = availabilities.filter((f) => f.preferred === 1);
return preferred.map((p) => {
return {
availabilityType: p?.status,
@@ -577,7 +575,6 @@ export class DomainAvailabilityService {
supplierInfo: p?.requestStatusCode,
lastRequest: p?.requested,
itemId: p.itemId,
priceMaintained: p.priceMaintained,
};
});
}

View File

@@ -28,13 +28,7 @@ import {
StoreCheckoutBranchService,
ItemsResult,
} from '@swagger/checkout';
import {
DisplayOrderDTO,
DisplayOrderItemDTO,
OrderCheckoutService,
ReorderValues,
ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString,
} from '@swagger/oms';
import { DisplayOrderDTO, DisplayOrderItemDTO, OrderCheckoutService, ReorderValues } from '@swagger/oms';
import { isNullOrUndefined, memorize } from '@utils/common';
import { combineLatest, Observable, of, concat, isObservable, throwError } from 'rxjs';
import { bufferCount, catchError, filter, first, map, mergeMap, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
@@ -378,9 +372,8 @@ export class DomainCheckoutService {
_setBuyer({ processId, buyer }: { processId: number; buyer: BuyerDTO }): Observable<CheckoutDTO> {
return this.getCheckout({ processId }).pipe(
first(),
mergeMap((checkout) => {
console.log('checkout', checkout, processId);
return this._buyerService
mergeMap((checkout) =>
this._buyerService
.StoreCheckoutBuyerSetBuyerPOST({
checkoutId: checkout?.id,
buyerDTO: buyer,
@@ -388,8 +381,8 @@ export class DomainCheckoutService {
.pipe(
map((response) => response.result),
tap((checkout) => this.store.dispatch(DomainCheckoutActions.setCheckout({ processId, checkout })))
);
})
)
)
);
}
@@ -717,47 +710,6 @@ export class DomainCheckoutService {
.pipe(mergeMap((_) => completeOrder$.pipe(tap(console.log.bind(window, 'completeOrder$')))));
}
completeKulturpassOrder({
processId,
orderItemSubsetId,
}: {
processId: number;
orderItemSubsetId: number;
}): Observable<ResponseArgsOfValueTupleOfIEnumerableOfDisplayOrderDTOAndIEnumerableOfKeyValueDTOOfStringAndString> {
const refreshShoppingCart$ = this.getShoppingCart({ processId, latest: true }).pipe(first());
const refreshCheckout$ = this.getCheckout({ processId, refresh: true }).pipe(first());
const setBuyer$ = this.getBuyer({ processId }).pipe(
first(),
mergeMap((buyer) => this._setBuyer({ processId, buyer }))
);
const setPayer$ = this.getPayer({ processId }).pipe(
first(),
mergeMap((payer) => this._setPayer({ processId, payer }))
);
const checkAvailabilities$ = this.checkAvailabilities({ processId });
const updateAvailabilities$ = this.updateAvailabilities({ processId });
return refreshShoppingCart$.pipe(
mergeMap((_) => refreshCheckout$),
mergeMap((_) => checkAvailabilities$),
mergeMap((_) => updateAvailabilities$),
mergeMap((_) => setBuyer$),
mergeMap((_) => setPayer$),
mergeMap((checkout) =>
this.orderCheckoutService.OrderCheckoutCreateKulturPassOrder({
payload: {
checkoutId: checkout.id,
orderItemSubsetId: String(orderItemSubsetId),
},
})
)
);
}
updateDestination({
processId,
destinationId,
@@ -978,5 +930,6 @@ export class DomainCheckoutService {
private updateProcessCount(processId: number, count: number) {
this.applicationService.patchProcessData(processId, { count });
}
//#endregion
}

View File

@@ -18,15 +18,14 @@ import {
NotificationChannel,
PayerDTO,
PayerService,
QueryTokenDTO,
ResponseArgsOfIEnumerableOfBonusCardInfoDTO,
ShippingAddressDTO,
ShippingAddressService,
} from '@swagger/crm';
import { isArray, memorize } from '@utils/common';
import { isArray } from '@utils/common';
import { PagedResult, Result } from 'apps/domain/defs/src/public-api';
import { Observable, of, ReplaySubject } from 'rxjs';
import { catchError, map, mergeMap, retry, shareReplay } from 'rxjs/operators';
import { catchError, map, mergeMap, retry } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CrmCustomerService {
@@ -39,14 +38,6 @@ export class CrmCustomerService {
private loyaltyCardService: LoyaltyCardService
) {}
@memorize()
filterSettings() {
return this.customerService.CustomerCustomerQuerySettings().pipe(
map((res) => res.result),
shareReplay(1)
);
}
complete(queryString: string, filter?: { [key: string]: string }): Observable<Result<AutocompleteDTO[]>> {
return this.customerService.CustomerCustomerAutocomplete({
input: queryString,
@@ -75,15 +66,6 @@ export class CrmCustomerService {
});
}
getCustomersWithQueryToken(queryToken: QueryTokenDTO) {
if (queryToken.skip === undefined) queryToken.skip = 0;
if (queryToken.take === undefined) queryToken.take = 20;
if (queryToken.input === undefined) queryToken.input = { qs: '' };
if (queryToken.filter === undefined) queryToken.filter = {};
return this.customerService.CustomerListCustomers(queryToken);
}
getCustomersByCustomerCardNumber(queryString: string): Observable<PagedResult<CustomerInfoDTO>> {
return this.customerService.CustomerGetCustomerByBonuscard(!!queryString ? queryString : undefined);
}

View File

@@ -21,7 +21,6 @@ export class CollectOnDeliveryNoteActionHandler extends ActionHandler<OrderItems
const response = await this.orderService
.OrderCollectOnDeliveryNote({
data,
eagerLoading: 1,
})
.toPromise();
@@ -30,7 +29,7 @@ export class CollectOnDeliveryNoteActionHandler extends ActionHandler<OrderItems
return {
...context,
receipts: response.result.map((r) => r.data),
receipts: response.result,
};
}
}

View File

@@ -1,35 +0,0 @@
import { Injectable } from '@angular/core';
import { ActionHandler } from '@core/command';
import { OrderService } from '@swagger/oms';
import { OrderItemsContext } from './order-items.context';
@Injectable()
export class CollectWithSmallAmountinvoiceActionHandler extends ActionHandler<OrderItemsContext> {
constructor(private orderService: OrderService) {
super('COLLECT_WITH_SMALLAMOUNTINVOICE');
}
async handler(context: OrderItemsContext): Promise<OrderItemsContext> {
const data: Record<number, number> = {};
context.items.forEach((orderItemSubsetId) => {
data[orderItemSubsetId.orderItemSubsetId] =
context.itemQuantity?.get(orderItemSubsetId.orderItemSubsetId) ?? orderItemSubsetId.quantity;
});
const response = await this.orderService
.OrderCollectWithSmallAmountInvoice({
data,
eagerLoading: 1,
})
.toPromise();
// Für korrekte Navigation nach Aufruf, da ProcessingStatus Serverseitig auf abgeholt gesetzt wird
context.items?.forEach((i) => (i.processingStatus = 256));
return {
...context,
receipts: response.result.map((r) => r.data),
};
}
}

View File

@@ -1,3 +1,4 @@
// start:ng42.barrel
export * from './accepted.action-handler';
export * from './arrived.action-handler';
export * from './assembled.action-handler';
@@ -6,10 +7,6 @@ export * from './back-to-stock.action-handler';
export * from './canceled-by-buyer.action-handler';
export * from './canceled-by-retailer.action-handler';
export * from './canceled-by-supplier.action-handler';
export * from './change-order-item-status-base.action-handler';
export * from './collect-on-deliverynote.action-handler';
export * from './collect-with-smallamountinvoice.action-handler';
export * from './create-returnitem.action-handler';
export * from './create-shipping-note.action-handler';
export * from './delivered.action-handler';
export * from './determine-supplier.action-handler';
@@ -28,15 +25,16 @@ export * from './parked.action-handler';
export * from './placed.action-handler';
export * from './preperation-for-shipping.action-handler';
export * from './print-compartment-label.action-handler';
export * from './print-pricediffqrcodelabel.action-handler';
export * from './print-shipping-note.action-handler';
export * from './print-smallamountinvoice.action-handler';
export * from './re-order.action-handler';
export * from './re-ordered.action-handler';
export * from './re-order.action-handler';
export * from './redirected-internally.action-handler';
export * from './requested.action-handler';
export * from './reserved.action-handler';
export * from './returned-by-buyer.action-handler';
export * from './shipping-note.action-handler';
export * from './shop-with-kulturpass.action-handler';
export * from './supplier-temporarily-out-of-stock.action-handler copy';
export * from './collect-on-deliverynote.action-handler';
export * from './create-returnitem.action-handler';
export * from './print-pricediffqrcodelabel.action-handler';
// end:ng42.barrel

View File

@@ -1,4 +1,4 @@
import { OrderItemListItemDTO, ReceiptDTO, OrderDTO } from '@swagger/oms';
import { OrderItemListItemDTO, ReceiptDTO } from '@swagger/oms';
export interface OrderItemsContext {
items: OrderItemListItemDTO[];
@@ -12,6 +12,4 @@ export interface OrderItemsContext {
receipts?: ReceiptDTO[];
shippingDelayComment?: string;
order?: OrderDTO;
}

View File

@@ -27,8 +27,7 @@ export class PrintShippingNoteActionHandler extends ActionHandler<OrderItemsCont
printerType: 'Label',
print: async (printer) => {
try {
const receipts = data?.receipts?.filter((r) => r?.receiptType & 1);
for (const group of groupBy(receipts, (receipt) => receipt?.buyer?.buyerNumber)) {
for (const group of groupBy(data?.receipts, (receipt) => receipt?.buyer?.buyerNumber)) {
await this.domainPrinterService.printShippingNote({ printer, receipts: group?.items?.map((r) => r?.id) }).toPromise();
}
return {

View File

@@ -1,56 +0,0 @@
import { Injectable } from '@angular/core';
import { ActionHandler } from '@core/command';
import { OrderItemsContext } from './order-items.context';
import { OMSPrintService } from '@swagger/print';
import { UiModalService } from '@ui/modal';
import { PrintModalComponent, PrintModalData } from '@modal/printer';
import { NativeContainerService } from 'native-container';
import { groupBy } from '@ui/common';
@Injectable()
export class PrintSmallamountinvoiceActionHandler extends ActionHandler<OrderItemsContext> {
constructor(
private uiModal: UiModalService,
private omsPrintService: OMSPrintService,
private nativeContainerService: NativeContainerService
) {
super('PRINT_SMALLAMOUNTINVOICE');
}
async handler(data: OrderItemsContext): Promise<OrderItemsContext> {
await this.uiModal
.open({
content: PrintModalComponent,
config: { showScrollbarY: false },
data: {
printImmediately: !this.nativeContainerService.isNative,
printerType: 'Label',
print: async (printer) => {
try {
const receipts = data?.receipts?.filter((r) => r?.receiptType & 128);
for (const group of groupBy(receipts, (receipt) => receipt?.buyer?.buyerNumber)) {
await this.omsPrintService
.OMSPrintKleinbetragsrechnung({
data: group?.items?.map((r) => r?.id),
printer,
})
.toPromise();
}
return {
error: false,
};
} catch (error) {
console.error(error);
return {
error: true,
message: error?.message || error,
};
}
},
} as PrintModalData,
})
.afterClosed$.toPromise();
return data;
}
}

View File

@@ -1,82 +0,0 @@
import { Injectable } from '@angular/core';
import { OrderItemsContext } from './order-items.context';
import { ActionHandler, CommandService } from '@core/command';
import { KulturpassOrderModalService } from '@shared/modals/kulturpass-order-modal';
import { DisplayOrderItemSubsetDTO, OrderItemListItemDTO, ReceiptDTO } from '@swagger/oms';
import { DomainReceiptService } from '../receipt.service';
import { DomainGoodsService } from '../goods.service';
import { map } from 'rxjs/operators';
@Injectable()
export class ShopWithKulturpassActionHandler extends ActionHandler<OrderItemsContext> {
constructor(
private _modal: KulturpassOrderModalService,
private _receiptService: DomainReceiptService,
private _goodsService: DomainGoodsService
) {
super('SHOP_WITH_KULTURPASS');
}
async handler(data: OrderItemsContext, service: CommandService): Promise<OrderItemsContext> {
const items: OrderItemListItemDTO[] = [];
const receipts: ReceiptDTO[] = [];
let command: string;
for (const item of data.items) {
const result = await this._modal.open({ orderItemListItem: item, order: data.order }).afterClosed$.toPromise();
if (result.data == null) {
return data;
}
const displayOrder = result.data[0];
command = result.data[1];
if (displayOrder) {
const subsetItems = displayOrder.items.reduce((acc, item) => [...acc, ...item.subsetItems], [] as DisplayOrderItemSubsetDTO[]);
const orderItems = await this.getItems(displayOrder.orderNumber);
items.push(...orderItems);
const subsetItemIds = subsetItems.map((item) => item.id);
const r = await this.getReceipts(subsetItemIds);
receipts.push(...r);
}
}
if (!command) {
return {
...data,
items,
receipts,
};
} else {
return service.handleCommand(command, {
...data,
items,
receipts,
});
}
}
getReceipts(ids: number[]) {
return this._receiptService
.getReceipts({
receiptType: 128,
eagerLoading: 1,
ids,
})
.pipe(map((res) => res.result.map((data) => data.item3.data).filter((data) => !!data)))
.toPromise();
}
getItems(orderNumber: string) {
return this._goodsService
.getWarenausgabeItemByOrderNumber(orderNumber, false)
.pipe(map((res) => res.result))
.toPromise();
}
}

View File

@@ -1,7 +1,5 @@
import { Injectable } from '@angular/core';
import { AutocompleteTokenDTO, OrderService, QueryTokenDTO } from '@swagger/oms';
import { memorize } from '@utils/common';
import { map, shareReplay } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class DomainCustomerOrderService {
@@ -25,11 +23,7 @@ export class DomainCustomerOrderService {
});
}
@memorize()
settings() {
return this._orderService.OrderKundenbestellungenSettings().pipe(
map((res) => res?.result),
shareReplay()
);
return this._orderService.OrderKundenbestellungenSettings();
}
}

View File

@@ -1,11 +0,0 @@
import { PackageArrivalStatusDTO } from '@swagger/wws';
export abstract class PackageInspectionEvent {
constructor(public readonly type: string) {}
}
export class PackageStatusChangedEvent extends PackageInspectionEvent {
constructor(public readonly packageId: string, public readonly status: PackageArrivalStatusDTO) {
super('PackageStatusChangedEvent');
}
}

View File

@@ -1,8 +1,10 @@
import { Injectable } from '@angular/core';
import {
ListResponseArgsOfPackageDTO,
ListResponseArgsOfPackageDTO2,
PackageArrivalStatusDTO,
PackageDetailResponseDTO,
PackageDTO,
PackageDTO2,
QuerySettingsDTO,
QueryTokenDTO,
@@ -11,18 +13,13 @@ import {
ResponseArgsOfQuerySettingsDTO,
WareneingangService,
} from '@swagger/wws';
import { Observable, Subject } from 'rxjs';
import { map, tap } from 'rxjs/operators';
import { PackageInspectionEvent, PackageStatusChangedEvent } from './events';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class DomainPackageInspectionService {
private _events = new Subject<PackageInspectionEvent>();
events = this._events.asObservable();
constructor(private _wareneingang: WareneingangService) {}
getQuerySettingsResponse(): Observable<ResponseArgsOfQuerySettingsDTO> {
@@ -50,26 +47,23 @@ export class DomainPackageInspectionService {
}
changePackageStatusResponse(pkg: PackageDTO2, modifier: string): Observable<ResponseArgsOfPackageArrivalStatusDTO> {
return this._wareneingang
.WareneingangChangePackageStatus({
packageId: pkg.id,
payload: {
id: pkg.id,
annotation: pkg.annotation,
area: pkg.area,
arrivalChecked: pkg.arrivalChecked,
arrivalStatus: pkg.arrivalStatus,
deliveryNoteNumber: pkg.deliveryNoteNumber,
deliveryTarget: pkg.deliveryTarget,
estimatedDeliveryDate: pkg.estimatedDeliveryDate,
packageNumber: pkg.packageNumber,
supplier: pkg.supplier,
trackingNumber: pkg.trackingNumber,
scanId: pkg.scanId,
},
modifier,
})
.pipe(tap((res) => this._events.next(new PackageStatusChangedEvent(pkg.id, res.result))));
return this._wareneingang.WareneingangChangePackageStatus({
packageId: pkg.id,
payload: {
id: pkg.id,
annotation: pkg.annotation,
area: pkg.area,
arrivalChecked: pkg.arrivalChecked,
arrivalStatus: pkg.arrivalStatus,
deliveryNoteNumber: pkg.deliveryNoteNumber,
deliveryTarget: pkg.deliveryTarget,
estimatedDeliveryDate: pkg.estimatedDeliveryDate,
packageNumber: pkg.packageNumber,
supplier: pkg.supplier,
trackingNumber: pkg.trackingNumber,
},
modifier,
});
}
changePackageStatus(pkg: PackageDTO2, modifier: string): Observable<PackageArrivalStatusDTO> {

View File

@@ -1,6 +1,6 @@
/*
* Public API Surface of package-inspection
*/
export * from './lib/events';
export * from './lib/package-inspection.service';
export * from './lib/package-inspection.module';

View File

@@ -4,8 +4,6 @@ import {
CanActivateCartGuard,
CanActivateCartWithProcessIdGuard,
CanActivateCustomerGuard,
CanActivateCustomerOrdersGuard,
CanActivateCustomerOrdersWithProcessIdGuard,
CanActivateCustomerWithProcessIdGuard,
CanActivateGoodsInGuard,
CanActivateGoodsOutGuard,
@@ -61,22 +59,22 @@ const routes: Routes = [
{
path: 'order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersGuard],
canActivate: [CanActivateGoodsOutGuard],
},
{
path: ':processId/order',
loadChildren: () => import('@page/customer-order').then((m) => m.CustomerOrderModule),
canActivate: [CanActivateCustomerOrdersWithProcessIdGuard],
canActivate: [CanActivateGoodsOutWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},
{
path: 'customer',
loadChildren: () => import('@page/customer-rd').then((m) => m.CustomerModule),
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
canActivate: [CanActivateCustomerGuard],
},
{
path: ':processId/customer',
loadChildren: () => import('@page/customer-rd').then((m) => m.CustomerModule),
loadChildren: () => import('@page/customer').then((m) => m.PageCustomerModule),
canActivate: [CanActivateCustomerWithProcessIdGuard],
resolve: { processId: ProcessIdResolver },
},

View File

@@ -32,11 +32,11 @@ import { IsaErrorHandler } from './providers/isa.error-handler';
import { ScanAdapterModule, ScanAdapterService, ScanditScanAdapterModule } from '@adapter/scan';
import { RootStateService } from './store/root-state.service';
import * as Commands from './commands';
import { UiIconModule, UI_ICON_CFG } from '@ui/icon';
import { PreviewComponent } from './preview';
import { NativeContainerService } from 'native-container';
import { ShellModule } from '@shared/shell';
import { MainComponent } from './main.component';
import { IconModule } from '@shared/components/icon';
registerLocaleData(localeDe, localeDeExtra);
registerLocaleData(localeDe, 'de', localeDeExtra);
@@ -106,7 +106,7 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
ScanAdapterModule.forRoot(),
ScanditScanAdapterModule.forRoot(),
PlatformModule,
IconModule.forRoot(),
UiIconModule.forRoot(),
],
providers: [
{
@@ -135,6 +135,11 @@ export function _notificationsHubOptionsFactory(config: Config, auth: AuthServic
useClass: IsaErrorHandler,
},
{ provide: LOCALE_ID, useValue: 'de-DE' },
{
provide: UI_ICON_CFG,
useFactory: (config: Config) => config.get('@ui/icon'),
deps: [Config],
},
],
bootstrap: [AppComponent],
})

View File

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

View File

@@ -1,51 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCustomerOrdersWithProcessIdGuard implements CanActivate {
constructor(private readonly _applicationService: ApplicationService, private readonly _breadcrumbService: BreadcrumbService) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const process = await this._applicationService
.getProcessById$(+route.params.processId)
.pipe(first())
.toPromise();
if (!process) {
await this._applicationService.createProcess({
id: +route.params.processId,
type: 'customer-order',
section: 'customer',
name: `Kundenbestellungen`,
});
}
await this.removeBreadcrumbWithSameProcessId(route);
this._applicationService.activateProcess(+route.params.processId);
return true;
}
// Fix #3292: Alle Breadcrumbs die nichts mit dem aktuellen Prozess zu tun haben, müssen removed werden
async removeBreadcrumbWithSameProcessId(route: ActivatedRouteSnapshot) {
const crumbs = await this._breadcrumbService
.getBreadcrumbByKey$(+route.params.processId)
.pipe(first())
.toPromise();
// Entferne alle Crumbs die nichts mit den Kundenbestellungen zu tun haben
if (crumbs.length > 1) {
const crumbsToRemove = crumbs.filter((crumb) => crumb.tags.find((tag) => tag === 'customer-order') === undefined);
for (const crumb of crumbsToRemove) {
await this._breadcrumbService.removeBreadcrumb(crumb.id);
}
}
}
processNumber(processes: ApplicationProcess[]) {
const processNumbers = processes?.map((process) => Number(process?.name?.replace(/\D/g, '')));
return !!processNumbers && processNumbers?.length > 0 ? Math.max(...processNumbers) + 1 : 1;
}
}

View File

@@ -1,97 +0,0 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { CustomerOrdersNavigationService } from '@shared/services';
import { first } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class CanActivateCustomerOrdersGuard implements CanActivate {
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _navigationService: CustomerOrdersNavigationService
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
const processes = await this._applicationService.getProcesses$('customer').pipe(first()).toPromise();
let 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 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, route, lastActivatedCartCheckoutProcessId);
return false;
}
if (!lastActivatedProcessId) {
await this.fromGoodsOutProcess(processes, route);
return false;
} else {
await this._navigationService.navigateToCustomerOrdersSearch({ processId: lastActivatedProcessId });
}
return false;
}
// Bei offenen Kundenbestellungen und Klick auf Kundenbestellungen
async fromGoodsOutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot) {
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._navigationService.navigateToCustomerOrdersSearch({ processId: newProcessId });
}
// Bei offener Bestellbestätigung und Klick auf Kundenbestellungen
async fromCartCheckoutProcess(processes: ApplicationProcess[], route: ActivatedRouteSnapshot, processId: number) {
// Um alle Checkout Daten zu resetten die mit dem Prozess assoziiert sind
this._checkoutService.removeProcess({ processId });
// Ändere type cart-checkout zu customer-order
this._applicationService.patchProcess(processId, {
id: processId,
type: 'cart',
section: 'customer',
name: `Vorgang ${this.processNumber(processes.filter((process) => process.type === 'cart'))}`,
data: {},
});
// Navigation
await this._navigationService.navigateToCustomerOrdersSearch({ processId });
}
getUrlFromSnapshot(route: ActivatedRouteSnapshot, url: string[] = []): string[] {
url.push(...route.url.map((segment) => segment.path));
if (route.firstChild) {
return this.getUrlFromSnapshot(route.firstChild, url);
}
return url.filter((segment) => !!segment);
}
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;
}
}

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from '@angular/router';
import { ApplicationProcess, ApplicationService } from '@core/application';
import { DomainCheckoutService } from '@domain/checkout';
import { ProductCatalogNavigationService } from '@shared/services';
@@ -10,7 +10,8 @@ export class CanActivateProductGuard implements CanActivate {
constructor(
private readonly _applicationService: ApplicationService,
private readonly _checkoutService: DomainCheckoutService,
private readonly _navigationService: ProductCatalogNavigationService
private readonly _navigationService: ProductCatalogNavigationService,
private readonly _router: Router
) {}
async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
@@ -42,7 +43,7 @@ export class CanActivateProductGuard implements CanActivate {
await this.fromCartProcess(processes);
return false;
} else {
await this._navigationService.navigateToProductSearch({ processId: lastActivatedProcessId });
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(lastActivatedProcessId)]));
}
return false;
@@ -82,7 +83,7 @@ export class CanActivateProductGuard implements CanActivate {
});
// Navigation
await this._navigationService.navigateToProductSearch({ processId });
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
}
// Bei offener Bestellbestätigung und Klick auf Footer Artikelsuche
@@ -100,7 +101,7 @@ export class CanActivateProductGuard implements CanActivate {
});
// Navigation
await this._navigationService.navigateToProductSearch({ processId });
await this._router.navigate(this.getUrlFromSnapshot(route, ['/kunde', String(processId)]));
}
getUrlFromSnapshot(route: ActivatedRouteSnapshot, url: string[] = []): string[] {

View File

@@ -5,8 +5,6 @@ export * from './can-activate-customer.guard';
export * from './can-activate-goods-in.guard';
export * from './can-activate-goods-out-with-process-id.guard';
export * from './can-activate-goods-out.guard';
export * from './can-activate-customer-orders.guard';
export * from './can-activate-customer-orders-with-process-id.guard';
export * from './can-activate-product-with-process-id.guard';
export * from './can-activate-product.guard';
export * from './can-activate-remission.guard';

View File

@@ -52,7 +52,11 @@ export class IsAuthenticatedGuard implements CanActivate {
return undefined;
}
const result = await this._scanService.scan()?.toPromise();
const result = await this._scanService
.scan({
exclude: ['Dev'],
})
?.toPromise();
if (typeof result === 'string') {
try {

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -68,7 +68,6 @@
},
"checkForUpdates": 3600000,
"licence": {
"scandit": "AZ7zLw2eLmFWHbYP4RDq8VAEgAxmNGYcPU8YpOc3DryEXj4zMzYQFrQuUm0YewGQYEESXjpRwGX1NYmKY3pXHnAn2DeqIzh2an+FUu9socQlbQnJiHJHoWBAqcqWSua+P12tc95P3s9aaEEYvSjUy7Md88f7N+sk6zZbUmqbMXeXqmZwdkmRoUY/2w0CiiiA4gBFHgu4sMeNQ9dWyfxKTUPf5AnsxnuYpCt5KLxJWSYDv8HHj0mx8DCJTe1m2ony97Lge3JbJ5Dd+Zz6SCwqik7fv53Qole9s/3m66lYFWKAzWRKkHN1zts78CmPxPb+AAHVoqlBM3duvYmnCxxGOmlXabKUNuDR2ExaMu/nlo532jqqy25Cet/FP1UAs96ZGRgzEcHxGPp6kA53lJ15zd+cxz6G93E83AmYJkhddXBQElWEaGtQRfrEzRGmvcksR+V8MMYjGmhkVbQxGGqpnfP4IxbuEFcef6bxxTiulzo75gXoqZTt+7C1qpDcrMM3Yp0Z8RBw3JlV2tLk4FYFZpxY8QrXIcjvRYKExtQ9e5sSbST4Vx95YhEUd6iX0SBPDzcmgR4/Ef6gvJfoWgz68+rqhBGckphdHi2Mf/pYuAlh2jbwtrkErE2xWARBejR/UcU/A3F7k9RkFd5/QZC7qhsE6bZH7uhpkptIbi5XkXagwYy1oJD7yJs4VLOJteYWferRm8h1auxXew5tL8VLHciF+lLj6h8PTUDt2blLgUjHtualqlCwdSTzJyYwk4oswGGDk6E48X7LXpzuhtR8TYTOi2REN0uuTbO/slFBRw+CaYUnD0LjB9p2lb8ndcdV9adzBKmwPxiOtlOELQ=="
},
"@shared/icon": "/assets/icons.json"
"scandit": "AQZyKCc+BEkNL00Y3h3FjawGLF+INUj7cVb0My91hl8ffiW873T8FTV1k4TIZJx5RwcJlYxhgsxHVcnM4AJgSwJhbAfxJmP/3XGijLlLp3XUIRjQwFtf7UlZAFZ7Vrt1/WSf7kxxrFQ2SE2AQwLqPg9DL+hHEfd4xT/15n8p2q7qUlCKLsV6jF12Pd7koFNSWNL3ZIkRtd1ma99/321dnwAJHFGXqWg5nprJ7sYtqUqNQ8Er9SlvKbhnw3AipHzKpz0O3oNfUsr6NlZivRBhMhCZLo5WpXo1m9uIU8zLEWMNDJ+wGUctcGxE3eCptP2zLXUgxxjB+0EXOUtT/GWUc/Ip61CMiyUf7Paz026E2eYil2yWgfkTP5CUgDMNGZFuAA1T5PhB9FRW51CjAIvwOKVMCvfixJiVoUsXHnWH2ZnXqtbDR/uEZBE7OKoBlaPL4G3Lvgdqym5EjROAztUXb6wOmVDiGzzqgizyZnIcxFBSKJAownGj9Vh4/Y/Ag1xzGzNtjz3ngSRfMfIIq/q2Q51uiLiv7mBVliPvPWMUTfTjnqnK/OSBlR2ID+COJqnUKpQMedPyOT3IMznmM6gQCmyYO5KE0MkfhFh6+pdNi6oJM2iZsxK1Z1V+GRSOIwrJEoajjDJkh439XjXk8NExFvplrLjK/oL/dsHIZiG6U5GVWW92kGkuXkJCeUz1CET3paxbGqwrd53r5d6gFABbC12CtcP2JeH4YYCpHYyPQacf0prj9Hdq3wDztShC9tH+4UQS/GbaDHKcS1ANIyPuTxHmBFtPuCJ9Uagy5QBEc8eAz2nfsbfaUxYzco6u/zhNsFbqp6zgQIxs5OcqDQ=="
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -69,7 +69,6 @@
},
"checkForUpdates": 3600000,
"licence": {
"scandit": "AZZzfQ+eLFl3Dzf1QSBag1lDibIoOPh4W33erRIRe3SDUMkHDX8eczEjd2TnfRMWoE5lXOBGtESCWICN9EbrmI1S9Lu5APsvvEOD+K54ADwIVawx0HNZRAc8/+9Vf/izcEGOFQFGBQJyR6vzdzFv5HcjznhxI9E3LiF+uVQPtCqsVYzpkMWIrC5VCg2uwNrj9Bw6f8zYi/lZPrDMS5yVKVcajeK7sh9QAq17dR0opjIIuP5t5nDEJ7hnITwtTR5HaM6cX/KhKpTILOgKexvLYqrK6QJWpU85sDwqwn6T7av4V68qL3XrUo60dScop4QsvraQe1HkRsffl6DkAEoX0RNMS5qVWjGerW7lvA/DQd9hsAO3jWFDR9hVDyt2VvmzzFKnHYqTYxC5qG4bCEJ0RJjy6tEP5Q7vL5SxWygVadmjPv+TwDOCS7DxzxIjcO+BXQY7gW6qn0hx9fXzyvO3avrGWqyImMlgEApZq+36ANqtRcPD/stEe4i0N9dSPhYoHPcc/9/9jpts43FozlgfY4wY8Wt5ybB3X0caISMmB/klFIJKKN7num439z3+Xk7ENB/Xvb0XAtnOt/cuxQYsGQ7fb62GOO/7Va5fdE9ZfaIJsS5ToE6oIbV04pLUssJf9cUMsyPFVELYSJmyGPQQFRz0TTxxRvPapIWrfa2x5x3hYUpNTAdY3v0fN9l/1ZqNSBmIBLH/LoXaVJQ2DydGD1/QFZ2Z/S7zTYKg5/cSEpUgiYtbwutNZSjRH29ucSizC524k+Zst95T8G7LJaWCT8SQAcKXqCnjpiEGWzD++h0jXjn6BWjUnIHi0te+27vF/z6UQL00sWco5hUIqF66EiU="
},
"@shared/icon": "/assets/icons.json"
"scandit": "AfHi/mY+RbwJD5nC7SuWn3I14pFUOfSbQ2QG//4aV3zWQjwix30kHqsqraA8ZiipDBql8YlwIyV6VPBMUiAX4s9YHDxHHsWwq2BUB3ImzDEcU1jmMH/5yakGUYpCQ68D0iZ8SG9sS0QBb3iFdCHc1r9DFr1cMTxM7zOvb/AUoIVmieHZXnx9ioUgCvczsLiuX3hwvTW3lhbvJ4uUyqTWK4sWFVwoY4AIWSFrPwwrkV2DksMKT5fMJT3GWgPypvTIGwWvpRfLWwKlc1Z3ckyb84khsnaWD2wr+hdgu/K8YIMmgGszm5KIZ/G05YfDNZtQ4jby+5RZvQwWR8rxM35rJgf73OkMSpuL9jw3T0TTAlvpkGRLzVVuCw9VjlBLqfPNEZ6VsEwFuAla9IYUvFHCsjypg2J6UpxHXrTYmbsSu5Jm8frVfS5znPPTO9D/4rF6ZVv2PxY9PgUgJUvwMa/VMc/nse3RRRf8RGT4rUItfJDFO8pujD76vVEWq/KixQRoMdLgDLyxhsFVftkxqhZhyEfFZzsEy49LSojJ28vpHpBWLeCQBmnZ7JZ4C5yOQiqSQV/assBq2zJN2q+vCDp8qy5j1rED1SX5Ec7JpgpgnU4chLIf5Zn7bP/hNGT3pEYBuXeDXXN8ke1pcc3fc3m0FysDG0o56XVCUqImZ8Ezi8eujZciKDrWbtljhKTj7cnfuJx0sVHF6Bh5i4YfgA/Z+NL+MtH2EVIF67e6hEz6PWYTcoh3ybBaJfxb2FNvGJutNKg04GwMhYq6K2IddBt0fDiBt0SGM0oSBlUP3DKCUmXcf2a6ASbrcqv6Wz1jHt0pY4U8bEpg7qSbW3VDyvdPgyQ="
}
}

View File

@@ -69,7 +69,6 @@
},
"checkForUpdates": 3600000,
"licence": {
"scandit": "AZZzfQ+eLFl3Dzf1QSBag1lDibIoOPh4W33erRIRe3SDUMkHDX8eczEjd2TnfRMWoE5lXOBGtESCWICN9EbrmI1S9Lu5APsvvEOD+K54ADwIVawx0HNZRAc8/+9Vf/izcEGOFQFGBQJyR6vzdzFv5HcjznhxI9E3LiF+uVQPtCqsVYzpkMWIrC5VCg2uwNrj9Bw6f8zYi/lZPrDMS5yVKVcajeK7sh9QAq17dR0opjIIuP5t5nDEJ7hnITwtTR5HaM6cX/KhKpTILOgKexvLYqrK6QJWpU85sDwqwn6T7av4V68qL3XrUo60dScop4QsvraQe1HkRsffl6DkAEoX0RNMS5qVWjGerW7lvA/DQd9hsAO3jWFDR9hVDyt2VvmzzFKnHYqTYxC5qG4bCEJ0RJjy6tEP5Q7vL5SxWygVadmjPv+TwDOCS7DxzxIjcO+BXQY7gW6qn0hx9fXzyvO3avrGWqyImMlgEApZq+36ANqtRcPD/stEe4i0N9dSPhYoHPcc/9/9jpts43FozlgfY4wY8Wt5ybB3X0caISMmB/klFIJKKN7num439z3+Xk7ENB/Xvb0XAtnOt/cuxQYsGQ7fb62GOO/7Va5fdE9ZfaIJsS5ToE6oIbV04pLUssJf9cUMsyPFVELYSJmyGPQQFRz0TTxxRvPapIWrfa2x5x3hYUpNTAdY3v0fN9l/1ZqNSBmIBLH/LoXaVJQ2DydGD1/QFZ2Z/S7zTYKg5/cSEpUgiYtbwutNZSjRH29ucSizC524k+Zst95T8G7LJaWCT8SQAcKXqCnjpiEGWzD++h0jXjn6BWjUnIHi0te+27vF/z6UQL00sWco5hUIqF66EiU="
},
"@shared/icon": "/assets/icons.json"
"scandit": "AfHi/mY+RbwJD5nC7SuWn3I14pFUOfSbQ2QG//4aV3zWQjwix30kHqsqraA8ZiipDBql8YlwIyV6VPBMUiAX4s9YHDxHHsWwq2BUB3ImzDEcU1jmMH/5yakGUYpCQ68D0iZ8SG9sS0QBb3iFdCHc1r9DFr1cMTxM7zOvb/AUoIVmieHZXnx9ioUgCvczsLiuX3hwvTW3lhbvJ4uUyqTWK4sWFVwoY4AIWSFrPwwrkV2DksMKT5fMJT3GWgPypvTIGwWvpRfLWwKlc1Z3ckyb84khsnaWD2wr+hdgu/K8YIMmgGszm5KIZ/G05YfDNZtQ4jby+5RZvQwWR8rxM35rJgf73OkMSpuL9jw3T0TTAlvpkGRLzVVuCw9VjlBLqfPNEZ6VsEwFuAla9IYUvFHCsjypg2J6UpxHXrTYmbsSu5Jm8frVfS5znPPTO9D/4rF6ZVv2PxY9PgUgJUvwMa/VMc/nse3RRRf8RGT4rUItfJDFO8pujD76vVEWq/KixQRoMdLgDLyxhsFVftkxqhZhyEfFZzsEy49LSojJ28vpHpBWLeCQBmnZ7JZ4C5yOQiqSQV/assBq2zJN2q+vCDp8qy5j1rED1SX5Ec7JpgpgnU4chLIf5Zn7bP/hNGT3pEYBuXeDXXN8ke1pcc3fc3m0FysDG0o56XVCUqImZ8Ezi8eujZciKDrWbtljhKTj7cnfuJx0sVHF6Bh5i4YfgA/Z+NL+MtH2EVIF67e6hEz6PWYTcoh3ybBaJfxb2FNvGJutNKg04GwMhYq6K2IddBt0fDiBt0SGM0oSBlUP3DKCUmXcf2a6ASbrcqv6Wz1jHt0pY4U8bEpg7qSbW3VDyvdPgyQ="
}
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
export const environment = {
production: true,
debug: false,
};

View File

@@ -4,7 +4,6 @@
export const environment = {
production: false,
debug: false,
};
/*

View File

@@ -14,28 +14,26 @@ if (environment.production) {
const debugService = new DebugService();
if (environment.debug) {
const consoleLog = console.log;
const consoleLog = console.log;
console.log = (...args) => {
debugService.add({ type: 'log', args });
consoleLog(...args);
};
console.log = (...args) => {
debugService.add({ type: 'log', args });
consoleLog(...args);
};
const consoleWarn = console.warn;
const consoleWarn = console.warn;
console.warn = (...args) => {
debugService.add({ type: 'warn', args });
consoleWarn(...args);
};
console.warn = (...args) => {
debugService.add({ type: 'warn', args });
consoleWarn(...args);
};
const consoleError = console.error;
const consoleError = console.error;
console.error = (...args) => {
debugService.add({ type: 'error', args });
consoleError(...args);
};
}
console.error = (...args) => {
debugService.add({ type: 'error', args });
consoleError(...args);
};
platformBrowserDynamic([{ provide: DebugService, useValue: debugService }])
.bootstrapModule(AppModule)

View File

@@ -67,18 +67,3 @@
@apply block bg-gray-300 h-6;
animation: load 1s ease-in-out infinite;
}
@layer components {
.input-control {
@apply rounded border border-solid border-[#AEB7C1] px-4 py-[1.125rem] outline-none;
}
// .input-control:focus,
// .input-control:not(:placeholder-shown) {
// @apply bg-white;
// }
.input-control.ng-touched.ng-invalid {
@apply border-brand;
}
}

View File

@@ -59,7 +59,8 @@ export class PriceUpdateItemComponent {
itemId: Number(item?.product?.catalogProductNumber),
branchId: defaultBranch?.id,
})
)
),
shareReplay(1)
);
constructor(

View File

@@ -5,7 +5,7 @@
class="absolute right-0 top-0 h-14 rounded px-5 text-lg bg-cadet-blue flex flex-row flex-nowrap items-center justify-center"
type="button"
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</button>
</div>
@@ -16,7 +16,7 @@
<shell-filter-overlay #filterOverlay class="relative">
<div class="relative">
<button type="button" class="absolute top-4 right-4 text-cadet" (click)="closeFilterOverlay()">
<shared-icon [icon]="'close'" [size]="28"></shared-icon>
<ui-svg-icon [icon]="'close'" [size]="28"></ui-svg-icon>
</button>
</div>

View File

@@ -2,13 +2,13 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
import { UiFilterNextModule } from '@ui/filter';
import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner';
import { PriceUpdateListModule } from './price-update-list';
import { PriceUpdateComponent } from './price-update.component';
import { IconComponent } from '@shared/components/icon';
@NgModule({
imports: [CommonModule, PriceUpdateListModule, UiFilterNextModule, SharedFilterOverlayModule, UiSpinnerModule, IconComponent],
imports: [CommonModule, PriceUpdateListModule, UiIconModule, UiFilterNextModule, SharedFilterOverlayModule, UiSpinnerModule],
exports: [PriceUpdateComponent],
declarations: [PriceUpdateComponent],
providers: [],

View File

@@ -1,5 +1,5 @@
<ng-container *ngIf="!showRecommendations">
<div #detailsContainer class="page-article-details__container px-5 relative">
<div class="page-article-details__container px-5 relative">
<ng-container *ngIf="store.item$ | async; let item">
<div class="page-article-details__product-details mb-3">
<div class="page-article-details__product-bookmark justify-self-end">
@@ -183,18 +183,11 @@
></div>
<ng-template #showAvailabilityPickUpIcon>
<div
#uiOverlayTrigger="uiOverlayTrigger"
[uiOverlayTrigger]="orderDeadlineTooltip"
*ngIf="store.isPickUpAvailabilityAvailable$ | async"
class="page-article-details__product-pick-up-availability w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
[class.tooltip-active]="uiOverlayTrigger.opened"
class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3"
>
<shared-icon icon="isa-box-out" [size]="24"></shared-icon>
<ui-icon class="mx-1" icon="box_out" size="18px"></ui-icon>
</div>
<ui-tooltip [warning]="true" yPosition="above" xPosition="after" [yOffset]="-12" #orderDeadlineTooltip [closeable]="true">
<b #orderDeadline>{{ (store.pickUpAvailability$ | async)?.orderDeadline | orderDeadline }}</b>
</ui-tooltip>
</ng-template>
<div
@@ -226,6 +219,7 @@
<span *ngIf="store.isDownload$ | async" class="flex flex-row items-center">
<div class="w-[2.25rem] h-[2.25rem] bg-[#D8DFE5] rounded-[5px_5px_0px_5px] flex items-center justify-center ml-3">
<ui-icon class="mx-1" icon="download" size="18px"></ui-icon>
<span class="font-bold">Download</span>
</div>
</span>
</div>
@@ -240,7 +234,7 @@
</ng-container>
</div>
<div class="page-article-details__shelfinfo text-right" *ngIf="store.isDownload$ | async">
<div class="page-article-details__shelfinfo" *ngIf="store.isDownload$ | async">
<ng-container
*ngIf="
item?.stockInfos && item?.shelfInfos && (item?.stockInfos)[0]?.compartment && (item?.shelfInfos)[0]?.label;
@@ -324,7 +318,11 @@
<hr class="bg-[#E6EFF9] border-t-2 my-3" />
<div #description class="page-article-details__product-description flex flex-col flex-grow" *ngIf="item.texts?.length > 0">
<div
#description
class="page-article-details__product-description flex flex-col flex-grow overflow-hidden overflow-y-scroll"
*ngIf="item.texts?.length > 0"
>
<div class="whitespace-pre-line">
{{ item.texts[0].value }}
</div>
@@ -358,7 +356,7 @@
<ng-container *ngIf="!showRecommendations">
<div
*ngIf="store.item$ | async; let item"
class="page-article-details__actions w-full sticky text-center left-0 -mb-4 bottom-10 z-fixed"
class="page-article-details__actions w-full absolute text-center left-0 bottom-10 z-fixed"
>
<button
*ngIf="!(store.isDownload$ | async)"
@@ -378,7 +376,8 @@
</button>
</div>
</ng-container>
<div class="page-article-details__product-recommendations sticky bottom-0 -mx-5">
<div class="page-article-details__product-recommendations -mx-5">
<button
*ngIf="store.item$ | async; let item"
class="shadow-[#dce2e9_0px_-2px_18px_0px] sticky bottom-4 border-none outline-none left-0 right-0 flex items-center px-5 h-14 min-h-[3.5rem] bg-white w-full"

View File

@@ -3,7 +3,7 @@
}
.page-article-details__container {
@apply h-full w-full overflow-y-scroll overflow-hidden bg-white rounded shadow-card flex flex-col;
@apply h-full w-full bg-white rounded shadow-card flex flex-col;
}
.page-article-details__product-details {
@@ -93,11 +93,7 @@
}
.page-article-details__actions {
button:disabled {
@apply bg-inactive-branch cursor-not-allowed;
&:disabled {
@apply bg-inactive-branch;
}
}
.tooltip-active {
@apply bg-[#596470] text-white;
}

View File

@@ -1,4 +1,4 @@
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ElementRef, ViewChild } from '@angular/core';
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ElementRef } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { DomainPrinterService } from '@domain/printer';
@@ -8,7 +8,7 @@ import { BranchDTO } from '@swagger/checkout';
import { UiModalService } from '@ui/modal';
import { ModalReviewsComponent } from '@modal/reviews';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, filter, first, map, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators';
import { debounceTime, filter, first, map, shareReplay, switchMap, takeUntil, withLatestFrom } from 'rxjs/operators';
import { ArticleDetailsStore } from './article-details.store';
import { ModalImagesComponent } from 'apps/modal/images/src/public-api';
import { ProductImageService } from 'apps/cdn/product-image/src/public-api';
@@ -21,7 +21,7 @@ import { DatePipe } from '@angular/common';
import { PurchaseOptionsModalService } from '@shared/modals/purchase-options-modal';
import { DomainAvailabilityService } from '@domain/availability';
import { EnvironmentService } from '@core/environment';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { ProductCatalogNavigationService } from '@shared/services';
import { DomainCheckoutService } from '@domain/checkout';
@Component({
@@ -133,13 +133,6 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
showMore: boolean = false;
@ViewChild('detailsContainer', { read: ElementRef, static: false })
detailsContainer: ElementRef;
get detailsContainerNative(): HTMLElement {
return this.detailsContainer?.nativeElement;
}
constructor(
public readonly applicationService: ApplicationService,
private activatedRoute: ActivatedRoute,
@@ -150,10 +143,10 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
private breadcrumb: BreadcrumbService,
private _dateAdapter: DateAdapter,
private _datePipe: DatePipe,
public elementRef: ElementRef,
private _purchaseOptionsModalService: PurchaseOptionsModalService,
private _availability: DomainAvailabilityService,
private _navigationService: ProductCatalogNavigationService,
private _checkoutNavigationService: CheckoutNavigationService,
private _environment: EnvironmentService,
private _router: Router,
private _domainCheckoutService: DomainCheckoutService
@@ -182,13 +175,11 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
});
const id$ = this.activatedRoute.params.pipe(
tap((_) => (this.showRecommendations = false)),
map((params) => Number(params?.id) || undefined),
filter((f) => !!f)
);
const ean$ = this.activatedRoute.params.pipe(
tap((_) => (this.showRecommendations = false)),
map((params) => params?.ean || undefined),
filter((f) => !!f)
);
@@ -344,9 +335,6 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
type: 'add',
processId: this.applicationService.activatedProcessId,
items: [item],
pickupBranch: selectedBranch,
inStoreBranch: selectedBranch,
preSelectOption: !!selectedBranch ? { option: 'in-store', showOptionOnly: true } : undefined,
})
.afterClosed$.subscribe(async (result) => {
if (result?.data === 'continue') {
@@ -355,18 +343,20 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
.pipe(first())
.toPromise();
if (customer) {
await this.navigateToShoppingCart();
this.navigateToShoppingCart();
} else {
await this.navigateToCustomerSearch();
this.navigateToCustomerSearch();
}
console.log('continue');
} else if (result?.data === 'continue-shopping') {
this.navigateToResultList();
console.log('continue-shopping');
}
});
}
async navigateToShoppingCart() {
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: this.applicationService.activatedProcessId });
navigateToShoppingCart() {
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/cart/review`]);
}
async navigateToCustomerSearch() {
@@ -388,23 +378,23 @@ export class ArticleDetailsComponent implements OnInit, OnDestroy {
}
async navigateToResultList() {
const processId = this.applicationService.activatedProcessId;
let crumbs = await this.breadcrumb
.getBreadcrumbsByKeyAndTags$(this.applicationService.activatedProcessId, ['catalog'])
.pipe(first())
.toPromise();
crumbs = crumbs.filter((crumb) => !crumb.tags?.includes('details'));
const crumb = crumbs[crumbs.length - 1];
if (!!crumb) {
this._router.navigate(this._navigationService.getArticleSearchResultsPath(processId), { queryParams: crumb.params });
if (crumb) {
this._router.navigate([crumb.path], { queryParams: crumb.params });
} else {
this._navigationService.navigateToProductSearch({ processId });
this._router.navigate([`/kunde/${this.applicationService.activatedProcessId}/product`]);
}
}
scrollTop(div: HTMLDivElement) {
this.detailsContainerNative?.scrollTo({ top: 0, behavior: 'smooth' });
div?.scrollTo({ top: 0, behavior: 'smooth' });
}
loadImage() {

View File

@@ -10,8 +10,6 @@ import { ArticleRecommendationsComponent } from './recommendations/article-recom
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';
@NgModule({
imports: [
@@ -23,9 +21,7 @@ import { IconModule } from '@shared/components/icon';
UiSliderModule,
UiCommonModule,
UiTooltipModule,
IconModule,
PipesModule,
OrderDeadlinePipeModule,
],
exports: [ArticleDetailsComponent, ArticleRecommendationsComponent],
declarations: [ArticleDetailsComponent, ArticleRecommendationsComponent],

View File

@@ -129,11 +129,7 @@ export class ArticleDetailsStore extends ComponentStore<ArticleDetailsState> {
//#region Abholung
readonly fetchingPickUpAvailability$ = this.select((s) => s.fetchingPickUpAvailability);
readonly pickUpAvailability$: Observable<AvailabilityDTO & { orderDeadline?: string }> = combineLatest([
this.itemData$,
this.branch$,
this.isDownload$,
]).pipe(
readonly pickUpAvailability$: Observable<AvailabilityDTO> = combineLatest([this.itemData$, this.branch$, this.isDownload$]).pipe(
tap(() => this.patchState({ fetchingPickUpAvailability: true, fetchingPickUpAvailabilityError: undefined })),
switchMap(([item, branch, isDownload]) =>
!!item && !!branch && !isDownload

View File

@@ -17,8 +17,7 @@
<a
class="article"
*ngFor="let recommendation of store.recommendations$ | async"
[routerLink]="getDetailsPath(recommendation.product.ean)"
[queryParams]="{ main_qs: recommendation.product.ean }"
[routerLink]="['/kunde', applicationService.activatedProcessId, 'product', 'details', 'ean', recommendation.product.ean]"
(click)="close.emit()"
>
<img [src]="recommendation.product?.ean | productImage: 195:315:true" alt="product-image" />

View File

@@ -1,7 +1,6 @@
import { Component, EventEmitter, Output } from '@angular/core';
import { ApplicationService } from '@core/application';
import { ArticleDetailsStore } from '../article-details.store';
import { ProductCatalogNavigationService } from '@shared/services';
@Component({
selector: 'page-article-recommendations',
@@ -12,13 +11,5 @@ export class ArticleRecommendationsComponent {
@Output()
close = new EventEmitter<void>();
constructor(
public readonly store: ArticleDetailsStore,
private readonly _applicationService: ApplicationService,
private readonly _navigationService: ProductCatalogNavigationService
) {}
getDetailsPath(ean?: string) {
return this._navigationService.getArticleDetailsPath({ processId: this._applicationService.activatedProcessId, ean });
}
constructor(public readonly store: ArticleDetailsStore, public readonly applicationService: ApplicationService) {}
}

View File

@@ -7,8 +7,9 @@ import { ArticleSearchService } from './article-search.store';
import { FocusSearchboxEvent } from './focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from './providers';
import { ProductCatalogNavigationService } from '@shared/services';
import { FilterAutocompleteProvider } from 'apps/shared/components/filter/src/lib';
import { isEqual } from 'lodash';
import { EnvironmentService } from '@core/environment';
import { FilterAutocompleteProvider } from '@shared/components/filter';
@Component({
selector: 'page-article-search',

View File

@@ -7,9 +7,10 @@ import { SearchResultsModule } from './search-results/search-results.module';
import { SearchMainModule } from './search-main/search-main.module';
import { SearchFilterModule } from './search-filter/search-filter.module';
import { ArticleSearchService } from './article-search.store';
import { SharedFilterOverlayModule } from '@shared/components/filter-overlay';
@NgModule({
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule],
imports: [CommonModule, RouterModule, UiIconModule, SearchResultsModule, SearchMainModule, SearchFilterModule, SharedFilterOverlayModule],
exports: [ArticleSearchComponent],
declarations: [ArticleSearchComponent],
providers: [ArticleSearchService],

View File

@@ -152,7 +152,6 @@ export class ArticleSearchService extends ComponentStore<ArticleSearchState> {
search = this.effect((options$: Observable<{ clear?: boolean }>) =>
options$.pipe(
tap((options) => {
this.searchStarted.next({ clear: options?.clear });
this.patchState({
searchState: 'fetching',
items: options.clear ? [] : this.items,

View File

@@ -8,7 +8,7 @@
[routerLink]="closeFilterRoute"
queryParamsHandling="preserve"
>
<shared-icon icon="close" [size]="25"></shared-icon>
<ui-icon icon="close" size="15px"></ui-icon>
</a>
</div>

View File

@@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ChangeDetectionStrategy, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { Observable, Subject } from 'rxjs';
@@ -15,6 +15,9 @@ import { Filter, FilterComponent } from 'apps/shared/components/filter/src/lib';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
@Output()
close = new EventEmitter();
_processId$ = this._activatedRoute.parent.data.pipe(map((data) => Number(data.processId)));
fetching$: Observable<boolean>;
@@ -30,10 +33,6 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
return this._environment.matchDesktop();
}
get isTablet() {
return this._environment.matchTablet();
}
get showFilterClose$() {
return this._environment.matchDesktop$.pipe(map((state) => !(state?.matches && this.leftOutlet === 'search')));
}
@@ -73,21 +72,6 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
ngOnInit() {
this.fetching$ = this.articleSearch.fetching$;
this.filter$ = this.articleSearch.filter$.pipe(map((filter) => Filter.create(filter)));
// #4143 To make Splitscreen Search and Filter work combined
this.articleSearch.searchStarted.pipe(takeUntil(this._onDestroy$)).subscribe(async (_) => {
let queryParams = {
...this.articleSearch.filter.getQueryParams(),
...this.cleanupQueryParams(this.uiFilterComponent?.uiFilter?.getQueryParams()),
};
// Always override query if not in tablet mode
if (!!this.articleSearch.filter.getQueryParams()?.main_qs && !this.isTablet) {
queryParams = { ...queryParams, main_qs: this.articleSearch.filter.getQueryParams()?.main_qs };
}
await this.articleSearch.setDefaultFilter(queryParams);
});
}
ngOnDestroy(): void {
@@ -97,6 +81,7 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
applyFilter(value: Filter) {
this.uiFilterComponent?.cancelAutocomplete();
this.articleSearch.setFilter(value);
this.articleSearch.search({ clear: true });
this.articleSearch.searchCompleted
.pipe(takeUntil(this._onDestroy$), withLatestFrom(this._processId$))
@@ -128,18 +113,4 @@ export class ArticleSearchFilterComponent implements OnInit, OnDestroy {
const queryParams = { main_qs: value?.getQueryParams()?.main_qs || '' };
this.articleSearch.setDefaultFilter(queryParams);
}
cleanupQueryParams(params: Record<string, string> = {}) {
const clean = { ...params };
for (const key in clean) {
if (Object.prototype.hasOwnProperty.call(clean, key)) {
if (clean[key] == undefined) {
delete clean[key];
}
}
}
return clean;
}
}

View File

@@ -1,13 +1,13 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { UiIconModule } from '@ui/icon';
import { UiSpinnerModule } from '@ui/spinner';
import { ArticleSearchFilterComponent } from './search-filter.component';
import { FilterModule } from '@shared/components/filter';
import { IconComponent } from '@shared/components/icon';
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
@NgModule({
imports: [CommonModule, RouterModule, FilterModule, UiSpinnerModule, IconComponent],
imports: [CommonModule, RouterModule, FilterNextModule, UiIconModule, UiSpinnerModule],
exports: [ArticleSearchFilterComponent],
declarations: [ArticleSearchFilterComponent],
providers: [],

View File

@@ -1,6 +1,4 @@
<div
class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-[calc(100vh-16.5rem)] desktop-small:h-[calc(100vh-15.1rem)]"
>
<div class="bg-white rounded py-10 px-4 text-center shadow-[0_-2px_24px_0_#dce2e9] h-full">
<h1 class="text-h3 text-[1.625rem] font-bold mb-[0.375rem]">Artikelsuche</h1>
<p class="text-lg mb-10">
Welchen Artikel suchen Sie?
@@ -28,24 +26,24 @@
[routerLink]="openFilterRoute"
queryParamsHandling="preserve"
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
</div>
<div class="flex flex-col items-start ml-12 desktop:ml-8 py-6 bg-white overflow-hidden h-[calc(100%-13.5rem)]">
<div class="flex flex-col items-start ml-12 desktop:ml-8 py-6 bg-white">
<h3 class="text-p3 font-bold mb-3">Deine letzten Suchanfragen</h3>
<ul class="flex flex-col justify-start overflow-hidden overflow-y-scroll items-start m-0 p-0 bg-white w-full">
<ul class="flex flex-col justify-start overflow-hidden items-start m-0 p-0 bg-white w-full">
<li class="list-none pb-3" *ngFor="let recentQuery of history$ | async">
<button
class="flex flex-row items-center outline-none border-none bg-white text-black text-p2 m-0 p-0"
(click)="setQueryHistory(filter, recentQuery.friendlyName)"
>
<shared-icon
<ui-icon
class="flex w-8 h-8 justify-center items-center mr-3 rounded-full text-black bg-[#edeff0]"
icon="magnify"
[size]="20"
></shared-icon>
icon="search"
size="0.875rem"
></ui-icon>
<p class="m-0 p-0 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-[25rem]">{{ recentQuery.friendlyName }}</p>
</button>
</li>

View File

@@ -18,7 +18,7 @@ import { ProductCatalogNavigationService } from '@shared/services';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArticleSearchMainComponent implements OnInit, OnDestroy {
readonly history$ = this.catalog.getSearchHistory({ take: 7 }).pipe(catchError(() => NEVER));
readonly history$ = this.catalog.getSearchHistory({ take: 5 }).pipe(catchError(() => NEVER));
fetching$ = this.searchService.fetching$;
@@ -112,17 +112,6 @@ export class ArticleSearchMainComponent implements OnInit, OnDestroy {
}
})
);
// #4143 To make Splitscreen Search and Filter work combined
this.subscriptions.add(
this.searchService.searchStarted.subscribe(async (_) => {
const queryParams = {
...this.cleanupQueryParams(this.searchService.filter.getQueryParams()),
main_qs: this.sharedFilterInputGroupMain?.uiInput?.value,
};
await this.searchService.setDefaultFilter(queryParams);
})
);
}
ngOnDestroy() {

View File

@@ -1,12 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { UiIconModule } from '@ui/icon';
import { ArticleSearchMainComponent } from './search-main.component';
import { FilterModule } from '@shared/components/filter';
import { FilterNextModule } from 'apps/shared/components/filter/src/lib';
import { RouterModule } from '@angular/router';
import { IconComponent, IconModule } from '@shared/components/icon';
@NgModule({
imports: [CommonModule, RouterModule, IconComponent, FilterModule, IconModule],
imports: [CommonModule, RouterModule, UiIconModule, FilterNextModule],
exports: [ArticleSearchMainComponent],
declarations: [ArticleSearchMainComponent],
providers: [],

View File

@@ -1,8 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Router } from '@angular/router';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { CheckoutNavigationService, ProductCatalogNavigationService } from '@shared/services';
import { UiModalRef } from '@ui/modal';
@Component({
@@ -12,27 +10,18 @@ import { UiModalRef } from '@ui/modal';
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AddedToCartModalComponent {
get isTablet() {
return this._environment.matchTablet();
}
constructor(
public ref: UiModalRef<any, any>,
private readonly _router: Router,
private readonly _applicationService: ApplicationService,
private readonly _navigation: ProductCatalogNavigationService,
private readonly _environment: EnvironmentService,
private readonly _checkoutNavigationService: CheckoutNavigationService
private readonly _applicationService: ApplicationService
) {}
async continue() {
if (this.isTablet) {
await this._navigation.navigateToProductSearch({ processId: this._applicationService.activatedProcessId });
}
continue() {
this._router.navigate([`/kunde/${this._applicationService.activatedProcessId}/product/search`]);
this.ref.close();
}
async toCart() {
await this._checkoutNavigationService.navigateToCheckoutReview({ processId: this._applicationService.activatedProcessId });
toCart() {
this._router.navigate([`/kunde/${this._applicationService.activatedProcessId}/cart/review`]);
this.ref.close();
}
}

View File

@@ -1,4 +1,4 @@
<ng-container *ngIf="!mainOutletActive; else mainOutlet">
<ng-container *ngIf="!(mainOutletActive$ | async); else mainOutlet">
<div class="bg-ucla-blue rounded w-[4.375rem] h-[5.625rem] animate-[load_1s_linear_infinite]"></div>
<div class="flex flex-col flex-grow">
<div class="h-4 bg-ucla-blue ml-4 mb-2 w-[7.8125rem] animate-[load_1s_linear_infinite]"></div>

View File

@@ -1,4 +1,7 @@
import { Component, ChangeDetectionStrategy, HostBinding, Input } from '@angular/core';
import { Component, ChangeDetectionStrategy, HostBinding } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ProductCatalogNavigationService } from '@shared/services';
import { shareReplay } from 'rxjs/operators';
@Component({
selector: 'page-search-result-item-loading',
@@ -7,12 +10,13 @@ import { Component, ChangeDetectionStrategy, HostBinding, Input } from '@angular
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SearchResultItemLoadingComponent {
@Input()
mainOutletActive?: boolean = false;
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
constructor() {}
constructor(private _navigationService: ProductCatalogNavigationService, private _activatedRoute: ActivatedRoute) {}
@HostBinding('style') get class() {
return this.mainOutletActive ? { height: '6.125rem' } : '';
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
}
}

View File

@@ -1,10 +1,9 @@
<a
class="page-search-result-item__item-card hover p-5 desktop-small:px-4 desktop-small:py-[0.625rem] h-[13.25rem] desktop-small:h-[11.3125rem] bg-white border border-solid border-transparent rounded"
[class.page-search-result-item__item-card-main]="mainOutletActive"
[class.page-search-result-item__item-card-main]="mainOutletActive$ | async"
[routerLink]="detailsPath"
[routerLinkActive]="!isTablet && !mainOutletActive ? 'active' : ''"
[routerLinkActive]="!isTablet && !(mainOutletActive$ | async) ? 'active' : ''"
[queryParamsHandling]="!isTablet ? 'preserve' : ''"
(click)="isDesktop ? scrollIntoView() : ''"
>
<div class="page-search-result-item__item-thumbnail text-center mr-4 w-[50px] h-[79px]">
<img
@@ -16,7 +15,10 @@
/>
</div>
<div class="page-search-result-item__item-grid-container" [class.page-search-result-item__item-grid-container-main]="mainOutletActive">
<div
class="page-search-result-item__item-grid-container"
[class.page-search-result-item__item-grid-container-main]="mainOutletActive$ | async"
>
<div
class="page-search-result-item__item-contributors desktop-small:text-p3 font-bold text-[#0556B4] text-ellipsis overflow-hidden max-w-[24rem] whitespace-nowrap"
>
@@ -65,7 +67,7 @@
<div
class="page-search-result-item__item-price desktop-small:text-p3 font-bold justify-self-end"
[class.page-search-result-item__item-price-main]="mainOutletActive"
[class.page-search-result-item__item-price-main]="mainOutletActive$ | async"
>
{{ item?.catalogAvailability?.price?.value?.value | currency: 'EUR':'code' }}
</div>
@@ -83,7 +85,7 @@
<div
class="page-search-result-item__item-stock desktop-small:text-p3 font-bold z-dropdown justify-self-start"
[class.justify-self-end]="!mainOutletActive"
[class.justify-self-end]="!(mainOutletActive$ | async)"
[uiOverlayTrigger]="tooltip"
[overlayTriggerDisabled]="!(stockTooltipText$ | async)"
>
@@ -113,9 +115,9 @@
<div
class="page-search-result-item__item-ssc desktop-small:text-p3 w-full text-right overflow-hidden text-ellipsis whitespace-nowrap"
[class.page-search-result-item__item-ssc-main]="mainOutletActive"
[class.page-search-result-item__item-ssc-main]="mainOutletActive$ | async"
>
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive">
<div class="hidden" [class.page-search-result-item__item-ssc-tooltip]="mainOutletActive$ | async">
{{ item?.catalogAvailability?.ssc }} - {{ item?.catalogAvailability?.sscText }}
</div>
<strong>{{ item?.catalogAvailability?.ssc }}</strong> - {{ item?.catalogAvailability?.sscText }}

View File

@@ -1,5 +1,5 @@
import { DatePipe } from '@angular/common';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostBinding, ElementRef } from '@angular/core';
import { Component, ChangeDetectionStrategy, Input, EventEmitter, Output, HostListener, HostBinding } from '@angular/core';
import { ApplicationService } from '@core/application';
import { EnvironmentService } from '@core/environment';
import { DomainAvailabilityService, DomainInStockService } from '@domain/availability';
@@ -8,9 +8,10 @@ import { ItemDTO } from '@swagger/cat';
import { DateAdapter } from '@ui/common';
import { isEqual } from 'lodash';
import { combineLatest } from 'rxjs';
import { debounceTime, switchMap, map, shareReplay, filter } from 'rxjs/operators';
import { debounceTime, switchMap, map, shareReplay, filter, first } from 'rxjs/operators';
import { ArticleSearchService } from '../article-search.store';
import { ProductCatalogNavigationService } from '@shared/services';
import { ActivatedRoute } from '@angular/router';
export interface SearchResultItemComponentState {
item?: ItemDTO;
@@ -50,9 +51,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}
}
@Input()
mainOutletActive?: boolean = false;
@Output()
selectedChange = new EventEmitter<ItemDTO>();
@@ -77,10 +75,6 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return this._environment.matchTablet();
}
get isDesktop() {
return this._environment.matchDesktop();
}
get detailsPath() {
return this._navigationService.getArticleDetailsPath({ processId: this.applicationService.activatedProcessId, itemId: this.item?.id });
}
@@ -89,6 +83,10 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
return this._navigationService.getArticleSearchResultsPath(this.applicationService.activatedProcessId);
}
get mainOutletActive$() {
return this._navigationService?.mainOutletActive$(this._activatedRoute).pipe(shareReplay());
}
defaultBranch$ = this._availability.getDefaultBranch();
selectedBranchId$ = this.applicationService.activatedProcessId$.pipe(
@@ -99,7 +97,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
map(([defaultBranch, selectedBranch]) => {
const branch = selectedBranch ?? defaultBranch;
return branch.branchType !== 4;
})
}),
shareReplay(1)
);
stockTooltipText$ = combineLatest([this.defaultBranch$, this.selectedBranchId$]).pipe(
@@ -115,7 +114,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
}
}
return '';
})
}),
shareReplay(1)
);
inStock$ = combineLatest([this.item$, this.selectedBranchId$, this.defaultBranch$]).pipe(
@@ -123,7 +123,8 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
filter(([item, branch, defaultBranch]) => !!item && !!defaultBranch),
switchMap(([item, branch, defaultBranch]) =>
this._stockService.getInStock$({ itemId: item.id, branchId: branch?.id ?? defaultBranch?.id })
)
),
shareReplay(1)
);
constructor(
@@ -135,7 +136,7 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
private _availability: DomainAvailabilityService,
private _environment: EnvironmentService,
private _navigationService: ProductCatalogNavigationService,
private _elRef: ElementRef<HTMLElement>
private _activatedRoute: ActivatedRoute
) {
super({
selected: false,
@@ -143,21 +144,16 @@ export class SearchResultItemComponent extends ComponentStore<SearchResultItemCo
});
}
scrollIntoView() {
this._elRef.nativeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
setSelected() {
const isSelected = this._articleSearchService.selectedItemIds.includes(this.item?.id);
this._articleSearchService.setSelected({ selected: !isSelected, itemId: this.item?.id });
// #4135 Code auskommentiert bis zur Klärung
// if (!this.isTablet) {
// this.selectedChange.emit(this.item);
// }
if (!this.isTablet) {
this.selectedChange.emit(this.item);
}
}
@HostBinding('style') get class() {
return this.mainOutletActive ? { height: '6.125rem' } : '';
return this._navigationService.mainOutletActive(this._activatedRoute) ? { height: '6.125rem' } : '';
}
}

View File

@@ -22,7 +22,7 @@
[routerLink]="filterRoute"
queryParamsHandling="preserve"
>
<shared-icon class="mr-2" icon="filter-variant"></shared-icon>
<ui-svg-icon class="mr-2" icon="filter-variant"></ui-svg-icon>
Filter
</a>
</div>
@@ -47,40 +47,32 @@
</shared-order-by-filter>
</div>
<div class="h-full relative">
<cdk-virtual-scroll-viewport
#scrollContainer
class="product-list h-full"
[itemSize]="(mainOutletActive$ | async) ? 98 : 181"
minBufferPx="1200"
[maxBufferPx]="maxBufferCdkScrollContainer$ | async"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<search-result-item
class="page-search-results__result-item"
[class.page-search-results__result-item-main]="mainOutletActive$ | async"
*cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
(selectedChange)="addToCart($event)"
[selectable]="isSelectable(item)"
[item]="item"
[mainOutletActive]="mainOutletActive$ | async"
></search-result-item>
<page-search-result-item-loading
[mainOutletActive]="mainOutletActive$ | async"
*ngIf="fetching$ | async"
></page-search-result-item-loading>
</cdk-virtual-scroll-viewport>
<div class="actions z-sticky h-0">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary"
(click)="addToCart()"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>
</div>
</div>
<cdk-virtual-scroll-viewport
#scrollContainer
class="product-list"
[itemSize]="(mainOutletActive$ | async) ? 98 : 187"
minBufferPx="2800"
maxBufferPx="2800"
(scrolledIndexChange)="scrolledIndexChange($event)"
>
<search-result-item
class="page-search-results__result-item"
[class.page-search-results__result-item-main]="mainOutletActive$ | async"
*cdkVirtualFor="let item of results$ | async; trackBy: trackByItemId"
(selectedChange)="addToCart($event)"
[selectable]="isSelectable(item)"
[item]="item"
></search-result-item>
<page-search-result-item-loading *ngIf="fetching$ | async"></page-search-result-item-loading>
</cdk-virtual-scroll-viewport>
<!-- #4135 Code auskommentiert bis zur Klärung -->
<!-- <div *ngIf="isTablet" class="actions z-fixed"> -->
<div *ngIf="isTablet" class="actions z-fixed">
<button
[disabled]="loading$ | async"
*ngIf="(selectedItemIds$ | async)?.length > 0"
class="cta-cart cta-action-primary"
(click)="addToCart()"
>
<ui-spinner [show]="loading$ | async">In den Warenkorb legen</ui-spinner>
</button>
</div>

View File

@@ -30,7 +30,9 @@
}
.actions {
@apply flex sticky bottom-10 items-center justify-center;
@apply fixed bottom-16 inline-grid grid-flow-col gap-7;
left: 50%;
transform: translateX(-50%);
.cta-cart {
@apply border-2 border-solid border-brand rounded-full py-3 px-6 font-bold text-lg outline-none self-end whitespace-nowrap no-underline;

View File

@@ -1,15 +1,5 @@
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import {
Component,
ChangeDetectionStrategy,
OnInit,
OnDestroy,
ViewChild,
ViewChildren,
QueryList,
TrackByFunction,
AfterViewInit,
} from '@angular/core';
import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, ViewChild, ViewChildren, QueryList, TrackByFunction } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { ApplicationService } from '@core/application';
import { BreadcrumbService } from '@core/breadcrumb';
@@ -21,7 +11,7 @@ import { UiErrorModalComponent, UiModalService } from '@ui/modal';
import { CacheService } from 'apps/core/cache/src/public-api';
import { isEqual } from 'lodash';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { debounceTime, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { debounceTime, first, map, shareReplay, 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';
@@ -34,7 +24,7 @@ import { Filter, FilterInputGroupMainComponent } from 'apps/shared/components/fi
styleUrls: ['search-results.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterViewInit {
export class ArticleSearchResultsComponent implements OnInit, OnDestroy {
@ViewChildren(SearchResultItemComponent) listItems: QueryList<SearchResultItemComponent>;
@ViewChild('scrollContainer', { static: true })
scrollContainer: CdkVirtualScrollViewport;
@@ -68,10 +58,6 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
return this._environment.matchTablet();
}
get isDesktop() {
return this._environment.matchDesktop();
}
hasFilter$ = combineLatest([this.searchService.filter$, this.searchService.defaultSettings$]).pipe(
map(([filter, defaultFilter]) => {
const filterQueryParams = filter?.getQueryParams();
@@ -88,27 +74,9 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
}
get mainOutletActive$() {
return this._environment.matchTablet$.pipe(map((state) => this._navigationService.mainOutletActive(this.route, state?.matches)));
return this._navigationService?.mainOutletActive$(this.route).pipe(shareReplay());
}
get rightOutletLocation() {
return this._navigationService.getOutletLocations(this.route).right;
}
// Ticket #4169 Splitscreen
// Render genug Artikel um bei Navigation auf Trefferliste | PDP zum angewählten Artikel zu Scrollen
maxBufferCdkScrollContainer$ = this.results$.pipe(
withLatestFrom(this.mainOutletActive$),
map(([results, mainOutlet]) => {
if (!mainOutlet && results?.length > 0) {
// Splitscreen mode: Items Length * Item Pixel Height
return results.length * 181;
} else {
return 1200;
}
})
);
constructor(
public searchService: ArticleSearchService,
private route: ActivatedRoute,
@@ -158,22 +126,15 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
const cleanQueryParams = this.cleanupQueryParams(queryParams);
if (!isEqual(cleanQueryParams, this.cleanupQueryParams(this.searchService.filter.getQueryParams()))) {
if (this.rightOutletLocation !== 'filter') {
await this.searchService.setDefaultFilter(queryParams);
}
await this.searchService.setDefaultFilter(queryParams);
const data = this.getCachedData(processId, queryParams, selectedBranch?.id);
if (data.items?.length > 0) {
this.searchService.setItems(data.items);
this.searchService.setHits(data.hits);
}
if (data.items?.length === 0 && this.rightOutletLocation !== 'filter') {
this.searchService.setItems(data.items);
this.searchService.setHits(data.hits);
if (data.items?.length === 0) {
this.search();
} else {
if (!this.isDesktop || this._navigationService.mainOutletActive(this.route)) {
this.scrollTop(Number(queryParams.scroll_position ?? 0));
} else {
this.scrollItemIntoView();
}
this.scrollTop(Number(queryParams.scroll_position ?? 0));
const selectedItemIds: Array<string> = queryParams?.selected_item_ids?.split(',') ?? [];
for (const id of selectedItemIds) {
if (id) {
@@ -183,12 +144,8 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
}
}
const process = await this.application.getProcessById$(processId).pipe(first()).toPromise();
if (!!process) {
await this.updateBreadcrumbs(processId, queryParams);
await this.createBreadcrumb(processId, queryParams);
}
await this.updateBreadcrumbs(processId, queryParams);
await this.createBreadcrumb(processId, queryParams);
if (this.isTablet || this.route?.outlet === 'main') {
await this.removeDetailsBreadcrumb(processId);
}
@@ -201,23 +158,11 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
const params = state.filter.getQueryParams();
if ((state.hits === 1 && this.isTablet) || (!this.isTablet && !this._navigationService.mainOutletActive(this.route))) {
const item = state.items.find((f) => f);
const ean = this.route?.snapshot?.params?.ean;
const itemId = this.route?.snapshot?.params?.id ? Number(this.route?.snapshot?.params?.id) : item.id; // Nicht zum ersten Item der Liste springen wenn bereits eines selektiert ist
// Navigation from Cart uses ean
if (!!ean) {
this._navigationService.navigateToDetails({
processId,
ean,
queryParams: this.isTablet ? undefined : params,
});
} else {
this._navigationService.navigateToDetails({
processId,
itemId,
queryParams: this.isTablet ? undefined : params,
});
}
this._navigationService.navigateToDetails({
processId,
itemId: item.id,
queryParams: this.isTablet ? undefined : params,
});
} else if (this.isTablet || this._navigationService.mainOutletActive(this.route)) {
this._navigationService.navigateToResults({
processId,
@@ -227,24 +172,6 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
}
})
);
// #4143 To make Splitscreen Search and Filter work combined
this.subscriptions.add(
this.searchService.searchStarted.subscribe(async (options) => {
if (!options?.clear) {
const queryParams = {
...this.cleanupQueryParams(this.searchService.filter.getQueryParams()),
main_qs: this.sharedFilterInputGroupMain?.uiInput?.value,
};
await this.searchService.setDefaultFilter(queryParams);
}
})
);
}
ngAfterViewInit(): void {
this.scrollItemIntoView();
}
ngOnDestroy() {
@@ -272,20 +199,22 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
search(filter?: Filter) {
if (!!filter) {
this.sharedFilterInputGroupMain.cancelAutocomplete();
this.searchService.setFilter(filter);
}
this.searchService.search({ clear: true });
}
scrollTop(scrollPos: number) {
setTimeout(() => this.scrollContainer.scrollTo({ top: scrollPos }), 0);
async removeDetailBreadcrumb(processId: number) {
const crumbs = await this.breadcrumb.getBreadcrumbsByKeyAndTags$(processId, ['catalog', 'details']).pipe(first()).toPromise();
for (const crumb of crumbs) {
this.breadcrumb.removeBreadcrumb(crumb.id);
}
}
scrollItemIntoView() {
setTimeout(() => {
const item = this.listItems?.find((item) => item.item.id === Number(this.route?.snapshot?.params?.id));
item?.scrollIntoView();
}, 0);
scrollTop(scrollPos: number) {
setTimeout(() => this.scrollContainer.scrollTo({ top: scrollPos }), 0);
}
async scrolledIndexChange(index: number) {
@@ -332,7 +261,7 @@ export class ArticleSearchResultsComponent implements OnInit, OnDestroy, AfterVi
await this.breadcrumb.addBreadcrumbIfNotExists({
key: processId,
name,
path: this._navigationService.getArticleSearchResultsPath(processId),
path: this._navigationService.getArticleSearchResultsPath(this.application.activatedProcessId),
params: queryParams,
section: 'customer',
tags: ['catalog', 'filter', 'results'],

View File

@@ -15,10 +15,9 @@ import { SearchResultItemLoadingComponent } from './search-result-item-loading.c
import { SearchResultItemComponent } from './search-result-item.component';
import { ArticleSearchResultsComponent } from './search-results.component';
import { SearchResultSelectedPipe } from './selected/search-result-selected.pipe';
import { FilterAutocompleteProvider, FilterModule, OrderByFilterModule } from '@shared/components/filter';
import { FilterAutocompleteProvider, FilterNextModule, OrderByFilterModule } from 'apps/shared/components/filter/src/lib';
import { FocusSearchboxEvent } from '../focus-searchbox.event';
import { ArticleSearchMainAutocompleteProvider } from '../providers';
import { IconComponent } from '@shared/components/icon';
@NgModule({
imports: [
@@ -33,8 +32,7 @@ import { IconComponent } from '@shared/components/icon';
OrderByFilterModule,
ScrollingModule,
UiTooltipModule,
FilterModule,
IconComponent,
FilterNextModule,
],
exports: [ArticleSearchResultsComponent, SearchResultItemComponent],
declarations: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,349 +0,0 @@
<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">
{{ feature?.description }}
</div>
</div>
<button
[disabled]="editButtonDisabled$ | async"
class="page-customer-order-details-header__edit-cta bg-transparent text-brand font-bold border-none text-p1"
*ngIf="editClick.observers.length"
(click)="editClick.emit(orderItem)"
>
Bearbeiten
</button>
</div>
<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>
{{ orderItem?.lastName }}
{{ orderItem?.firstName }}
</div>
<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]">
{{ 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">
<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"
/>
</svg>
<strong> Bezahlt über KulturPass </strong>
</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="w-[9rem]">Kundennummer</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="w-[9rem]">Vorgang-ID</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="w-[9rem]">Bestelldatum</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="w-[9rem]">Status</div>
<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"
[icon]="icon"
></shared-icon>
<span *ngIf="!(canEditStatus$ | async)">
{{ orderItem?.processingStatus | processingStatus }}
</span>
<ng-container *ngIf="canEditStatus$ | async">
<button
class="cta-status-dropdown"
[uiOverlayTrigger]="statusDropdown"
[disabled]="changeStatusDisabled$ | async"
#dropdown="uiOverlayTrigger"
>
<div class="mr-[0.375rem]">
{{ orderItem?.processingStatus | processingStatus }}
</div>
<shared-icon
[size]="24"
[class.rotate-0]="!dropdown.opened"
[class.-rotate-180]="dropdown.opened"
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)">
{{ 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>
</div>
<div class="flex flex-row page-customer-order-details-header__order-source" data-detail-id="Bestellkanal">
<div class="w-[9rem]">Bestellkanal</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"
[ngSwitch]="orderItem.processingStatus"
data-detail-id="Geaendert"
>
<!-- orderType 1 === Abholung / Rücklage; orderType 2 === Versand / B2B-Versand; orderType 4 === Download (ist manchmal auch orderType 2) -->
<ng-container *ngIf="orderItem.orderType === 1; else changeDate">
<ng-container *ngSwitchCase="16">
<ng-container *ngTemplateOutlet="vslLieferdatum"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="8192">
<ng-container *ngTemplateOutlet="vslLieferdatum"></ng-container>
</ng-container>
<ng-container *ngSwitchCase="128">
<ng-container *ngTemplateOutlet="abholfrist"></ng-container>
</ng-container>
</ng-container>
<ng-template #changeDate>
<div class="w-[9rem]">Geändert</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)"
>
<ng-container *ngTemplateOutlet="preferredPickUpDate"></ng-container>
</div>
<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="w-[9rem]">Benachrichtigung</div>
<div class="flex flex-row font-bold">{{ (notificationsChannel | notificationsChannel) || '-' }}</div>
</div>
<div
*ngIf="!!digOrderNumber && orderItem.orderType !== 1"
class="flex flex-row page-customer-order-details-header__dig-number"
data-detail-id="Dig-Bestellnummer"
>
<div class="w-[9rem]">Dig-Bestell Nr.</div>
<div class="flex flex-row font-bold">{{ digOrderNumber }}</div>
</div>
</div>
</div>
</div>
<div class="flex flex-row items-center relative bg-[#F5F7FA] p-4 rounded-t">
<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">
<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">
<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">
<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">
<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">
<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">
<shared-icon [size]="24" icon="isa-download"></shared-icon>
</div>
<p class="font-bold text-p1">Download</p>
</ng-container>
</ng-container>
</div>
<div class="page-customer-order-details-header__additional-addresses" *ngIf="showAddresses">
<button (click)="openAddresses = !openAddresses" class="text-[#0556B4]">
Lieferadresse / Rechnungsadresse {{ openAddresses ? 'ausblenden' : 'anzeigen' }}
</button>
<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">
<p>Lieferadresse</p>
<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?.address?.street }} {{ order.shipping?.data?.address?.streetNumber }}</p>
<p>{{ order.shipping?.data?.address?.zipCode }} {{ order.shipping?.data?.address?.city }}</p>
<p>{{ order.shipping?.data?.address?.info }}</p>
</div>
</div>
<div *ngIf="order.billing" class="page-customer-order-details-header__addresses-popover-billing">
<p>Rechnungsadresse</p>
<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?.address?.street }} {{ order.billing?.data?.address?.streetNumber }}</p>
<p>{{ order.billing?.data?.address?.zipCode }} {{ order.billing?.data?.address?.city }}</p>
<p>{{ order.billing?.data?.address?.info }}</p>
</div>
</div>
</div>
</div>
</div>
<div 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>
<ng-template #featureLoading>
<div class="fetch-wrapper">
<div class="fetching"></div>
<div class="fetching"></div>
<div class="fetching"></div>
</div>
</ng-template>
<ng-template #abholfrist>
<div class="w-[9rem]">Abholfrist</div>
<div *ngIf="!(changeDateLoader$ | async)" class="flex flex-row font-bold">
<button
[uiOverlayTrigger]="deadlineDatepicker"
#deadlineDatepickerTrigger="uiOverlayTrigger"
[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>
</button>
<ui-datepicker
#deadlineDatepicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[selected]="orderItem?.pickUpDeadline"
(save)="updatePickupDeadline($event)"
>
</ui-datepicker>
</div>
<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="w-[9rem]">Zurücklegen bis</div>
<div *ngIf="!(changePreferredDateLoader$ | async)" class="flex flex-row font-bold">
<button
[uiOverlayTrigger]="preferredPickUpDatePicker"
#preferredPickUpDatePickerTrigger="uiOverlayTrigger"
[disabled]="(!isKulturpass && !!orderItem?.features?.paid) || (changeDateDisabled$ | async)"
class="cta-pickup-preferred"
>
<strong *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>
</button>
<ui-datepicker
#preferredPickUpDatePicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[selected]="(preferredPickUpDate$ | async) || today"
(save)="updatePreferredPickUpDate($event)"
>
</ui-datepicker>
</div>
<ui-spinner *ngIf="changePreferredDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"> </ui-spinner>
</ng-template>
<ng-template #vslLieferdatum>
<div class="w-[9rem]">vsl. Lieferdatum</div>
<div *ngIf="!(changeDateLoader$ | async)" class="flex flex-row font-bold">
<button
class="cta-datepicker"
[disabled]="changeDateDisabled$ | async"
[uiOverlayTrigger]="uiDatepicker"
#datepicker="uiOverlayTrigger"
>
<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>
</button>
<ui-datepicker
#uiDatepicker
yPosition="below"
xPosition="after"
[xOffset]="8"
[min]="minDateDatepicker"
[disabledDaysOfWeek]="[0]"
[selected]="orderItem?.estimatedShippingDate"
(save)="updateEstimatedShippingDate($event)"
>
</ui-datepicker>
</div>
<ui-spinner *ngIf="changeDateLoader$ | async; let loader" class="flex flex-row font-bold loader" [show]="loader"></ui-spinner>
</ng-template>
</ng-container>

View File

@@ -1,140 +0,0 @@
:host {
@apply block mb-[0.125rem];
}
.page-customer-order-details-header__edit-cta {
&:disabled {
@apply text-inactive-customer;
}
}
.page-customer-order-details-header__paid-marker {
@apply font-bold flex items-center justify-end;
shared-icon {
@apply mr-2 text-white bg-green-600 w-px-25 h-px-25 flex items-center justify-center rounded-full;
}
}
.page-customer-order-details-header__details {
@apply grid grid-flow-row pt-4;
}
.page-customer-order-details-header__details-header {
@apply flex flex-row justify-between font-bold;
}
.page-customer-order-details-header__details-wrapper {
@apply grid grid-flow-row gap-x-6 gap-y-[0.375rem];
grid-template-columns: 50% auto;
grid-template-areas:
'buyernumber .'
'ordernumber .'
'orderdate processingstatus'
'ordersource changedate'
'dignotification pickup';
.detail {
shared-icon {
@apply flex items-center;
}
.loader {
width: 130px;
}
}
}
.page-customer-order-details-header__buyer-number {
grid-area: buyernumber;
}
.page-customer-order-details-header__order-number {
grid-area: ordernumber;
}
.page-customer-order-details-header__order-date {
grid-area: orderdate;
}
.page-customer-order-details-header__processing-status {
grid-area: processingstatus;
}
.page-customer-order-details-header__order-source {
grid-area: ordersource;
}
.page-customer-order-details-header__change-date {
grid-area: changedate;
}
.page-customer-order-details-header__pick-up {
grid-area: pickup;
}
.page-customer-order-details-header__dig-and-notification {
grid-area: dignotification;
}
.cta-status-dropdown,
.cta-datepicker,
.cta-pickup-deadline,
.cta-pickup-preferred {
@apply flex flex-row border-none outline-none text-p2 font-bold bg-transparent items-center px-0 mx-0;
&:disabled {
@apply text-disabled-customer cursor-not-allowed;
shared-icon {
@apply text-disabled-customer;
}
}
}
.page-customer-order-details-header__select {
@apply flex flex-col items-end;
}
.page-customer-order-details-header__additional-addresses {
.page-customer-order-details-header__addresses-popover {
@apply absolute inset-x-0 top-16 bottom-0 z-popover;
.close {
@apply bg-white absolute right-0 p-6;
}
.page-customer-order-details-header__addresses-popover-data {
@apply flex flex-col bg-white p-6 z-popover min-h-[200px];
box-shadow: 0px 6px 24px rgba(206, 212, 219, 0.8);
.page-customer-order-details-header__addresses-popover-delivery {
@apply grid mb-6;
grid-template-columns: 153px auto;
.page-customer-order-details-header__addresses-popover-delivery-data {
p {
@apply font-bold;
}
}
}
.page-customer-order-details-header__addresses-popover-billing {
@apply grid;
grid-template-columns: 153px auto;
.page-customer-order-details-header__addresses-popover-billing-data {
p {
@apply font-bold;
}
}
}
}
}
}
.cta-select-all {
@apply text-brand bg-transparent text-p2 font-bold outline-none border-none;
}
.fetch-wrapper {
@apply grid grid-flow-col gap-4;
}
.fetching {
@apply w-24 h-px-20 bg-customer;
animation: load 0.75s linear infinite;
}

View File

@@ -1,217 +0,0 @@
import {
Component,
ChangeDetectionStrategy,
ChangeDetectorRef,
Output,
EventEmitter,
Input,
OnChanges,
SimpleChanges,
} from '@angular/core';
import { CrmCustomerService } from '@domain/crm';
import { DomainOmsService } from '@domain/oms';
import { NotificationChannel } from '@swagger/checkout';
import { KeyValueDTOOfStringAndString, OrderDTO, OrderItemListItemDTO } from '@swagger/oms';
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';
@Component({
selector: 'page-customer-order-details-header',
templateUrl: 'customer-order-details-header.component.html',
styleUrls: ['customer-order-details-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerOrderDetailsHeaderComponent implements OnChanges {
@Output()
editClick = new EventEmitter<OrderItemListItemDTO>();
@Output()
handleAction = new EventEmitter<KeyValueDTOOfStringAndString>();
@Input()
order: OrderDTO;
get isKulturpass() {
return this.order?.features?.orderSource === 'KulturPass';
}
minDateDatepicker = this.dateAdapter.addCalendarDays(this.dateAdapter.today(), -1);
today = this.dateAdapter.today();
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)));
preferredPickUpDate$ = new BehaviorSubject<Date>(undefined);
notificationsChannel: NotificationChannel = 0;
changeDateLoader$ = new BehaviorSubject<boolean>(false);
changePreferredDateLoader$ = new BehaviorSubject<boolean>(false);
changeStatusLoader$ = new BehaviorSubject<boolean>(false);
changeStatusDisabled$ = this._store.changeActionDisabled$;
changeDateDisabled$ = this.changeStatusDisabled$;
features$ = 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))
)
),
shareReplay()
);
statusActions$ = this.orderItem$.pipe(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)
);
crudaUpdate$ = this.orderItem$.pipe(map((orederItem) => !!(orederItem?.cruda & 4)));
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)
);
openAddresses: boolean = false;
get digOrderNumber(): string {
return this.order?.linkedRecords?.find((_) => true)?.number;
}
get showAddresses(): boolean {
return (this.order?.orderType === 2 || this.order?.orderType === 4) && (!!this.order?.shipping || !!this.order?.billing);
}
get showFeature(): boolean {
return !!this.order?.features && !!this.order?.features?.orderType;
}
constructor(
private _store: CustomerOrderDetailsStore,
private customerService: CrmCustomerService,
private dateAdapter: DateAdapter,
private omsService: DomainOmsService,
private cdr: ChangeDetectorRef
) {}
ngOnChanges(changes: SimpleChanges): void {
if (changes.order) {
this.findLatestPreferredPickUpDate();
this.computeNotificationChannel();
}
}
computeNotificationChannel() {
const order = this.order;
this.notificationsChannel = order?.notificationChannels ?? 0;
this.cdr.markForCheck();
}
async updatePickupDeadline(deadline: Date) {
this.changeDateLoader$.next(true);
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) {
try {
const res = await this.omsService
.setPickUpDeadline(item.orderId, item.orderItemId, item.orderItemSubsetId, deadline?.toISOString())
.pipe(first())
.toPromise();
item.pickUpDeadline = deadline.toISOString();
} catch (error) {
console.error(error);
}
}
}
this.changeDateLoader$.next(false);
this.changeStatusDisabled$.next(false);
this._store.updateOrderItems(orderItems);
}
async updateEstimatedShippingDate(estimatedShippingDate: Date) {
this.changeDateLoader$.next(true);
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) {
try {
const res = await this.omsService
.setEstimatedShippingDate(item.orderId, item.orderItemId, item.orderItemSubsetId, estimatedShippingDate?.toISOString())
.pipe(first())
.toPromise();
item.estimatedShippingDate = res.estimatedShippingDate;
} catch (error) {
console.error(error);
}
}
}
this.changeDateLoader$.next(false);
this.changeStatusDisabled$.next(false);
this._store.updateOrderItems(orderItems);
}
async handleActionClick(action: KeyValueDTOOfStringAndString) {
this.changeStatusLoader$.next(true);
this.handleAction.emit(action);
setTimeout(() => this.changeStatusLoader$.next(false), 1000);
this.cdr.markForCheck();
}
selectAll() {
this._store.selectAllOrderItems();
}
async updatePreferredPickUpDate(date: Date) {
this.changePreferredDateLoader$.next(true);
this.changeStatusDisabled$.next(true);
const orderItems = cloneDeep(this._store.items);
const data: Record<string, string> = {};
orderItems.forEach((item) => {
data[`${item.orderItemSubsetId}`] = date?.toISOString();
});
try {
await this.omsService.setPreferredPickUpDate({ data }).toPromise();
this.order.items.forEach((item) => {
item.data.subsetItems.forEach((subsetItem) => (subsetItem.data.preferredPickUpDate = date.toISOString()));
});
this.findLatestPreferredPickUpDate();
} catch (error) {
console.error(error);
}
this.changePreferredDateLoader$.next(false);
this.changeStatusDisabled$.next(false);
}
findLatestPreferredPickUpDate() {
let latestDate;
const subsetItems = this.order?.items
?.reduce((acc, item) => {
return [...acc, ...item.data.subsetItems];
}, [])
?.filter((a) => !!a.data.preferredPickUpDate);
if (subsetItems?.length > 0) {
latestDate = new Date(
subsetItems?.reduce((a, b) => {
return new Date(a.data.preferredPickUpDate) > new Date(b.data.preferredPickUpDate) ? a : b;
})?.data?.preferredPickUpDate
);
}
this.preferredPickUpDate$.next(latestDate);
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './customer-order-details-header.component';
// end:ng42.barrel

View File

@@ -1,233 +0,0 @@
<ng-container *ngIf="orderItem$ | async; let orderItem">
<div #features class="page-customer-order-details-item__features">
<ng-container *ngIf="orderItem?.features?.prebooked">
<img [uiOverlayTrigger]="prebookedTooltip" src="/assets/images/tag_icon_preorder.svg" [alt]="orderItem?.features?.prebooked" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #prebookedTooltip [closeable]="true">
Artikel wird für Sie vorgemerkt.
</ui-tooltip>
</ng-container>
<ng-container *ngIf="notificationsSent$ | async; let notificationsSent">
<ng-container *ngIf="notificationsSent?.NOTIFICATION_EMAIL">
<img [uiOverlayTrigger]="emailTooltip" src="/assets/images/email_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #emailTooltip [closeable]="true">
Per E-Mail benachrichtigt <br />
<ng-container *ngFor="let notification of notificationsSent?.NOTIFICATION_EMAIL">
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr<br />
</ng-container>
</ui-tooltip>
</ng-container>
<ng-container *ngIf="notificationsSent?.NOTIFICATION_SMS">
<img [uiOverlayTrigger]="smsTooltip" src="/assets/images/sms_bookmark.svg" />
<ui-tooltip yPosition="above" xPosition="after" [yOffset]="-11" [xOffset]="-8" #smsTooltip [closeable]="true">
Per SMS benachrichtigt <br />
<ng-container *ngFor="let notification of notificationsSent?.NOTIFICATION_SMS">
{{ notification | date: 'dd.MM.yyyy | HH:mm' }} Uhr<br />
</ng-container>
</ui-tooltip>
</ng-container>
</ng-container>
</div>
<div class="page-customer-order-details-item__item-container">
<div class="page-customer-order-details-item__thumbnail">
<img [src]="orderItem.product?.ean | productImage" [alt]="orderItem.product?.name" />
</div>
<div class="page-customer-order-details-item__details">
<div class="flex flex-row justify-between items-start mb-[1.3125rem]">
<h3
[uiElementDistance]="features"
#elementDistance="uiElementDistance"
[style.max-width.px]="elementDistance.distanceChange | async"
class="flex flex-col"
>
<div class="font-normal mb-[0.375rem]">{{ orderItem.product?.contributors }}</div>
<div>{{ orderItem.product?.name }}</div>
</h3>
<div class="history-wrapper flex flex-col items-end justify-center">
<button class="cta-history text-p1" (click)="historyClick.emit(orderItem)">
Historie
</button>
<input
*ngIf="selectable$ | async"
[ngModel]="selected$ | async"
(ngModelChange)="setSelected($event)"
class="isa-select-bullet mt-4"
type="checkbox"
/>
</div>
</div>
<div class="detail">
<div class="label">Menge</div>
<div class="value">
<ng-container *ngIf="!(canChangeQuantity$ | async)"> {{ orderItem?.quantity }}x </ng-container>
<ui-quantity-dropdown
*ngIf="canChangeQuantity$ | async"
[showTrash]="false"
[range]="orderItem?.quantity"
[(ngModel)]="quantity"
[showSpinner]="false"
>
</ui-quantity-dropdown>
<span class="overall-quantity">(von {{ orderItem?.overallQuantity }})</span>
</div>
</div>
<div class="detail" *ngIf="!!orderItem.product?.formatDetail">
<div class="label">Format</div>
<div class="value">
<img
*ngIf="orderItem?.product?.format && orderItem?.product?.format !== 'UNKNOWN'"
class="format-icon"
[src]="'/assets/images/Icon_' + orderItem.product?.format + '.svg'"
alt="format icon"
/>
<span>{{ orderItem.product?.formatDetail }}</span>
</div>
</div>
<div class="detail" *ngIf="!!orderItem.product?.ean">
<div class="label">ISBN/EAN</div>
<div class="value">{{ orderItem.product?.ean }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.price">
<div class="label">Preis</div>
<div class="value">{{ orderItem.price | currency: 'EUR' }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.retailPrice?.vat?.inPercent">
<div class="label">MwSt</div>
<div class="value">{{ orderItem.retailPrice?.vat?.inPercent }}%</div>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="orderItem.supplier">
<div class="label">Lieferant</div>
<div class="value">{{ orderItem.supplier }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.ssc || !!orderItem.sscText">
<div class="label">Meldenummer</div>
<div class="value">{{ orderItem.ssc }} - {{ orderItem.sscText }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.targetBranch">
<div class="label">Zielfiliale</div>
<div class="value">{{ orderItem.targetBranch }}</div>
</div>
<div class="detail">
<div class="label">
<ng-container
*ngIf="
orderItemFeature(orderItem) === 'Versand' ||
orderItemFeature(orderItem) === 'B2B-Versand' ||
orderItemFeature(orderItem) === 'DIG-Versand'
"
>{{ orderItem?.estimatedDelivery ? 'Lieferung zwischen' : 'Lieferung ab' }}</ng-container
>
<ng-container *ngIf="orderItemFeature(orderItem) === 'Abholung' || orderItemFeature(orderItem) === 'Rücklage'">
Abholung ab
</ng-container>
</div>
<ng-container *ngIf="!!orderItem?.estimatedDelivery || !!orderItem?.estimatedShippingDate">
<div class="value bg-[#D8DFE5] rounded w-max px-2">
<ng-container *ngIf="!!orderItem?.estimatedDelivery; else estimatedShippingDate">
{{ orderItem?.estimatedDelivery?.start | date: 'dd.MM.yy' }} und
{{ orderItem?.estimatedDelivery?.stop | date: 'dd.MM.yy' }}
</ng-container>
</div>
</ng-container>
<ng-template #estimatedShippingDate>
<ng-container *ngIf="!!orderItem?.estimatedShippingDate">
{{ orderItem?.estimatedShippingDate | date: 'dd.MM.yy' }}
</ng-container>
</ng-template>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="!!orderItem?.compartmentCode">
<div class="label">Abholfachnr.</div>
<div class="value">{{ orderItem?.compartmentCode }}</div>
</div>
<div class="detail">
<div class="label">Vormerker</div>
<div class="value">{{ orderItem.isPrebooked ? 'Ja' : 'Nein' }}</div>
</div>
<hr class="border-[#EDEFF0] border-t-2 my-4" />
<div class="detail" *ngIf="!!orderItem.paymentProcessing">
<div class="label">Zahlungsweg</div>
<div class="value">{{ orderItem.paymentProcessing || '-' }}</div>
</div>
<div class="detail" *ngIf="!!orderItem.paymentType">
<div class="label">Zahlungsart</div>
<div class="value">{{ orderItem.paymentType | paymentType }}</div>
</div>
<h4 class="receipt-header" *ngIf="receiptCount$ | async; let count">
{{ count > 1 ? 'Belege' : 'Beleg' }}
</h4>
<ng-container *ngFor="let receipt of receipts$ | async">
<div class="detail" *ngIf="!!receipt?.receiptNumber">
<div class="label">Belegnummer</div>
<div class="value">{{ receipt?.receiptNumber }}</div>
</div>
<div class="detail" *ngIf="!!receipt?.printedDate">
<div class="label">Erstellt am</div>
<div class="value">{{ receipt?.printedDate | date: 'dd.MM.yy | HH:mm' }} Uhr</div>
</div>
<div class="detail" *ngIf="!!receipt?.receiptText">
<div class="label">Rechnungstext</div>
<div class="value">{{ receipt?.receiptText || '-' }}</div>
</div>
<div class="detail" *ngIf="!!receipt?.receiptType">
<div class="label">Belegart</div>
<div class="value">
{{ receipt?.receiptType === 1 ? 'Lieferschein' : receipt?.receiptType === 64 ? 'Zahlungsbeleg' : '-' }}
</div>
</div>
</ng-container>
<div class="page-customer-order-details-item__comment flex flex-col items-start mt-[1.625rem]">
<div class="label mb-[0.375rem]">Anmerkung</div>
<div class="flex flex-row w-full">
<textarea
matInput
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
maxlength="200"
#specialCommentInput
(keydown.delete)="triggerResize()"
(keydown.backspace)="triggerResize()"
type="text"
name="comment"
placeholder="Eine Anmerkung hinzufügen"
[formControl]="specialCommentControl"
[class.inactive]="!specialCommentControl.dirty"
></textarea>
<div class="comment-actions">
<button
type="reset"
class="clear"
*ngIf="!!specialCommentControl.value?.length"
(click)="specialCommentControl.setValue(''); saveSpecialComment(); triggerResize()"
>
<shared-icon icon="close" [size]="24"></shared-icon>
</button>
<button
class="cta-save"
type="submit"
*ngIf="specialCommentControl?.enabled && specialCommentControl.dirty"
(click)="saveSpecialComment()"
>
Speichern
</button>
</div>
</div>
</div>
</div>
</div>
</ng-container>

View File

@@ -1,123 +0,0 @@
:host {
@apply grid grid-flow-row gap-0.5 relative;
}
button {
@apply px-1;
}
.page-customer-order-details-item__features {
@apply absolute grid grid-flow-col gap-2 -top-1 right-24;
img {
@apply w-12 h-12;
z-index: 1;
}
}
.page-customer-order-details-item__item-container {
@apply grid gap-8 bg-white p-4;
grid-template-columns: auto 1fr;
}
.page-customer-order-details-item__thumbnail {
img {
@apply rounded shadow-cta w-[3.625rem] h-[5.9375rem];
}
}
.page-customer-order-details-item__details {
@apply relative;
h3 {
@apply mt-0 font-bold;
}
.detail {
@apply grid gap-x-7 my-1;
grid-template-columns: auto 1fr auto;
.label {
@apply w-[8.125rem];
}
.value {
@apply flex flex-row items-center font-bold;
.format-icon {
@apply mr-2;
}
}
.overall-quantity {
@apply ml-2;
}
}
}
.page-customer-order-details-item__comment {
textarea {
@apply w-full flex-grow rounded bg-[#EDEFF0] border-[#AEB7C1] border border-solid outline-none text-p2 p-4;
resize: none;
height: auto !important;
}
textarea.inactive {
@apply text-warning font-bold;
@apply w-full flex-grow rounded bg-[#EDEFF0] border-[#AEB7C1] border border-solid outline-none text-p2 p-4 text-warning font-bold;
// ipad color fix
-webkit-text-fill-color: rgb(190, 129, 0);
opacity: 1;
}
textarea::placeholder,
textarea.inactive::placeholder {
@apply text-[#89949E] font-normal;
-webkit-text-fill-color: #89949e;
}
input {
@apply flex-grow bg-transparent border-none outline-none text-p2 mx-4;
}
input.inactive {
@apply text-warning font-bold;
@apply flex-grow bg-transparent border-none outline-none text-p2 mx-4 text-warning font-bold;
// ipad color fix
-webkit-text-fill-color: rgb(190, 129, 0);
opacity: 1;
}
button {
@apply bg-transparent text-brand font-bold text-p2 outline-none border-none;
}
button.clear {
@apply text-black;
}
.comment-actions {
@apply flex justify-center items-center;
}
}
.history-wrapper {
@apply text-right;
}
.cta-history {
@apply font-bold text-brand outline-none border-none bg-transparent -mr-1;
}
.cta-edit,
.cta-save {
@apply -mr-1;
}
ui-select-bullet {
@apply absolute top-20 right-0 p-5;
}
.receipt-header {
@apply mb-1;
}

View File

@@ -1,237 +0,0 @@
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import {
Component,
ChangeDetectionStrategy,
Input,
Output,
EventEmitter,
ChangeDetectorRef,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import { UntypedFormControl } from '@angular/forms';
import { DomainOmsService, DomainReceiptService } from '@domain/oms';
import { ComponentStore, tapResponse } from '@ngrx/component-store';
import { OrderDTO, OrderItemListItemDTO, ReceiptDTO, ReceiptType } from '@swagger/oms';
import { isEqual } from 'lodash';
import { combineLatest, NEVER, Subject, Observable } from 'rxjs';
import { catchError, filter, first, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { CustomerOrderDetailsStore } from '../customer-order-details.store';
export interface CustomerOrderDetailsItemComponentState {
orderItem?: OrderItemListItemDTO;
order?: OrderDTO;
quantity?: number;
receipts?: ReceiptDTO[];
selected?: boolean;
more: boolean;
}
@Component({
selector: 'page-customer-order-details-item',
templateUrl: 'customer-order-details-item.component.html',
styleUrls: ['customer-order-details-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CustomerOrderDetailsItemComponent extends ComponentStore<CustomerOrderDetailsItemComponentState> implements OnInit, OnDestroy {
@ViewChild('autosize') autosize: CdkTextareaAutosize;
@Output()
historyClick = new EventEmitter<OrderItemListItemDTO>();
@Output()
specialCommentChanged = new EventEmitter<void>();
@Input()
get orderItem() {
return this.get((s) => s.orderItem);
}
set orderItem(orderItem: OrderItemListItemDTO) {
if (!isEqual(this.orderItem, orderItem)) {
// Remove Prev OrderItem from selected list
this._store.selectOrderItem(this.orderItem, false);
this.patchState({ orderItem, quantity: orderItem?.quantity, receipts: [], more: false });
this.specialCommentControl.reset(orderItem?.specialComment);
// Add New OrderItem to selected list if selected was set to true by its input
if (this.get((s) => s.selected)) {
this._store.selectOrderItem(this.orderItem, true);
}
}
}
@Input()
get order() {
return this.get((s) => s.order);
}
set order(order: OrderDTO) {
this.patchState({ order });
}
readonly orderItem$ = this.select((s) => s.orderItem);
notificationsSent$ = this.orderItem$.pipe(
filter((oi) => !!oi),
switchMap((oi) =>
this._omsService
.getCompletedTasks({ orderId: oi.orderId, orderItemId: oi.orderItemId, orderItemSubsetId: oi.orderItemSubsetId, take: 4, skip: 0 })
.pipe(catchError(() => NEVER))
)
);
canChangeQuantity$ = combineLatest([this.orderItem$, this._store.fetchPartial$]).pipe(
map(([item, partialPickup]) => ([16, 8192].includes(item?.processingStatus) || partialPickup) && item.quantity > 1)
);
get quantity() {
return this.get((s) => s.quantity);
}
set quantity(quantity: number) {
if (this.quantity !== quantity) {
this.patchState({ quantity });
}
}
readonly quantity$ = this.select((s) => s.quantity);
@Input()
get selected() {
return this._store.selectedeOrderItemSubsetIds.includes(this.orderItem?.orderItemSubsetId);
}
set selected(selected: boolean) {
if (this.selected !== selected) {
this.patchState({ selected });
this._store.selectOrderItem(this.orderItem, selected);
}
}
readonly selected$ = combineLatest([this.orderItem$, this._store.selectedeOrderItemSubsetIds$]).pipe(
map(([orderItem, selectedItems]) => selectedItems.includes(orderItem?.orderItemSubsetId))
);
@Output()
selectedChange = new EventEmitter<boolean>();
get selectable() {
return this._store.itemsSelectable && this._store.items.length > 1 && this._store.fetchPartial;
}
readonly selectable$ = combineLatest([this._store.items$, this._store.itemsSelectable$, this._store.fetchPartial$]).pipe(
map(([orderItems, selectable, fetchPartial]) => orderItems.length > 1 && selectable && fetchPartial)
);
get receipts() {
return this.get((s) => s.receipts);
}
set receipts(receipts: ReceiptDTO[]) {
if (!isEqual(this.receipts, receipts)) {
this.patchState({ receipts });
this._store.updateReceipts(receipts);
}
}
readonly receipts$ = this.select((s) => s.receipts);
readonly receiptCount$ = this.select((s) => s.receipts?.length);
specialCommentControl = new UntypedFormControl();
more$ = this.select((s) => s.more);
private _onDestroy$ = new Subject();
constructor(
private _store: CustomerOrderDetailsStore,
private _domainReceiptService: DomainReceiptService,
private _omsService: DomainOmsService,
private _cdr: ChangeDetectorRef
) {
super({
more: false,
});
}
ngOnInit() {}
ngOnDestroy() {
// Remove Prev OrderItem from selected list
this._store.selectOrderItem(this.orderItem, false);
this._onDestroy$.next();
this._onDestroy$.complete();
}
loadReceipts = this.effect((done$: Observable<(receipts: ReceiptDTO[]) => void | undefined>) =>
done$.pipe(
withLatestFrom(this.orderItem$),
filter(([_, orderItem]) => !!orderItem),
switchMap(([done, orderItem]) =>
this._domainReceiptService
.getReceipts({
receiptType: 65 as ReceiptType,
ids: [orderItem.orderItemSubsetId],
eagerLoading: 1,
})
.pipe(
tapResponse(
(res) => {
const receipts = res.result.map((r) => r.item3?.data).filter((f) => !!f);
this.receipts = receipts;
if (typeof done === 'function') {
done?.(receipts);
}
},
(err) => {
if (typeof done === 'function') {
done?.([]);
}
},
() => {}
)
)
)
)
);
async saveSpecialComment() {
const { orderId, orderItemId, orderItemSubsetId } = this.orderItem;
try {
this.specialCommentControl.reset(this.specialCommentControl.value);
const res = await this._omsService
.patchComment({ orderId, orderItemId, orderItemSubsetId, specialComment: this.specialCommentControl.value ?? '' })
.pipe(first())
.toPromise();
this.orderItem = { ...this.orderItem, specialComment: this.specialCommentControl.value ?? '' };
this._store.updateOrderItems([this.orderItem]);
this.specialCommentChanged.emit();
} catch (error) {
console.error(error);
}
}
setMore(more: boolean) {
this.patchState({ more });
if (more && this.receipts.length === 0) {
this.loadReceipts(undefined);
}
}
setSelected(selected: boolean) {
this.selected = selected;
this.selectedChange.emit(selected);
this._cdr.markForCheck();
}
orderItemFeature(orderItemListItem: OrderItemListItemDTO) {
const orderItems = this.order?.items;
return orderItems?.find((orderItem) => orderItem.data.id === orderItemListItem.orderItemId)?.data?.features?.orderType;
}
triggerResize() {
this.autosize.reset();
}
}

View File

@@ -1,3 +0,0 @@
// start:ng42.barrel
export * from './customer-order-details-item.component';
// end:ng42.barrel

View File

@@ -1,28 +0,0 @@
<div class="page-customer-order-details-tags__wrapper">
<button
class="page-customer-order-details-tags__tag"
type="button"
[class.selected]="tag === (selected$ | async) && !inputFocus.focused"
*ngFor="let tag of defaultTags"
(click)="setCompartmentInfo(tag)"
>
{{ tag }}
</button>
<button
(click)="inputFocus.focus()"
type="button"
class="page-customer-order-details-tags__tag"
[class.selected]="(inputValue$ | async) === (selected$ | async) && (inputValue$ | async)"
>
<input
#inputFocus="uiFocus"
uiFocus
type="text"
[ngModel]="inputValue$ | async"
(ngModelChange)="inputValueSubject.next($event); setCompartmentInfo(inputValue)"
placeholder="..."
[size]="controlSize$ | async"
maxlength="15"
/>
</button>
</div>

View File

@@ -1,40 +0,0 @@
:host {
@apply block bg-white p-4;
}
.page-customer-order-details-tags__wrapper {
@apply grid grid-flow-col justify-center gap-2 w-auto mx-auto;
}
.page-customer-order-details-tags__tag {
@apply w-auto text-p2 border border-solid bg-white border-inactive-customer py-px-10 px-5 font-bold text-inactive-customer rounded-full;
&:focus:not(.selected) {
@apply bg-white border-inactive-customer text-inactive-customer;
}
&.selected,
&:focus-within {
@apply bg-inactive-customer text-white;
}
input {
@apply border-none outline-none text-p2 font-bold w-auto text-center text-inactive-customer bg-transparent p-0 m-0;
}
input::placeholder {
@apply text-inactive-customer;
}
input:focus {
@apply text-white;
}
input:focus::placeholder {
@apply text-white;
}
&.selected input:not(:focus) {
@apply text-white;
}
}

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