Merged PR 1974: feat(crm): introduce PrimaryCustomerCardResource and format-name utility

feat(crm): introduce PrimaryCustomerCardResource and format-name utility

Replace SelectedCustomerBonusCardsResource with a new PrimaryCustomerCardResource
that automatically loads and exposes the primary customer card as a signal.
This simplifies customer card access across the application by providing a
centralized, root-level injectable resource with automatic tab synchronization.

Create new @isa/utils/format-name library to consolidate customer name formatting
logic previously duplicated across components. The utility formats names with
configurable first name, last name, and organization name fields.

Key changes:
- Add PrimaryCustomerCardResource as providedIn root service with automatic
  customer selection tracking via effect
- Remove SelectedCustomerBonusCardsResource and its manual provisioning
- Extract formatName function to dedicated utility library with Vitest setup
- Update all reward-related components to use new resource pattern
- Migrate OMS components to use centralized format-name utility
- Add comprehensive unit tests for formatName function

BREAKING CHANGE: SelectedCustomerBonusCardsResource has been removed

Ref: #5389
This commit is contained in:
Nino Righi
2025-10-21 13:11:03 +00:00
committed by Lorenz Hilpert
parent f549c59bc8
commit 0b76552211
38 changed files with 592 additions and 208 deletions

View File

@@ -1,49 +0,0 @@
import { effect, inject, Injectable, resource, signal } from '@angular/core';
import { CrmSearchService, CrmTabMetadataService } from '../services';
import { TabService } from '@isa/core/tabs';
@Injectable()
export class CustomerBonusCardsResource {
#customerService = inject(CrmSearchService);
#params = signal<{ customerId: number | undefined }>({
customerId: undefined,
});
params(params: { customerId?: number }) {
this.#params.update((p) => ({ ...p, ...params }));
}
readonly resource = resource({
params: () => this.#params(),
loader: async ({ params, abortSignal }) => {
if (!params.customerId) {
return undefined;
}
const res = await this.#customerService.fetchCustomerCards(
{
customerId: params.customerId,
},
abortSignal,
);
return res.result;
},
});
}
@Injectable()
export class SelectedCustomerBonusCardsResource extends CustomerBonusCardsResource {
#tabId = inject(TabService).activatedTabId;
#customerMetadata = inject(CrmTabMetadataService);
constructor() {
super();
effect(() => {
const tabId = this.#tabId();
const customerId = tabId
? this.#customerMetadata.selectedCustomerId(tabId)
: undefined;
this.params({ customerId });
});
}
}

View File

@@ -1,5 +1,5 @@
export * from './country.resource';
export * from './customer-bonus-cards.resource';
export * from './primary-customer-card.resource';
export * from './customer-shipping-address.resource';
export * from './customer-shipping-addresses.resource';
export * from './customer.resource';

View File

@@ -0,0 +1,121 @@
import {
computed,
effect,
inject,
Injectable,
resource,
signal,
untracked,
} from '@angular/core';
import { CrmSearchService, CrmTabMetadataService } from '../services';
import { injectTabId } from '@isa/core/tabs';
import { BonusCardInfo } from '../models';
import { getPrimaryBonusCard } from '../helpers';
/**
* Resource for the primary customer card of a customer.
*
* This resource automatically loads the primary bonus card of a customer
* as soon as a `customerId` is set in the tab metadata. An internal effect
* monitors changes to the `selectedCustomerId` in the active tab and
* triggers loading of the customer card automatically.
*
* @example
* ```typescript
* const resource = inject(PrimaryCustomerCardResource);
*
* // Primary customer card (automatically loaded via effect)
* const card = resource.primaryCustomerCard();
*
* // Loading status
* const isLoading = resource.loading();
* ```
*/
@Injectable({ providedIn: 'root' })
export class PrimaryCustomerCardResource {
#customerMetadata = inject(CrmTabMetadataService);
#customerService = inject(CrmSearchService);
#tabId = injectTabId();
#customerId = signal<number | undefined>(undefined);
#primaryCustomerCardResource = resource({
params: computed(() => ({ customerId: this.#customerId() })),
loader: async ({
params,
abortSignal,
}): Promise<BonusCardInfo | undefined> => {
const tabId = this.#tabId();
if (!tabId || !params.customerId) {
return undefined;
}
const res = await this.#customerService.fetchCustomerCards(
{ customerId: params.customerId },
abortSignal,
);
if (res?.result?.length > 0) {
return getPrimaryBonusCard(res.result);
}
return undefined;
},
defaultValue: undefined,
});
/**
* Signal containing the primary customer card of the selected customer.
* Automatically updated when the `selectedCustomerId` changes in the tab metadata.
*/
readonly primaryCustomerCard =
this.#primaryCustomerCardResource.value.asReadonly();
/**
* Signal indicating whether the customer card is currently being loaded.
*/
readonly loading = this.#primaryCustomerCardResource.isLoading;
/**
* Signal containing an error message if an error occurred during loading.
* Returns `null` if no error is present.
*/
readonly error = computed(
() => this.#primaryCustomerCardResource.error()?.message ?? null,
);
/**
* Loads the primary customer card for the specified customer ID.
*
* @param customerId - The ID of the customer whose primary card should be loaded
*/
loadPrimaryCustomerCard(customerId: number | undefined) {
this.#customerId.set(customerId);
}
constructor() {
/**
* Effect: Monitors changes to the `selectedCustomerId` in the tab metadata
* and automatically loads the primary customer card whenever a new customer ID
* is set. This ensures that the customer card is always in sync with the
* customer selection in the active tab.
*
* Uses `untracked` to prevent creating additional signal dependencies when
* checking if the customer ID has actually changed, avoiding unnecessary reloads.
*/
effect(() => {
const tabId = this.#tabId();
let customerId = undefined;
if (tabId) {
customerId = this.#customerMetadata.selectedCustomerId(tabId);
}
untracked(() => {
if (this.#customerId() !== customerId) {
this.loadPrimaryCustomerCard(customerId);
}
});
});
}
}