refactor(lib-checkout,lib-crm): replace SelectedCustomerFacade with CrmTabMetadataService

Remove the redundant SelectedCustomerFacade which was just a thin wrapper
around CrmTabMetadataService. Update all consumers to use CrmTabMetadataService
directly for better consistency and reduced indirection.

Changes:
- Remove SelectedCustomerFacade and its exports
- Update reward catalog components to use SelectedCustomerBonusCardsResource
- Replace local resource factories with global resources
- Update purchase options modal and customer details components
- Simplify reward action component logic and improve button state handling

Ref: #5202, #5263, #5358
This commit is contained in:
Nino
2025-09-30 18:14:54 +02:00
parent 37840b1565
commit c767c60d31
16 changed files with 129 additions and 184 deletions

View File

@@ -5,3 +5,4 @@ export * from './models';
export * from './schemas';
export * from './services';
export * from './store';
export * from './resources';

View File

@@ -1,3 +1 @@
export * from './reward-catalog.resource';
export * from './reward-customer-card.resource';
export * from './reward-shopping-cart.resource';

View File

@@ -1,31 +0,0 @@
import { inject, resource } from '@angular/core';
import { injectTabId } from '@isa/core/tabs';
import { CustomerCardsFacade } from '@isa/crm/data-access';
import { SelectedCustomerFacade } from '@isa/crm/data-access';
export const createRewardCustomerCardResource = () => {
const tabId = injectTabId();
const customerCardsFacade = inject(CustomerCardsFacade);
const selectedCustomerFacade = inject(SelectedCustomerFacade);
return resource({
loader: async ({ abortSignal }) => {
const customerId = selectedCustomerFacade.get(tabId()!);
if (!customerId) {
return undefined;
}
const fetchCustomerCardsResponse = await customerCardsFacade.get(
{ customerId },
abortSignal,
);
const activePrimaryCard =
fetchCustomerCardsResponse.result?.find(
(card) => card.isPrimary && card.isActive,
) ?? undefined;
return activePrimaryCard;
},
});
};

View File

@@ -1,28 +0,0 @@
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;
},
});
};

View File

@@ -4,7 +4,10 @@
uiButton
color="brand"
size="large"
[disabled]="!hasSelectedItems()"
[disabled]="
!hasSelectedItems() ||
(!!primaryBonusCard() && primaryBonusCardPoints() <= 0)
"
(click)="continueToPurchasingOptions()"
>
Prämie auswählen

View File

@@ -1,8 +1,8 @@
import {
ChangeDetectionStrategy,
Component,
linkedSignal,
inject,
computed,
} from '@angular/core';
import {
RewardCatalogStore,
@@ -13,9 +13,12 @@ import { injectTabId } from '@isa/core/tabs';
import { ButtonComponent } from '@isa/ui/buttons';
import { PurchaseOptionsModalService } from '@modal/purchase-options';
import { firstValueFrom } from 'rxjs';
import { createRewardCustomerCardResource } from '../resources';
import { Router } from '@angular/router';
import { getRouteToCustomer } from '../helpers';
import {
getPrimaryBonusCard,
SelectedCustomerBonusCardsResource,
} from '@isa/crm/data-access';
@Component({
selector: 'reward-action',
@@ -32,16 +35,24 @@ export class RewardActionComponent {
#purchasingOptionsModal = inject(PurchaseOptionsModalService);
#shoppingCartFacade = inject(ShoppingCartFacade);
#checkoutMetadataService = inject(CheckoutMetadataService);
#customerBonusCardsResource = inject(SelectedCustomerBonusCardsResource)
.resource;
rewardCustomerCardResource = createRewardCustomerCardResource(); // TODO: Refactor and use global resource
readonly customerCardResponseValue =
this.#customerBonusCardsResource.value.asReadonly();
customerCardResponseValue = linkedSignal(() =>
this.rewardCustomerCardResource.value(),
bonusCards = computed(() => {
return this.customerCardResponseValue() ?? [];
});
primaryBonusCard = computed(() => getPrimaryBonusCard(this.bonusCards()));
primaryBonusCardPoints = computed(
() => this.primaryBonusCard()?.totalPoints ?? 0,
);
selectedItems = linkedSignal(() => this.#store.selectedItems());
selectedItems = computed(() => this.#store.selectedItems());
hasSelectedItems = linkedSignal(() => {
hasSelectedItems = computed(() => {
return Object.keys(this.selectedItems() || {}).length > 0;
});
@@ -82,10 +93,7 @@ export class RewardActionComponent {
}
async #navigation(tabId: number) {
const hasCustomer = this.customerCardResponseValue();
if (hasCustomer) {
// TODO: Update Reward Shopping Cart Resource
} else {
if (!this.primaryBonusCard()) {
const route = getRouteToCustomer(tabId);
await this.#router.navigate(route.path, {
queryParams: route.queryParams,

View File

@@ -18,6 +18,8 @@ import { RewardListComponent } from './reward-list/reward-list.component';
import { injectRestoreScrollPosition } from '@isa/utils/scroll-position';
import { RewardActionComponent } from './reward-action/reward-action.component';
import { TabService } from '@isa/core/tabs';
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
import { SelectedCustomerBonusCardsResource } from '@isa/crm/data-access';
/**
* Factory function to retrieve query settings from the activated route data.
@@ -33,6 +35,8 @@ function querySettingsFactory() {
styleUrl: './reward-catalog.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
SelectedRewardShoppingCartResource,
SelectedCustomerBonusCardsResource,
provideFilter(
withQuerySettingsFactory(querySettingsFactory),
withQueryParamsSync(),

View File

@@ -1,3 +1,3 @@
:host {
@apply h-[9.5rem] desktop:h-32 w-full flex flex-row gap-20 rounded-2xl bg-isa-neutral-400 p-6 text-isa-neutral-900;
@apply h-[9.5rem] desktop:h-32 w-full flex flex-row justify-between rounded-2xl bg-isa-neutral-400 p-6;
}

View File

@@ -1,43 +1,45 @@
<div class="flex flex-col gap-[0.125rem]">
<div class="flex flex-col gap-1">
<span class="isa-text-body-1-regular"
>{{ card()?.firstName }} {{ card()?.lastName }}
</span>
<span class="isa-text-body-1-bold"
>{{ customerCardTotalPoints() }} Lesepunkte</span
<div class="flex flex-row gap-20 text-isa-neutral-900">
<div class="flex flex-col gap-[0.125rem]">
<div class="flex flex-col gap-1">
<span class="isa-text-body-1-regular"
>{{ primaryBonusCard()?.firstName }} {{ primaryBonusCard()?.lastName }}
</span>
<span class="isa-text-body-1-bold"
>{{ primaryBonusCardPoints() }} Lesepunkte</span
>
</div>
<ui-text-button
class="self-start -ml-[0.6rem]"
type="button"
color="subtle"
size="small"
(click)="resetCustomerAndCart()"
>
Zurücksetzen
</ui-text-button>
</div>
<ui-text-button
class="self-start -ml-[0.6rem]"
type="button"
color="subtle"
size="small"
(click)="reset.emit()"
>
Zurücksetzen
</ui-text-button>
<div class="flex flex-col gap-2">
<span class="isa-text-body-1-regular">Prämien ausgewählt</span>
<span
*uiSkeletonLoader="shoppingCartResponseFetching()"
class="isa-text-body-1-bold"
>{{ cartItemsLength() }}</span
>
</div>
</div>
<div class="flex flex-col gap-2">
<span class="isa-text-body-1-regular">Prämien ausgewählt</span>
<span
*uiSkeletonLoader="shoppingCartResponseFetching()"
class="isa-text-body-1-bold"
>{{ cartItemsLength() }}</span
>
</div>
@if (cartItemsLength()) {
<button
@if (cartItemsLength() && primaryBonusCardPoints() > 0) {
<a
class="self-start"
data-which="continue-to-reward-checkout"
data-what="continue-to-reward-checkout"
uiButton
color="brand"
size="large"
[disabled]="disableContinueCta()"
(click)="continueToRewardCheckout()"
routerLink="./cart"
>
Prämienausgabe
</button>
</a>
}

View File

@@ -1,60 +1,68 @@
import {
ChangeDetectionStrategy,
Component,
input,
linkedSignal,
output,
computed,
inject,
} from '@angular/core';
import { ButtonComponent, TextButtonComponent } from '@isa/ui/buttons';
import { BonusCardInfo } from '@isa/crm/data-access';
import { createRewardShoppingCartResource } from '../../resources';
import {
CrmTabMetadataService,
getPrimaryBonusCard,
SelectedCustomerBonusCardsResource,
} from '@isa/crm/data-access';
import { SkeletonLoaderDirective } from '@isa/ui/skeleton-loader';
import {
CheckoutMetadataService,
SelectedRewardShoppingCartResource,
} from '@isa/checkout/data-access';
import { RouterLink } from '@angular/router';
import { injectTabId } from '@isa/core/tabs';
@Component({
selector: 'reward-customer-card',
templateUrl: './reward-customer-card.component.html',
styleUrl: './reward-customer-card.component.css',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TextButtonComponent, ButtonComponent, SkeletonLoaderDirective],
imports: [
RouterLink,
TextButtonComponent,
ButtonComponent,
SkeletonLoaderDirective,
],
})
export class RewardCustomerCardComponent {
card = input.required<BonusCardInfo>();
reset = output<void>();
#crmTabMetadataService = inject(CrmTabMetadataService);
#checkoutMetadataService = inject(CheckoutMetadataService);
tabId = injectTabId();
#shoppingCartResource = inject(SelectedRewardShoppingCartResource).resource;
#customerBonusCardsResource = inject(SelectedCustomerBonusCardsResource)
.resource;
rewardShoppingCartResource = createRewardShoppingCartResource(); // TODO: Refactor and use global resource
readonly customerCardResponseValue =
this.#customerBonusCardsResource.value.asReadonly();
shoppingCartResponseValue = linkedSignal(() =>
this.rewardShoppingCartResource.value(),
bonusCards = computed(() => {
return this.customerCardResponseValue() ?? [];
});
primaryBonusCard = computed(() => getPrimaryBonusCard(this.bonusCards()));
primaryBonusCardPoints = computed(
() => this.primaryBonusCard()?.totalPoints ?? 0,
);
shoppingCartResponseFetching = linkedSignal(
() => this.rewardShoppingCartResource.status() === 'loading',
);
readonly shoppingCartResponseValue =
this.#shoppingCartResource.value.asReadonly();
readonly shoppingCartResponseFetching = this.#shoppingCartResource.isLoading;
cartItemsLength = linkedSignal(
cartItemsLength = computed(
() => this.shoppingCartResponseValue()?.items?.length ?? 0,
);
cartTotalLoyaltyPoints = linkedSignal(() => {
return (
this.shoppingCartResponseValue()?.items?.reduce(
(sum, item) => sum + (item?.data?.loyalty?.value ?? 0),
0,
) ?? 0
resetCustomerAndCart() {
this.#crmTabMetadataService.setSelectedCustomerId(this.tabId()!, undefined);
this.#checkoutMetadataService.setRewardShoppingCartId(
this.tabId()!,
undefined,
);
});
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
}
}

View File

@@ -1,11 +1,8 @@
@let card = customerCardResponseValue();
@let card = primaryBonusCard();
@let cardFetching = customerCardResponseFetching();
@if (!cardFetching) {
@if (card) {
<reward-customer-card
[card]="card"
(reset)="resetCustomer()"
></reward-customer-card>
<reward-customer-card></reward-customer-card>
} @else {
<reward-start-card></reward-start-card>
}

View File

@@ -1,15 +1,16 @@
import {
ChangeDetectionStrategy,
Component,
computed,
inject,
linkedSignal,
} from '@angular/core';
import { RewardStartCardComponent } from './reward-start-card/reward-start-card.component';
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';
import {
getPrimaryBonusCard,
SelectedCustomerBonusCardsResource,
} from '@isa/crm/data-access';
@Component({
selector: 'reward-header',
templateUrl: './reward-header.component.html',
@@ -22,18 +23,17 @@ import { SelectedCustomerFacade } from '@isa/crm/data-access';
],
})
export class RewardHeaderComponent {
tabId = injectTabId();
selectedCustomerFacade = inject(SelectedCustomerFacade);
rewardCustomerCardResource = createRewardCustomerCardResource(); // TODO: Refactor and use global resource
#customerBonusCardsResource = inject(SelectedCustomerBonusCardsResource)
.resource;
readonly customerCardResponseValue =
this.rewardCustomerCardResource.value.asReadonly();
this.#customerBonusCardsResource.value.asReadonly();
readonly customerCardResponseFetching =
this.rewardCustomerCardResource.isLoading;
this.#customerBonusCardsResource.isLoading;
resetCustomer() {
this.selectedCustomerFacade.clear(this.tabId()!);
this.rewardCustomerCardResource.reload();
}
bonusCards = computed(() => {
return this.customerCardResponseValue() ?? [];
});
primaryBonusCard = computed(() => getPrimaryBonusCard(this.bonusCards()));
}

View File

@@ -1,3 +1,2 @@
export * from './customer-cards.facade';
export * from './customer.facade';
export * from './selected-customer-id.facade';

View File

@@ -1,19 +0,0 @@
import { inject, Injectable } from '@angular/core';
import { CrmTabMetadataService } from '../services';
@Injectable({ providedIn: 'root' })
export class SelectedCustomerFacade {
#crmTabMetadataService = inject(CrmTabMetadataService);
set(tabId: number, customerId: number) {
this.#crmTabMetadataService.setSelectedCustomerId(tabId, customerId);
}
get(tabId: number) {
return this.#crmTabMetadataService.selectedCustomerId(tabId);
}
clear(tabId: number) {
this.#crmTabMetadataService.setSelectedCustomerId(tabId, undefined);
}
}