From 9d57ebf3763bb3f1b34c4d530c398769b6e8e61f Mon Sep 17 00:00:00 2001 From: Nino Righi Date: Tue, 30 Sep 2025 13:54:31 +0000 Subject: [PATCH] Merged PR 1961: feat(checkout-reward): implement reward catalog customer integration and purc... feat(checkout-reward): implement reward catalog customer integration and purchase flow - Add customer card resource and display in reward header with reset functionality - Implement shopping cart creation and management for reward purchases - Add purchase options modal integration with redemption points support - Extract route helper for customer navigation with proper query params - Update checkout metadata service constants with proper namespacing - Add reward context initialization for tab metadata - Improve component styling and layout for reward action buttons - Fix customer facade method signature to require AbortSignal parameter The reward catalog now supports full customer workflow from selection through purchase options with proper state management and navigation. Ref: #5263, #5358 --- .../purchase-options-list-item.component.ts | 2 +- .../purchase-options-modal.service.ts | 5 +- ...s-main-view-billing-addresses.component.ts | 105 +++++++++--- ...-main-view-delivery-addresses.component.ts | 162 +++++++++++++----- .../checkout/data-access/src/lib/constants.ts | 7 +- .../src/lib/facades/shopping-cart.facade.ts | 4 + ...dd-items-to-shopping-cart-params.schema.ts | 2 +- .../lib/services/checkout-metadata.service.ts | 8 +- .../helpers/get-route-to-customer.helper.ts | 14 ++ .../reward-catalog/src/lib/helpers/index.ts | 1 + .../reward-catalog/src/lib/resources/index.ts | 1 + .../reward-customer-card.resource.ts | 16 +- .../reward-shopping-cart.resource.ts | 28 +++ .../reward-action/reward-action.component.css | 3 + .../reward-action.component.html | 5 +- .../reward-action/reward-action.component.ts | 81 ++++++++- .../src/lib/reward-catalog.component.css | 3 + .../src/lib/reward-catalog.component.ts | 15 ++ .../reward-customer-card.component.html | 24 ++- .../reward-customer-card.component.ts | 53 ++++-- .../reward-header.component.html | 5 +- .../reward-header/reward-header.component.ts | 32 ++-- .../reward-start-card.component.html | 5 +- .../reward-start-card.component.ts | 30 +--- libs/core/tabs/src/lib/helpers.ts | 1 - libs/crm/data-access/src/index.ts | 1 + libs/crm/data-access/src/lib/constants.ts | 4 + .../src/lib/facades/customer.facade.ts | 2 +- .../facades/selected-customer-id.facade.ts | 8 +- .../lib/services/crm-tab-metadata.service.ts | 43 ++++- 30 files changed, 507 insertions(+), 163 deletions(-) create mode 100644 libs/checkout/feature/reward-catalog/src/lib/helpers/get-route-to-customer.helper.ts create mode 100644 libs/checkout/feature/reward-catalog/src/lib/helpers/index.ts create mode 100644 libs/checkout/feature/reward-catalog/src/lib/resources/reward-shopping-cart.resource.ts diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts b/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts index 3ee5d58ba..ab6f61d40 100644 --- a/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts +++ b/apps/isa-app/src/modal/purchase-options/purchase-options-list-item/purchase-options-list-item.component.ts @@ -282,7 +282,7 @@ export class PurchaseOptionsListItemComponent return ( this.useRedemptionPoints() && this.isReservePurchaseOption() && - this.availability().inStock < 2 + (!this.availability() || this.availability().inStock < 2) ); }); diff --git a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts index c5a1030ae..686117338 100644 --- a/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts +++ b/apps/isa-app/src/modal/purchase-options/purchase-options-modal.service.ts @@ -44,6 +44,9 @@ export class PurchaseOptionsModalService { return Promise.resolve(undefined); } - return this.#customerFacade.fetchCustomer({ customerId }); + return this.#customerFacade.fetchCustomer( + { customerId }, + new AbortController().signal, + ); } } diff --git a/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view-billing-addresses/details-main-view-billing-addresses.component.ts b/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view-billing-addresses/details-main-view-billing-addresses.component.ts index 53bc22cfe..5bcce318f 100644 --- a/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view-billing-addresses/details-main-view-billing-addresses.component.ts +++ b/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view-billing-addresses/details-main-view-billing-addresses.component.ts @@ -1,9 +1,20 @@ -import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, Host, inject } from '@angular/core'; +import { + Component, + ChangeDetectionStrategy, + OnInit, + OnDestroy, + Host, + inject, +} from '@angular/core'; import { CustomerSearchStore } from '../../store'; import { CrmCustomerService } from '@domain/crm'; import { debounceTime, map, switchMap, takeUntil } from 'rxjs/operators'; import { Observable, Subject, combineLatest } from 'rxjs'; -import { AssignedPayerDTO, CustomerDTO, ListResponseArgsOfAssignedPayerDTO } from '@generated/swagger/crm-api'; +import { + AssignedPayerDTO, + CustomerDTO, + ListResponseArgsOfAssignedPayerDTO, +} from '@generated/swagger/crm-api'; import { AsyncPipe } from '@angular/common'; import { CustomerPipesModule } from '@shared/pipes/customer'; import { ComponentStore } from '@ngrx/component-store'; @@ -14,6 +25,8 @@ import { CustomerSearchNavigation } from '@shared/services/navigation'; import { RouterLink } from '@angular/router'; import { CustomerDetailsViewMainComponent } from '../details-main-view.component'; import { PayerDTO } from '@generated/swagger/checkout-api'; +import { CrmTabMetadataService } from '@isa/crm/data-access'; +import { injectTabId } from '@isa/core/tabs'; interface DetailsMainViewBillingAddressesComponentState { assignedPayers: AssignedPayerDTO[]; @@ -32,6 +45,9 @@ export class DetailsMainViewBillingAddressesComponent extends ComponentStore implements OnInit, OnDestroy { + tabId = injectTabId(); + crmTabMetadataService = inject(CrmTabMetadataService); + private _host = inject(CustomerDetailsViewMainComponent, { host: true }); private _store = inject(CustomerSearchStore); private _customerService = inject(CrmCustomerService); @@ -42,13 +58,20 @@ export class DetailsMainViewBillingAddressesComponent selectedPayer$ = this.select((state) => state.selectedPayer); - isNotBusinessKonto$ = this._store.isBusinessKonto$.pipe(map((isBusinessKonto) => !isBusinessKonto)); + isNotBusinessKonto$ = this._store.isBusinessKonto$.pipe( + map((isBusinessKonto) => !isBusinessKonto), + ); showCustomerAddress$ = combineLatest([ this._store.isBusinessKonto$, this._store.isMitarbeiter$, this._store.isKundenkarte$, - ]).pipe(map(([isBusinessKonto, isMitarbeiter, isKundenkarte]) => isBusinessKonto || isMitarbeiter || isKundenkarte)); + ]).pipe( + map( + ([isBusinessKonto, isMitarbeiter, isKundenkarte]) => + isBusinessKonto || isMitarbeiter || isKundenkarte, + ), + ); get showCustomerAddress() { return this._store.isBusinessKonto || this._store.isMitarbeiter; @@ -65,14 +88,22 @@ export class DetailsMainViewBillingAddressesComponent ), ); - canEditAddress$ = combineLatest([this._store.isKundenkarte$]).pipe(map(([isKundenkarte]) => isKundenkarte)); + canEditAddress$ = combineLatest([this._store.isKundenkarte$]).pipe( + map(([isKundenkarte]) => isKundenkarte), + ); customer$ = this._store.customer$; private _onDestroy$ = new Subject(); - editRoute$ = combineLatest([this._store.processId$, this._store.customerId$, this._store.isBusinessKonto$]).pipe( - map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b })), + editRoute$ = combineLatest([ + this._store.processId$, + this._store.customerId$, + this._store.isBusinessKonto$, + ]).pipe( + map(([processId, customerId, isB2b]) => + this._navigation.editRoute({ processId, customerId, isB2b }), + ), ); addBillingAddressRoute$ = combineLatest([ @@ -81,7 +112,9 @@ export class DetailsMainViewBillingAddressesComponent this._store.customerId$, ]).pipe( map(([canAddNewAddress, processId, customerId]) => - canAddNewAddress ? this._navigation.addBillingAddressRoute({ processId, customerId }) : undefined, + canAddNewAddress + ? this._navigation.addBillingAddressRoute({ processId, customerId }) + : undefined, ), ); @@ -116,6 +149,10 @@ export class DetailsMainViewBillingAddressesComponent .subscribe(([selectedPayer, customer]) => { if (selectedPayer) { this._host.setPayer(this._createPayerFromCrmPayerDTO(selectedPayer)); + this.crmTabMetadataService.setSelectedPayerAddressId( + this.tabId(), + selectedPayer?.payer?.id, + ); } else if (this.showCustomerAddress) { this._host.setPayer(this._createPayerFormCustomer(customer)); } @@ -133,7 +170,9 @@ export class DetailsMainViewBillingAddressesComponent title: payer.title, firstName: payer.firstName, lastName: payer.lastName, - communicationDetails: payer.communicationDetails ? { ...payer.communicationDetails } : undefined, + communicationDetails: payer.communicationDetails + ? { ...payer.communicationDetails } + : undefined, organisation: payer.organisation ? { ...payer.organisation } : undefined, address: payer.address ? { ...payer.address } : undefined, source: payer.id, @@ -150,8 +189,12 @@ export class DetailsMainViewBillingAddressesComponent title: customer.title, firstName: customer.firstName, lastName: customer.lastName, - communicationDetails: customer.communicationDetails ? { ...customer.communicationDetails } : undefined, - organisation: customer.organisation ? { ...customer.organisation } : undefined, + communicationDetails: customer.communicationDetails + ? { ...customer.communicationDetails } + : undefined, + organisation: customer.organisation + ? { ...customer.organisation } + : undefined, address: customer.address ? { ...customer.address } : undefined, }; } @@ -166,26 +209,36 @@ export class DetailsMainViewBillingAddressesComponent switchMap((customerId) => this._customerService .getAssignedPayers({ customerId }) - .pipe(tapResponse(this.handleLoadAssignedPayersResponse, this.handleLoadAssignedPayersError)), + .pipe( + tapResponse( + this.handleLoadAssignedPayersResponse, + this.handleLoadAssignedPayersError, + ), + ), ), ), ); - handleLoadAssignedPayersResponse = (response: ListResponseArgsOfAssignedPayerDTO) => { - const selectedPayer = response.result.reduce((prev, curr) => { - if (!prev) { + handleLoadAssignedPayersResponse = ( + response: ListResponseArgsOfAssignedPayerDTO, + ) => { + const selectedPayer = response.result.reduce( + (prev, curr) => { + if (!prev) { + return curr; + } + + const prevDate = new Date(prev?.isDefault ?? 0); + const currDate = new Date(curr?.isDefault ?? 0); + + if (prevDate > currDate) { + return prev; + } + return curr; - } - - const prevDate = new Date(prev?.isDefault ?? 0); - const currDate = new Date(curr?.isDefault ?? 0); - - if (prevDate > currDate) { - return prev; - } - - return curr; - }, undefined); + }, + undefined, + ); this.patchState({ assignedPayers: response.result, diff --git a/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view-delivery-addresses/details-main-view-delivery-addresses.component.ts b/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view-delivery-addresses/details-main-view-delivery-addresses.component.ts index c00d6f20e..3b6c8fd64 100644 --- a/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view-delivery-addresses/details-main-view-delivery-addresses.component.ts +++ b/apps/isa-app/src/page/customer/customer-search/details-main-view/details-main-view-delivery-addresses/details-main-view-delivery-addresses.component.ts @@ -1,9 +1,20 @@ -import { Component, ChangeDetectionStrategy, OnInit, OnDestroy, Host, inject } from '@angular/core'; +import { + Component, + ChangeDetectionStrategy, + OnInit, + OnDestroy, + Host, + inject, +} from '@angular/core'; import { CustomerSearchStore } from '../../store'; import { CrmCustomerService } from '@domain/crm'; import { map, switchMap, takeUntil } from 'rxjs/operators'; import { Observable, Subject, combineLatest } from 'rxjs'; -import { CustomerDTO, ListResponseArgsOfAssignedPayerDTO, ShippingAddressDTO } from '@generated/swagger/crm-api'; +import { + CustomerDTO, + ListResponseArgsOfAssignedPayerDTO, + ShippingAddressDTO, +} from '@generated/swagger/crm-api'; import { AsyncPipe } from '@angular/common'; import { CustomerPipesModule } from '@shared/pipes/customer'; import { ComponentStore } from '@ngrx/component-store'; @@ -13,6 +24,8 @@ import { UiModalService } from '@ui/modal'; import { CustomerSearchNavigation } from '@shared/services/navigation'; import { RouterLink } from '@angular/router'; import { CustomerDetailsViewMainComponent } from '../details-main-view.component'; +import { CrmTabMetadataService } from '@isa/crm/data-access'; +import { injectTabId } from '@isa/core/tabs'; interface DetailsMainViewDeliveryAddressesComponentState { shippingAddresses: ShippingAddressDTO[]; @@ -31,6 +44,9 @@ export class DetailsMainViewDeliveryAddressesComponent extends ComponentStore implements OnInit, OnDestroy { + tabId = injectTabId(); + crmTabMetadataService = inject(CrmTabMetadataService); + private _host = inject(CustomerDetailsViewMainComponent, { host: true }); private _store = inject(CustomerSearchStore); private _customerService = inject(CrmCustomerService); @@ -39,7 +55,9 @@ export class DetailsMainViewDeliveryAddressesComponent shippingAddresses$ = this.select((state) => state.shippingAddresses); - selectedShippingAddress$ = this.select((state) => state.selectedShippingAddress); + selectedShippingAddress$ = this.select( + (state) => state.selectedShippingAddress, + ); get selectedShippingAddress() { return this.get((s) => s.selectedShippingAddress); @@ -51,7 +69,12 @@ export class DetailsMainViewDeliveryAddressesComponent this._store.isBusinessKonto$, this._store.isMitarbeiter$, this._store.isKundenkarte$, - ]).pipe(map(([isBusinessKonto, isMitarbeiter, isKundenkarte]) => isBusinessKonto || isMitarbeiter || isKundenkarte)); + ]).pipe( + map( + ([isBusinessKonto, isMitarbeiter, isKundenkarte]) => + isBusinessKonto || isMitarbeiter || isKundenkarte, + ), + ); get showCustomerAddress() { return this._store.isBusinessKonto || this._store.isMitarbeiter; @@ -67,13 +90,29 @@ export class DetailsMainViewDeliveryAddressesComponent this._store.isMitarbeiter$, ]).pipe( map( - ([isOnlinekonto, isOnlineKontoMitKundenkarte, isKundenkarte, isBusinessKonto, isMitarbeiter]) => - isOnlinekonto || isOnlineKontoMitKundenkarte || isKundenkarte || isBusinessKonto || isMitarbeiter, + ([ + isOnlinekonto, + isOnlineKontoMitKundenkarte, + isKundenkarte, + isBusinessKonto, + isMitarbeiter, + ]) => + isOnlinekonto || + isOnlineKontoMitKundenkarte || + isKundenkarte || + isBusinessKonto || + isMitarbeiter, ), ); - editRoute$ = combineLatest([this._store.processId$, this._store.customerId$, this._store.isBusinessKonto$]).pipe( - map(([processId, customerId, isB2b]) => this._navigation.editRoute({ processId, customerId, isB2b })), + editRoute$ = combineLatest([ + this._store.processId$, + this._store.customerId$, + this._store.isBusinessKonto$, + ]).pipe( + map(([processId, customerId, isB2b]) => + this._navigation.editRoute({ processId, customerId, isB2b }), + ), ); addShippingAddressRoute$ = combineLatest([ @@ -82,15 +121,25 @@ export class DetailsMainViewDeliveryAddressesComponent this._store.customerId$, ]).pipe( map(([canAddNewAddress, processId, customerId]) => - canAddNewAddress ? this._navigation.addShippingAddressRoute({ processId, customerId }) : undefined, + canAddNewAddress + ? this._navigation.addShippingAddressRoute({ processId, customerId }) + : undefined, ), ); editShippingAddressRoute$ = (shippingAddressId: number) => - combineLatest([this.canEditAddress$, this._store.processId$, this._store.customerId$]).pipe( + combineLatest([ + this.canEditAddress$, + this._store.processId$, + this._store.customerId$, + ]).pipe( map(([canEditAddress, processId, customerId]) => { if (canEditAddress) { - return this._navigation.editShippingAddressRoute({ processId, customerId, shippingAddressId }); + return this._navigation.editShippingAddressRoute({ + processId, + customerId, + shippingAddressId, + }); } return undefined; }), @@ -100,7 +149,12 @@ export class DetailsMainViewDeliveryAddressesComponent this._store.isKundenkarte$, this._store.isBusinessKonto$, this._store.isMitarbeiter$, - ]).pipe(map(([isKundenkarte, isBusinessKonto, isMitarbeiter]) => isKundenkarte || isBusinessKonto || isMitarbeiter)); + ]).pipe( + map( + ([isKundenkarte, isBusinessKonto, isMitarbeiter]) => + isKundenkarte || isBusinessKonto || isMitarbeiter, + ), + ); constructor() { super({ @@ -110,20 +164,32 @@ export class DetailsMainViewDeliveryAddressesComponent } ngOnInit() { - this._store.customerId$.pipe(takeUntil(this._onDestroy$)).subscribe((customerId) => { - this.resetStore(); - if (customerId) { - this.loadShippingAddresses(customerId); - } - }); + this._store.customerId$ + .pipe(takeUntil(this._onDestroy$)) + .subscribe((customerId) => { + this.resetStore(); + if (customerId) { + this.loadShippingAddresses(customerId); + } + }); combineLatest([this.selectedShippingAddress$, this._store.customer$]) .pipe(takeUntil(this._onDestroy$)) .subscribe(([selectedShippingAddress, customer]) => { if (selectedShippingAddress) { - this._host.setShippingAddress(this._createShippingAddressFromShippingAddress(selectedShippingAddress)); + this._host.setShippingAddress( + this._createShippingAddressFromShippingAddress( + selectedShippingAddress, + ), + ); + this.crmTabMetadataService.setSelectedShippingAddressId( + this.tabId(), + selectedShippingAddress?.id, + ); } else if (this.showCustomerAddress) { - this._host.setShippingAddress(this._createShippingAddressFromCustomer(customer)); + this._host.setShippingAddress( + this._createShippingAddressFromCustomer(customer), + ); } }); } @@ -140,8 +206,12 @@ export class DetailsMainViewDeliveryAddressesComponent title: customer?.title, firstName: customer?.firstName, lastName: customer?.lastName, - communicationDetails: customer?.communicationDetails ? { ...customer?.communicationDetails } : undefined, - organisation: customer?.organisation ? { ...customer?.organisation } : undefined, + communicationDetails: customer?.communicationDetails + ? { ...customer?.communicationDetails } + : undefined, + organisation: customer?.organisation + ? { ...customer?.organisation } + : undefined, address: customer?.address ? { ...customer?.address } : undefined, }; } @@ -153,8 +223,12 @@ export class DetailsMainViewDeliveryAddressesComponent title: address.title, firstName: address.firstName, lastName: address.lastName, - communicationDetails: address.communicationDetails ? { ...address.communicationDetails } : undefined, - organisation: address.organisation ? { ...address.organisation } : undefined, + communicationDetails: address.communicationDetails + ? { ...address.communicationDetails } + : undefined, + organisation: address.organisation + ? { ...address.organisation } + : undefined, address: address.address ? { ...address.address } : undefined, source: address.id, }; @@ -165,26 +239,36 @@ export class DetailsMainViewDeliveryAddressesComponent switchMap((customerId) => this._customerService .getShippingAddresses({ customerId }) - .pipe(tapResponse(this.handleLoadShippingAddressesResponse, this.handleLoadAssignedPayersError)), + .pipe( + tapResponse( + this.handleLoadShippingAddressesResponse, + this.handleLoadAssignedPayersError, + ), + ), ), ), ); - handleLoadShippingAddressesResponse = (response: ListResponseArgsOfAssignedPayerDTO) => { - const selectedShippingAddress = response.result.reduce((prev, curr) => { - if (!this.showCustomerAddress && !prev) { + handleLoadShippingAddressesResponse = ( + response: ListResponseArgsOfAssignedPayerDTO, + ) => { + const selectedShippingAddress = response.result.reduce( + (prev, curr) => { + if (!this.showCustomerAddress && !prev) { + return curr; + } + + const prevDate = new Date(prev?.isDefault ?? 0); + const currDate = new Date(curr?.isDefault ?? 0); + + if (prevDate > currDate) { + return prev; + } + return curr; - } - - const prevDate = new Date(prev?.isDefault ?? 0); - const currDate = new Date(curr?.isDefault ?? 0); - - if (prevDate > currDate) { - return prev; - } - - return curr; - }, undefined); + }, + undefined, + ); this.patchState({ shippingAddresses: response.result, diff --git a/libs/checkout/data-access/src/lib/constants.ts b/libs/checkout/data-access/src/lib/constants.ts index 8a00b25c3..70b96993e 100644 --- a/libs/checkout/data-access/src/lib/constants.ts +++ b/libs/checkout/data-access/src/lib/constants.ts @@ -1,7 +1,8 @@ -export const SELECTED_BRANCH_METADATA_KEY = 'CHECKOUT_SELECTED_BRANCH_ID'; +export const SELECTED_BRANCH_METADATA_KEY = + 'checkout-data-access.checkoutSelectedBranchId'; export const CHECKOUT_SHOPPING_CART_ID_METADATA_KEY = - 'CHECKOUT_SHOPPING_CART_ID'; + 'checkout-data-access.checkoutShoppingCartId'; export const CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY = - 'CHECKOUT_REWARD_SHOPPING_CART_ID'; + 'checkout-data-access.checkoutRewardShoppingCartId'; diff --git a/libs/checkout/data-access/src/lib/facades/shopping-cart.facade.ts b/libs/checkout/data-access/src/lib/facades/shopping-cart.facade.ts index 47c6c6730..333c7d402 100644 --- a/libs/checkout/data-access/src/lib/facades/shopping-cart.facade.ts +++ b/libs/checkout/data-access/src/lib/facades/shopping-cart.facade.ts @@ -5,6 +5,10 @@ import { ShoppingCartService } from '../services'; export class ShoppingCartFacade { #shoppingCartService = inject(ShoppingCartService); + createShoppingCart() { + return this.#shoppingCartService.createShoppingCart(); + } + getShoppingCart(shoppingCartId: number, abortSignal?: AbortSignal) { return this.#shoppingCartService.getShoppingCart( shoppingCartId, diff --git a/libs/checkout/data-access/src/lib/schemas/can-add-items-to-shopping-cart-params.schema.ts b/libs/checkout/data-access/src/lib/schemas/can-add-items-to-shopping-cart-params.schema.ts index 631c819a5..b31ce816d 100644 --- a/libs/checkout/data-access/src/lib/schemas/can-add-items-to-shopping-cart-params.schema.ts +++ b/libs/checkout/data-access/src/lib/schemas/can-add-items-to-shopping-cart-params.schema.ts @@ -43,7 +43,7 @@ const CanAddOLAAvailabilitySchema = z.object({ const CanAddItemPayloadSchema = z.object({ availabilities: z.array(CanAddOLAAvailabilitySchema), - customerFeatures: z.record(z.string()), + customerFeatures: z.record(z.string().optional()), orderType: OrderTypeSchema, id: z.string(), }); diff --git a/libs/checkout/data-access/src/lib/services/checkout-metadata.service.ts b/libs/checkout/data-access/src/lib/services/checkout-metadata.service.ts index b709e3474..318dee365 100644 --- a/libs/checkout/data-access/src/lib/services/checkout-metadata.service.ts +++ b/libs/checkout/data-access/src/lib/services/checkout-metadata.service.ts @@ -22,7 +22,7 @@ export class CheckoutMetadataService { tabId, SELECTED_BRANCH_METADATA_KEY, z.number().optional(), - this.#tabService.entities(), + this.#tabService.entityMap(), ); } @@ -37,13 +37,13 @@ export class CheckoutMetadataService { tabId, CHECKOUT_SHOPPING_CART_ID_METADATA_KEY, z.number().optional(), - this.#tabService.entities(), + this.#tabService.entityMap(), ); } setRewardShoppingCartId(tabId: number, shoppingCartId: number | undefined) { this.#tabService.patchTabMetadata(tabId, { - CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY: shoppingCartId, + [CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY]: shoppingCartId, }); } @@ -52,7 +52,7 @@ export class CheckoutMetadataService { tabId, CHECKOUT_REWARD_SHOPPING_CART_ID_METADATA_KEY, z.number().optional(), - this.#tabService.entities(), + this.#tabService.entityMap(), ); } } diff --git a/libs/checkout/feature/reward-catalog/src/lib/helpers/get-route-to-customer.helper.ts b/libs/checkout/feature/reward-catalog/src/lib/helpers/get-route-to-customer.helper.ts new file mode 100644 index 000000000..108aaa5e0 --- /dev/null +++ b/libs/checkout/feature/reward-catalog/src/lib/helpers/get-route-to-customer.helper.ts @@ -0,0 +1,14 @@ +export const getRouteToCustomer = (tabId: number | null) => { + const path = [ + '/kunde', + tabId, + 'customer', + { outlets: { primary: 'search', side: 'search-customer-main' } }, + ].filter(Boolean); + + const queryParams = { + filter_customertype: 'webshop&loyalty;loyalty&!webshop', // Filter only Customer Card Customers + }; + + return { path, queryParams }; +}; diff --git a/libs/checkout/feature/reward-catalog/src/lib/helpers/index.ts b/libs/checkout/feature/reward-catalog/src/lib/helpers/index.ts new file mode 100644 index 000000000..897fa9b85 --- /dev/null +++ b/libs/checkout/feature/reward-catalog/src/lib/helpers/index.ts @@ -0,0 +1 @@ +export * from './get-route-to-customer.helper'; diff --git a/libs/checkout/feature/reward-catalog/src/lib/resources/index.ts b/libs/checkout/feature/reward-catalog/src/lib/resources/index.ts index 458225826..7e84cb91b 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/resources/index.ts +++ b/libs/checkout/feature/reward-catalog/src/lib/resources/index.ts @@ -1,2 +1,3 @@ export * from './reward-catalog.resource'; export * from './reward-customer-card.resource'; +export * from './reward-shopping-cart.resource'; diff --git a/libs/checkout/feature/reward-catalog/src/lib/resources/reward-customer-card.resource.ts b/libs/checkout/feature/reward-catalog/src/lib/resources/reward-customer-card.resource.ts index 269955490..dc8ddaaf3 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/resources/reward-customer-card.resource.ts +++ b/libs/checkout/feature/reward-catalog/src/lib/resources/reward-customer-card.resource.ts @@ -1,17 +1,15 @@ import { inject, resource } from '@angular/core'; -import { ResponseArgsError } from '@isa/common/data-access'; +import { injectTabId } from '@isa/core/tabs'; import { CustomerCardsFacade } from '@isa/crm/data-access'; +import { SelectedCustomerFacade } from '@isa/crm/data-access'; -export const createRewardCustomerCardResource = ( - params: () => { - customerId: number | undefined; - }, -) => { +export const createRewardCustomerCardResource = () => { + const tabId = injectTabId(); const customerCardsFacade = inject(CustomerCardsFacade); + const selectedCustomerFacade = inject(SelectedCustomerFacade); return resource({ - params, - loader: async ({ abortSignal, params }) => { - const { customerId } = params; + loader: async ({ abortSignal }) => { + const customerId = selectedCustomerFacade.get(tabId()!); if (!customerId) { return undefined; diff --git a/libs/checkout/feature/reward-catalog/src/lib/resources/reward-shopping-cart.resource.ts b/libs/checkout/feature/reward-catalog/src/lib/resources/reward-shopping-cart.resource.ts new file mode 100644 index 000000000..4f34b1e69 --- /dev/null +++ b/libs/checkout/feature/reward-catalog/src/lib/resources/reward-shopping-cart.resource.ts @@ -0,0 +1,28 @@ +import { inject, resource } from '@angular/core'; +import { + CheckoutMetadataService, + ShoppingCartFacade, +} from '@isa/checkout/data-access'; +import { injectTabId } from '@isa/core/tabs'; + +export const createRewardShoppingCartResource = () => { + const tabId = injectTabId(); + const checkoutMetadataService = inject(CheckoutMetadataService); + const shoppingCartFacade = inject(ShoppingCartFacade); + return resource({ + loader: async ({ abortSignal }) => { + const shoppingCartId = checkoutMetadataService.getRewardShoppingCartId( + tabId()!, + ); + + if (!shoppingCartId) { + return undefined; + } + + const fetchCustomerCardsResponse = + await shoppingCartFacade.getShoppingCart(shoppingCartId, abortSignal); + + return fetchCustomerCardsResponse; + }, + }); +}; diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.css b/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.css index e69de29bb..43a939bdb 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.css +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.css @@ -0,0 +1,3 @@ +:host { + @apply flex flex-col self-end fixed bottom-6; +} diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.html b/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.html index 9de3cd9e3..b28975cee 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.html +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-action/reward-action.component.html @@ -1,7 +1,6 @@ +} diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-customer-card/reward-customer-card.component.ts b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-customer-card/reward-customer-card.component.ts index b122da62a..7804a5323 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-customer-card/reward-customer-card.component.ts +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-customer-card/reward-customer-card.component.ts @@ -1,27 +1,60 @@ import { ChangeDetectionStrategy, Component, - inject, input, + linkedSignal, + output, } from '@angular/core'; -import { TextButtonComponent } from '@isa/ui/buttons'; -import { BonusCardInfo, SelectedCustomerFacade } from '@isa/crm/data-access'; -import { injectTabId } from '@isa/core/tabs'; +import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons'; +import { BonusCardInfo } from '@isa/crm/data-access'; +import { createRewardShoppingCartResource } from '../../resources'; +import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader'; @Component({ selector: 'reward-customer-card', templateUrl: './reward-customer-card.component.html', styleUrl: './reward-customer-card.component.css', changeDetection: ChangeDetectionStrategy.OnPush, - imports: [TextButtonComponent], + imports: [TextButtonComponent, ButtonComponent, SkeletonLoaderDirective], }) export class RewardCustomerCardComponent { - tabId = injectTabId(); - selectedCustomerFacade = inject(SelectedCustomerFacade); - card = input.required(); + reset = output(); - resetCustomer() { - this.selectedCustomerFacade.clear(this.tabId()!); + rewardShoppingCartResource = createRewardShoppingCartResource(); // TODO: Refactor and use global resource + + shoppingCartResponseValue = linkedSignal(() => + this.rewardShoppingCartResource.value(), + ); + + shoppingCartResponseFetching = linkedSignal( + () => this.rewardShoppingCartResource.status() === 'loading', + ); + + cartItemsLength = linkedSignal( + () => this.shoppingCartResponseValue()?.items?.length ?? 0, + ); + + cartTotalLoyaltyPoints = linkedSignal(() => { + return ( + this.shoppingCartResponseValue()?.items?.reduce( + (sum, item) => sum + (item?.data?.loyalty?.value ?? 0), + 0, + ) ?? 0 + ); + }); + + customerCardTotalPoints = linkedSignal(() => this.card()?.totalPoints ?? 0); + + disableContinueCta = linkedSignal(() => { + return ( + this.shoppingCartResponseFetching() || + this.customerCardTotalPoints() <= 0 || + this.cartTotalLoyaltyPoints() > this.customerCardTotalPoints() + ); + }); + + continueToRewardCheckout() { + // TODO: Navigate to Reward Checkout Page } } diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-header.component.html b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-header.component.html index 97d2d8f77..cc87ded5b 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-header.component.html +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-header.component.html @@ -2,7 +2,10 @@ @let cardFetching = customerCardResponseFetching(); @if (!cardFetching) { @if (card) { - + } @else { } diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-header.component.ts b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-header.component.ts index 0f859d1ce..1a0e3fd36 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-header.component.ts +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-header.component.ts @@ -5,11 +5,11 @@ import { linkedSignal, } from '@angular/core'; import { RewardStartCardComponent } from './reward-start-card/reward-start-card.component'; -import { injectTabId } from '@isa/core/tabs'; -import { SelectedCustomerFacade } from '@isa/crm/data-access'; import { RewardCustomerCardComponent } from './reward-customer-card/reward-customer-card.component'; import { createRewardCustomerCardResource } from '../resources'; import { IconButtonComponent } from '@isa/ui/buttons'; +import { injectTabId } from '@isa/core/tabs'; +import { SelectedCustomerFacade } from '@isa/crm/data-access'; @Component({ selector: 'reward-header', templateUrl: './reward-header.component.html', @@ -25,25 +25,15 @@ export class RewardHeaderComponent { tabId = injectTabId(); selectedCustomerFacade = inject(SelectedCustomerFacade); - selectedCustomerId = linkedSignal(() => - this.selectedCustomerFacade.get(this.tabId()!), - ); + rewardCustomerCardResource = createRewardCustomerCardResource(); // TODO: Refactor and use global resource - rewardCustomerCardResource = createRewardCustomerCardResource(() => { - return { - customerId: this.selectedCustomerId(), - }; - }); + readonly customerCardResponseValue = + this.rewardCustomerCardResource.value.asReadonly(); + readonly customerCardResponseFetching = + this.rewardCustomerCardResource.isLoading; - // TODO: Falls notwendig und die Karte allein nicht ausreicht, Customer Fetchen und überprüfen ob folgende features vorhanden sind: - // features?.some((c) => c.key === 'p4mUser') - // oder - // features?.some((c) => c.key === 'd-account' && c.description.includes('Kundenkarte') && c.enabled) - customerCardResponseValue = linkedSignal(() => - this.rewardCustomerCardResource.value(), - ); - - customerCardResponseFetching = linkedSignal( - () => this.rewardCustomerCardResource.status() === 'loading', - ); + resetCustomer() { + this.selectedCustomerFacade.clear(this.tabId()!); + this.rewardCustomerCardResource.reload(); + } } diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.html b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.html index 296dd45ae..fa7f28b97 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.html +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.html @@ -14,9 +14,8 @@ uiButton color="tertiary" size="large" - [routerLink]="pathToCrmSearch()" - [queryParams]="queryParams" - (click)="setTabContext()" + [routerLink]="route().path" + [queryParams]="route().queryParams" > Kund*in auswählen diff --git a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.ts b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.ts index 508256ae4..c5674dedf 100644 --- a/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.ts +++ b/libs/checkout/feature/reward-catalog/src/lib/reward-header/reward-start-card/reward-start-card.component.ts @@ -1,12 +1,8 @@ -import { - ChangeDetectionStrategy, - Component, - linkedSignal, - inject, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, computed } from '@angular/core'; import { ButtonComponent } from '@isa/ui/buttons'; -import { injectTabId, TabService } from '@isa/core/tabs'; +import { injectTabId } from '@isa/core/tabs'; import { RouterLink } from '@angular/router'; +import { getRouteToCustomer } from '../../helpers'; @Component({ selector: 'reward-start-card', templateUrl: './reward-start-card.component.html', @@ -15,24 +11,6 @@ import { RouterLink } from '@angular/router'; imports: [ButtonComponent, RouterLink], }) export class RewardStartCardComponent { - tabService = inject(TabService); tabId = injectTabId(); - - pathToCrmSearch = linkedSignal(() => - [ - '/kunde', - this.tabId(), - 'customer', - { outlets: { primary: 'search', side: 'search-customer-main' } }, - ].filter(Boolean), - ); - - queryParams = { - filter_customertype: 'webshop&loyalty;loyalty&!webshop', // Filter only Customer Card Customers - }; - - // Wichtig damit Kundensuche weiß, dass wir im Reward Kontext sind - setTabContext() { - this.tabService.patchTabMetadata(this.tabId()!, { context: 'reward' }); - } + route = computed(() => getRouteToCustomer(this.tabId())); } diff --git a/libs/core/tabs/src/lib/helpers.ts b/libs/core/tabs/src/lib/helpers.ts index 3ea834e0a..6ea78bd41 100644 --- a/libs/core/tabs/src/lib/helpers.ts +++ b/libs/core/tabs/src/lib/helpers.ts @@ -1,6 +1,5 @@ import z from 'zod'; import { Tab } from './schemas'; -import { computed } from '@angular/core'; export function getTabHelper( tabId: number, diff --git a/libs/crm/data-access/src/index.ts b/libs/crm/data-access/src/index.ts index c41c42d09..c16b8f53a 100644 --- a/libs/crm/data-access/src/index.ts +++ b/libs/crm/data-access/src/index.ts @@ -1,3 +1,4 @@ export * from './lib/facades'; export * from './lib/constants'; export * from './lib/models'; +export * from './lib/services'; diff --git a/libs/crm/data-access/src/lib/constants.ts b/libs/crm/data-access/src/lib/constants.ts index 9c3fa64f1..95070fbe6 100644 --- a/libs/crm/data-access/src/lib/constants.ts +++ b/libs/crm/data-access/src/lib/constants.ts @@ -1 +1,5 @@ export const SELECTED_CUSTOMER_ID = 'crm-data-access.selectedCustomerId'; +export const SELECTED_SHIPPING_ADDRESS_ID = + 'crm-data-access.selectedShippingAddressId'; +export const SELECTED_PAYER_ADDRESS_ID = + 'crm-data-access.selectedPayerAddressId'; diff --git a/libs/crm/data-access/src/lib/facades/customer.facade.ts b/libs/crm/data-access/src/lib/facades/customer.facade.ts index c7003a4a1..9eeec127b 100644 --- a/libs/crm/data-access/src/lib/facades/customer.facade.ts +++ b/libs/crm/data-access/src/lib/facades/customer.facade.ts @@ -9,7 +9,7 @@ export class CustomerFacade { async fetchCustomer( params: FetchCustomerInput, - abortSignal?: AbortSignal, + abortSignal: AbortSignal, ): Promise { const res = await this.#customerService.fetchCustomer(params, abortSignal); return res.result; diff --git a/libs/crm/data-access/src/lib/facades/selected-customer-id.facade.ts b/libs/crm/data-access/src/lib/facades/selected-customer-id.facade.ts index c4fdae141..ab9bda257 100644 --- a/libs/crm/data-access/src/lib/facades/selected-customer-id.facade.ts +++ b/libs/crm/data-access/src/lib/facades/selected-customer-id.facade.ts @@ -9,11 +9,11 @@ export class SelectedCustomerFacade { this.#crmTabMetadataService.setSelectedCustomerId(tabId, customerId); } - get(tab: number) { - return this.#crmTabMetadataService.selectedCustomerId(tab); + get(tabId: number) { + return this.#crmTabMetadataService.selectedCustomerId(tabId); } - clear(tab: number) { - this.#crmTabMetadataService.setSelectedCustomerId(tab, undefined); + clear(tabId: number) { + this.#crmTabMetadataService.setSelectedCustomerId(tabId, undefined); } } diff --git a/libs/crm/data-access/src/lib/services/crm-tab-metadata.service.ts b/libs/crm/data-access/src/lib/services/crm-tab-metadata.service.ts index 954c9bb0a..a0dd3b844 100644 --- a/libs/crm/data-access/src/lib/services/crm-tab-metadata.service.ts +++ b/libs/crm/data-access/src/lib/services/crm-tab-metadata.service.ts @@ -1,6 +1,10 @@ import { Injectable, inject } from '@angular/core'; -import { TabService, getMetadataHelper, getTabHelper } from '@isa/core/tabs'; -import { SELECTED_CUSTOMER_ID } from '../constants'; +import { TabService, getMetadataHelper } from '@isa/core/tabs'; +import { + SELECTED_CUSTOMER_ID, + SELECTED_PAYER_ADDRESS_ID, + SELECTED_SHIPPING_ADDRESS_ID, +} from '../constants'; import z from 'zod'; @Injectable({ providedIn: 'root' }) @@ -12,7 +16,25 @@ export class CrmTabMetadataService { tabId, SELECTED_CUSTOMER_ID, z.number().optional(), - this.#tabService.entities(), + this.#tabService.entityMap(), + ); + } + + selectedShippingAddressId(tabId: number): number | undefined { + return getMetadataHelper( + tabId, + SELECTED_SHIPPING_ADDRESS_ID, + z.number().optional(), + this.#tabService.entityMap(), + ); + } + + selectedPayerAddressId(tabId: number): number | undefined { + return getMetadataHelper( + tabId, + SELECTED_PAYER_ADDRESS_ID, + z.number().optional(), + this.#tabService.entityMap(), ); } @@ -21,4 +43,19 @@ export class CrmTabMetadataService { [SELECTED_CUSTOMER_ID]: customerId, }); } + + setSelectedShippingAddressId( + tabId: number, + shippingAddressId: number | undefined, + ) { + this.#tabService.patchTabMetadata(tabId, { + [SELECTED_SHIPPING_ADDRESS_ID]: shippingAddressId, + }); + } + + setSelectedPayerAddressId(tabId: number, payerAddressId: number | undefined) { + this.#tabService.patchTabMetadata(tabId, { + [SELECTED_PAYER_ADDRESS_ID]: payerAddressId, + }); + } }