mirror of
https://dev.azure.com/hugendubel/ISA/_git/ISA-Frontend
synced 2025-12-28 22:42:11 +01:00
Merged PR 2015: fix(crm-data-access, customer-details, reward-shopping-cart): persist selecte...
fix(crm-data-access, customer-details, reward-shopping-cart): persist selected addresses across navigation flows Implement address selection persistence using CRM tab metadata to ensure selected shipping addresses and payers are retained throughout the customer selection flow, particularly when navigating from Kundenkarte to reward cart. Changes include: - Create PayerResource and CustomerPayerAddressResource to load selected payer from tab metadata with fallback to customer as payer - Create PayerService to fetch payer data from CRM API with proper error handling and abort signal support - Update BillingAndShippingAddressCardComponent to prefer selected addresses from metadata over customer defaults, with computed loading state - Refactor continue() flow in CustomerDetailsViewMainComponent to load selected addresses from metadata before setting in checkout service - Add adapter logic to convert CRM payer/shipping address types to checkout types with proper type casting for incompatible enum types - Implement fallback chain: metadata selection → component state → customer default for both payer and shipping address This ensures address selections made in the address selection dialogs are properly preserved and applied when completing the customer selection flow, fixing the issue where addresses would revert to customer defaults. Ref: #5411
This commit is contained in:
committed by
Lorenz Hilpert
parent
b984a2cac2
commit
c3e9a03169
@@ -36,13 +36,21 @@ import { CrmCustomerService } from '@domain/crm';
|
||||
import { MessageModalComponent, MessageModalData } from '@modal/message';
|
||||
import { GenderSettingsService } from '@shared/services/gender';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { CrmTabMetadataService, Customer } from '@isa/crm/data-access';
|
||||
import { CustomerAdapter } from '@isa/checkout/data-access';
|
||||
import {
|
||||
CrmTabMetadataService,
|
||||
Customer,
|
||||
AssignedPayer,
|
||||
} from '@isa/crm/data-access';
|
||||
import {
|
||||
CustomerAdapter,
|
||||
ShippingAddressAdapter,
|
||||
} from '@isa/checkout/data-access';
|
||||
import {
|
||||
NavigateAfterRewardSelection,
|
||||
RewardSelectionPopUpService,
|
||||
} from '@isa/checkout/shared/reward-selection-dialog';
|
||||
import { NavigationStateService } from '@isa/core/navigation';
|
||||
import { ShippingAddressDTO as CrmShippingAddressDTO } from '@generated/swagger/crm-api';
|
||||
|
||||
export interface CustomerDetailsViewMainState {
|
||||
isBusy: boolean;
|
||||
@@ -407,9 +415,9 @@ export class CustomerDetailsViewMainComponent
|
||||
|
||||
await this._updateNotifcationChannelsAsync(currentBuyer);
|
||||
|
||||
this._setPayer();
|
||||
await this._setPayer();
|
||||
|
||||
this._setShippingAddress();
|
||||
await this._setShippingAddress();
|
||||
|
||||
// #5262 Check for reward selection flow before navigation
|
||||
if (this.hasReturnUrl()) {
|
||||
@@ -631,8 +639,46 @@ export class CustomerDetailsViewMainComponent
|
||||
}
|
||||
}
|
||||
|
||||
@log
|
||||
_setPayer() {
|
||||
@logAsync
|
||||
async _setPayer() {
|
||||
// Check if there's a selected payer in metadata (from previous address selection)
|
||||
const selectedPayerId = this.crmTabMetadataService.selectedPayerId(
|
||||
this.processId,
|
||||
);
|
||||
|
||||
if (selectedPayerId) {
|
||||
// Load the selected payer from metadata
|
||||
try {
|
||||
const payerResponse = await this.customerService
|
||||
.getPayer(selectedPayerId)
|
||||
.toPromise();
|
||||
|
||||
if (payerResponse?.result) {
|
||||
// Create AssignedPayer structure expected by adapter
|
||||
// Type cast needed due to incompatible enum types between CRM and Checkout APIs
|
||||
const assignedPayer = {
|
||||
payer: {
|
||||
id: selectedPayerId,
|
||||
data: payerResponse.result,
|
||||
},
|
||||
} as AssignedPayer;
|
||||
|
||||
const payer = CustomerAdapter.toPayerFromAssignedPayer(assignedPayer);
|
||||
|
||||
if (payer) {
|
||||
this._checkoutService.setPayer({
|
||||
processId: this.processId,
|
||||
payer,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load selected payer from metadata', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to current payer from component state
|
||||
if (this.payer) {
|
||||
this._checkoutService.setPayer({
|
||||
processId: this.processId,
|
||||
@@ -641,8 +687,41 @@ export class CustomerDetailsViewMainComponent
|
||||
}
|
||||
}
|
||||
|
||||
@log
|
||||
_setShippingAddress() {
|
||||
@logAsync
|
||||
async _setShippingAddress() {
|
||||
// Check if there's a selected shipping address in metadata (from previous address selection)
|
||||
const selectedShippingAddressId =
|
||||
this.crmTabMetadataService.selectedShippingAddressId(this.processId);
|
||||
|
||||
if (selectedShippingAddressId) {
|
||||
// Load the selected shipping address from metadata
|
||||
try {
|
||||
const addressResponse = await this.customerService
|
||||
.getShippingAddress(selectedShippingAddressId)
|
||||
.toPromise();
|
||||
|
||||
if (addressResponse?.result) {
|
||||
const shippingAddress = ShippingAddressAdapter.fromCrmShippingAddress(
|
||||
addressResponse.result as CrmShippingAddressDTO,
|
||||
);
|
||||
|
||||
if (shippingAddress) {
|
||||
this._checkoutService.setShippingAddress({
|
||||
processId: this.processId,
|
||||
shippingAddress,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Failed to load selected shipping address from metadata',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to current shipping address from component state
|
||||
if (this.shippingAddress) {
|
||||
this._checkoutService.setShippingAddress({
|
||||
processId: this.processId,
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
import {
|
||||
SelectedCustomerResource,
|
||||
getCustomerName,
|
||||
SelectedCustomerShippingAddressResource,
|
||||
SelectedCustomerPayerAddressResource,
|
||||
} from '@isa/crm/data-access';
|
||||
import { isaActionEdit } from '@isa/icons';
|
||||
import { IconButtonComponent } from '@isa/ui/buttons';
|
||||
@@ -25,10 +27,19 @@ import { NavigationStateService } from '@isa/core/navigation';
|
||||
})
|
||||
export class BillingAndShippingAddressCardComponent {
|
||||
#navigationState = inject(NavigationStateService);
|
||||
#shippingAddressResource = inject(SelectedCustomerShippingAddressResource);
|
||||
#payerAddressResource = inject(SelectedCustomerPayerAddressResource);
|
||||
|
||||
tabId = injectTabId();
|
||||
#customerResource = inject(SelectedCustomerResource).resource;
|
||||
|
||||
isLoading = this.#customerResource.isLoading;
|
||||
isLoading = computed(() => {
|
||||
return (
|
||||
this.#customerResource.isLoading() ||
|
||||
this.#shippingAddressResource.resource.isLoading() ||
|
||||
this.#payerAddressResource.resource.isLoading()
|
||||
);
|
||||
});
|
||||
|
||||
customer = computed(() => {
|
||||
return this.#customerResource.value();
|
||||
@@ -49,26 +60,44 @@ export class BillingAndShippingAddressCardComponent {
|
||||
}
|
||||
|
||||
payer = computed(() => {
|
||||
// Prefer selected payer from metadata over customer as payer
|
||||
const selectedPayer = this.#payerAddressResource.resource.value();
|
||||
if (selectedPayer) {
|
||||
return selectedPayer;
|
||||
}
|
||||
// Fallback to customer as payer
|
||||
return this.customer();
|
||||
});
|
||||
|
||||
payerName = computed(() => {
|
||||
return getCustomerName(this.payer());
|
||||
const payer = this.payer();
|
||||
return getCustomerName(payer);
|
||||
});
|
||||
|
||||
payerAddress = computed(() => {
|
||||
return this.customer()?.address;
|
||||
const payer = this.payer();
|
||||
if (!payer) return undefined;
|
||||
return payer.address;
|
||||
});
|
||||
|
||||
shippingAddress = computed(() => {
|
||||
// Prefer selected shipping address from metadata over customer default
|
||||
const selectedAddress = this.#shippingAddressResource.resource.value();
|
||||
if (selectedAddress) {
|
||||
return selectedAddress;
|
||||
}
|
||||
// Fallback to customer
|
||||
return this.customer();
|
||||
});
|
||||
|
||||
shippingName = computed(() => {
|
||||
return getCustomerName(this.shippingAddress());
|
||||
const shipping = this.shippingAddress();
|
||||
return getCustomerName(shipping);
|
||||
});
|
||||
|
||||
shippingAddressAddress = computed(() => {
|
||||
return this.shippingAddress()?.address;
|
||||
const shipping = this.shippingAddress();
|
||||
if (!shipping) return undefined;
|
||||
return shipping.address;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { effect, inject, Injectable, resource, signal } from '@angular/core';
|
||||
import { CrmTabMetadataService, PayerService } from '../services';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { CrmPayer } from '../schemas';
|
||||
|
||||
@Injectable()
|
||||
export class CustomerPayerAddressResource {
|
||||
#payerService = inject(PayerService);
|
||||
|
||||
#params = signal<{
|
||||
payerId: number | undefined;
|
||||
}>({
|
||||
payerId: undefined,
|
||||
});
|
||||
|
||||
params(params: { payerId?: number }) {
|
||||
this.#params.update((p) => ({ ...p, ...params }));
|
||||
}
|
||||
|
||||
readonly resource = resource({
|
||||
params: () => this.#params(),
|
||||
loader: async ({ params, abortSignal }): Promise<CrmPayer | undefined> => {
|
||||
if (!params.payerId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const res = await this.#payerService.fetchPayer(
|
||||
{
|
||||
payerId: params.payerId,
|
||||
},
|
||||
abortSignal,
|
||||
);
|
||||
|
||||
return res.result as CrmPayer;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SelectedCustomerPayerAddressResource extends CustomerPayerAddressResource {
|
||||
#tabId = inject(TabService).activatedTabId;
|
||||
#customerMetadata = inject(CrmTabMetadataService);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
effect(() => {
|
||||
const tabId = this.#tabId();
|
||||
const payerId = tabId
|
||||
? this.#customerMetadata.selectedPayerId(tabId)
|
||||
: undefined;
|
||||
this.params({ payerId });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './country.resource';
|
||||
export * from './customer-payer-address.resource';
|
||||
export * from './primary-customer-card.resource';
|
||||
export * from './customer-shipping-address.resource';
|
||||
export * from './customer-shipping-addresses.resource';
|
||||
export * from './customer.resource';
|
||||
export * from './payer.resource';
|
||||
|
||||
52
libs/crm/data-access/src/lib/resources/payer.resource.ts
Normal file
52
libs/crm/data-access/src/lib/resources/payer.resource.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { effect, inject, Injectable, resource, signal } from '@angular/core';
|
||||
import { CrmTabMetadataService } from '../services';
|
||||
import { TabService } from '@isa/core/tabs';
|
||||
import { CrmCustomerService } from '@domain/crm';
|
||||
import { PayerDTO } from '@generated/swagger/crm-api';
|
||||
|
||||
@Injectable()
|
||||
export class PayerResource {
|
||||
#customerService = inject(CrmCustomerService);
|
||||
|
||||
#params = signal<{
|
||||
payerId: number | undefined;
|
||||
}>({
|
||||
payerId: undefined,
|
||||
});
|
||||
|
||||
params(params: { payerId?: number }) {
|
||||
this.#params.update((p) => ({ ...p, ...params }));
|
||||
}
|
||||
|
||||
readonly resource = resource({
|
||||
params: () => this.#params(),
|
||||
loader: async ({ params, abortSignal }): Promise<PayerDTO | undefined> => {
|
||||
if (!params.payerId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const res = await this.#customerService
|
||||
.getPayer(params.payerId)
|
||||
.toPromise();
|
||||
|
||||
return res?.result;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SelectedPayerResource extends PayerResource {
|
||||
#tabId = inject(TabService).activatedTabId;
|
||||
#customerMetadata = inject(CrmTabMetadataService);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
effect(() => {
|
||||
const tabId = this.#tabId();
|
||||
const payerId = tabId
|
||||
? this.#customerMetadata.selectedPayerId(tabId)
|
||||
: undefined;
|
||||
this.params({ payerId });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const FetchPayerSchema = z.object({
|
||||
payerId: z.number().int().describe('Payer identifier'),
|
||||
});
|
||||
|
||||
export type FetchPayer = z.infer<typeof FetchPayerSchema>;
|
||||
export type FetchPayerInput = z.input<typeof FetchPayerSchema>;
|
||||
@@ -8,6 +8,7 @@ export * from './customer-feature-groups.schema';
|
||||
export * from './fetch-customer-cards.schema';
|
||||
export * from './fetch-customer-shipping-addresses.schema';
|
||||
export * from './fetch-customer.schema';
|
||||
export * from './fetch-payer.schema';
|
||||
export * from './fetch-shipping-address.schema';
|
||||
export * from './linked-record.schema';
|
||||
export * from './notification-channel.schema';
|
||||
|
||||
@@ -16,23 +16,43 @@ export const PayerSchema = z
|
||||
.object({
|
||||
address: AddressSchema.describe('Address').optional(),
|
||||
agentComment: z.string().describe('Agent comment').optional(),
|
||||
communicationDetails: CommunicationDetailsSchema.describe('Communication details').optional(),
|
||||
communicationDetails: CommunicationDetailsSchema.describe(
|
||||
'Communication details',
|
||||
).optional(),
|
||||
deactivationComment: z.string().describe('Deactivation comment').optional(),
|
||||
defaultPaymentPeriod: z.number().describe('Default payment period').optional(),
|
||||
defaultPaymentPeriod: z
|
||||
.number()
|
||||
.describe('Default payment period')
|
||||
.optional(),
|
||||
firstName: z.string().describe('First name').optional(),
|
||||
gender: GenderSchema.describe('Gender').optional(),
|
||||
isGuestAccount: z.boolean().describe('Whether guestAccount').optional(),
|
||||
label: EntityContainerSchema(LabelSchema).describe('Label').optional(),
|
||||
lastName: z.string().describe('Last name').optional(),
|
||||
organisation: OrganisationSchema.describe('Organisation information').optional(),
|
||||
organisation: OrganisationSchema.describe(
|
||||
'Organisation information',
|
||||
).optional(),
|
||||
payerGroup: z.string().describe('Payer group').optional(),
|
||||
payerNumber: z.string().describe('Unique payer account number').optional(),
|
||||
payerStatus: PayerStatusSchema.describe('Current status of the payer account').optional(),
|
||||
payerStatus: PayerStatusSchema.describe(
|
||||
'Current status of the payer account',
|
||||
).optional(),
|
||||
payerType: z.nativeEnum(PayerType).describe('Payer type').optional(),
|
||||
paymentTypes: z.array(PaymentSettingsSchema).describe('Payment types').optional(),
|
||||
standardInvoiceText: z.string().describe('Standard invoice text').optional(),
|
||||
statusChangeComment: z.string().describe('Status change comment').optional(),
|
||||
paymentTypes: z
|
||||
.array(PaymentSettingsSchema)
|
||||
.describe('Payment types')
|
||||
.optional(),
|
||||
standardInvoiceText: z
|
||||
.string()
|
||||
.describe('Standard invoice text')
|
||||
.optional(),
|
||||
statusChangeComment: z
|
||||
.string()
|
||||
.describe('Status change comment')
|
||||
.optional(),
|
||||
statusComment: z.string().describe('Status comment').optional(),
|
||||
title: z.string().describe('Title').optional(),
|
||||
})
|
||||
.extend(EntitySchema.shape);
|
||||
|
||||
export type CrmPayer = z.infer<typeof PayerSchema>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './country.service';
|
||||
export * from './crm-search.service';
|
||||
export * from './crm-tab-metadata.service';
|
||||
export * from './payer.service';
|
||||
export * from './shipping-address.service';
|
||||
|
||||
43
libs/crm/data-access/src/lib/services/payer.service.ts
Normal file
43
libs/crm/data-access/src/lib/services/payer.service.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { PayerService as GeneratedPayerService } from '@generated/swagger/crm-api';
|
||||
import {
|
||||
catchResponseArgsErrorPipe,
|
||||
ResponseArgs,
|
||||
takeUntilAborted,
|
||||
} from '@isa/common/data-access';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { logger } from '@isa/core/logging';
|
||||
import { FetchPayerInput, FetchPayerSchema, CrmPayer } from '../schemas';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PayerService {
|
||||
#payerService = inject(GeneratedPayerService);
|
||||
#logger = logger(() => ({
|
||||
service: 'PayerService',
|
||||
}));
|
||||
|
||||
async fetchPayer(
|
||||
params: FetchPayerInput,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ResponseArgs<CrmPayer>> {
|
||||
this.#logger.info('Fetching payer from API');
|
||||
const { payerId } = FetchPayerSchema.parse(params);
|
||||
|
||||
let req$ = this.#payerService
|
||||
.PayerGetPayer(payerId)
|
||||
.pipe(catchResponseArgsErrorPipe());
|
||||
|
||||
if (abortSignal) {
|
||||
req$ = req$.pipe(takeUntilAborted(abortSignal));
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await firstValueFrom(req$);
|
||||
this.#logger.debug('Successfully fetched payer');
|
||||
return res as ResponseArgs<CrmPayer>;
|
||||
} catch (error) {
|
||||
this.#logger.error('Error fetching payer', error);
|
||||
return undefined as unknown as ResponseArgs<CrmPayer>;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user