fix(shared-filter, reward-catalog): Added Branch Filter Toggle to Reward HSC View, Adjusted Controls Panel Filter Styling and Layout to fix mobile issues and added spacing to order-by-toolbar Refs: #5514, #5475
@isa/checkout/feature/reward-catalog
A comprehensive loyalty rewards catalog feature for Angular applications supporting reward item browsing, selection, and checkout for customers with bonus cards.
Overview
The Reward Catalog feature library provides a complete user interface for browsing and selecting loyalty reward items. It integrates customer bonus card information, manages reward item selection state, handles filtering and pagination, and orchestrates the checkout flow for reward redemption. The library supports both customer-selected and guest workflows, with automatic navigation to customer selection when needed.
Table of Contents
- Features
- Quick Start
- Core Concepts
- Component API Reference
- Usage Examples
- Routing and Navigation
- Filtering and Search
- State Management
- Architecture Notes
- Dependencies
- Testing
- Best Practices
Features
- Customer Context Management - Displays customer bonus card information and available points
- Reward Item Browsing - Searchable, filterable catalog of loyalty reward items
- Multi-Select Interface - Checkbox-based selection of multiple reward items
- Infinite Scroll Pagination - Automatic loading of additional items on scroll
- Customer Selection Flow - Automatic navigation to customer search for guest users
- Shopping Cart Integration - Creates and manages reward-specific shopping carts
- Purchase Options Modal - Launches purchase options dialog with redemption point calculation
- Responsive Design - Tablet and desktop layouts with breakpoint-aware rendering
- Filter Persistence - Query parameter synchronization for filter state
- Empty State Handling - User-friendly messages and reset actions when no results found
- Loading States - Skeleton loaders and spinners for async operations
- Tab Context Integration - Reward context tracking for multi-tab workflows
Quick Start
1. Route Configuration
import { Routes } from '@angular/router';
const routes: Routes = [
{
path: 'reward',
loadChildren: () =>
import('@isa/checkout/feature/reward-catalog').then(m => m.routes)
}
];
2. Basic Usage
The library is designed to be lazy-loaded as a route module:
// In your app routing configuration
{
path: 'kunde/:tabId/reward',
loadChildren: () =>
import('@isa/checkout/feature/reward-catalog').then(m => m.routes),
data: {
scrollPositionRestoration: true // Automatically handled
}
}
3. Navigation to Reward Catalog
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-feature',
template: '...'
})
export class FeatureComponent {
#router = inject(Router);
tabId = 123;
navigateToRewardCatalog(): void {
this.#router.navigate(['/kunde', this.tabId, 'reward']);
}
}
Core Concepts
Customer Workflows
The library supports two primary user workflows:
1. Guest Workflow (No Customer Selected)
When no customer is selected, the header displays a RewardStartCard:
// User sees welcome message
// CTA button: "Kund*in auswählen" (Select Customer)
// Clicking navigates to customer search filtered for loyalty customers
2. Customer Selected Workflow
When a customer with a bonus card is selected, the header displays a RewardCustomerCard:
// Displays: Customer name, available Lesepunkte (reading points)
// Shows: Number of rewards selected in shopping cart
// Actions:
// - "Zurücksetzen" (Reset) - Clears customer and cart
// - "Prämienausgabe" (Reward Checkout) - Navigates to checkout (when items selected)
Reward Item Selection
Items are selected using checkboxes with state managed by RewardCatalogStore:
// Selection flow:
1. User clicks checkbox on reward item
2. Item added to RewardCatalogStore.selectedItems
3. Selection state persists in session storage
4. "Prämie auswählen" button becomes enabled
5. User clicks button to open purchase options modal
6. Items added to reward shopping cart
7. Selection state cleared on successful addition
Search and Filtering
The library integrates with @isa/shared/filter for comprehensive filtering:
// Filter capabilities:
- Text search across item names and descriptions
- Category filtering
- Point value filtering
- Sorting options (by points, name, etc.)
- Filter state synced to URL query parameters
- Filter persistence across page reloads
Pagination Strategy
Infinite scroll pagination using intersection observer:
// Pagination flow:
1. Initial load fetches first page (e.g., 20 items)
2. User scrolls to bottom
3. InViewportDirective detects trigger element
4. searchTrigger set to 'reload'
5. Next page fetched and appended to existing items
6. Process repeats until all items loaded (itemsLength === hits)
Component API Reference
RewardCatalogComponent
Main container component that orchestrates the entire reward catalog feature.
Selector: reward-catalog
Providers:
SelectedRewardShoppingCartResource- Resource for fetching reward shopping cartSelectedCustomerBonusCardsResource- Resource for fetching customer bonus cardsprovideFilter()- Filter service configuration with query settings and URL sync
Methods:
search(trigger: SearchTrigger): void
Triggers a search operation with the specified trigger type.
Parameters:
trigger: 'scan' | 'input' | 'filter' | 'orderBy'- The search trigger type
Example:
// Called by filter-controls-panel
search('filter'); // User changed a filter
search('input'); // User typed in search box
initRewardContext(): void
Initializes the reward context in tab metadata. Called automatically in constructor.
Purpose: Marks the current tab as being in "reward" context, which is used by customer search to know the user's intent.
Example:
// Automatically called on component initialization
// Sets: TabMetadata.context = 'reward'
// Used by customer search to customize behavior
Host Classes:
/* Applied classes */
w-full flex flex-col gap-4 mt-5 isa-desktop:mt-6 overflow-x-hidden
RewardHeaderComponent
Displays customer information or guest welcome message based on customer selection state.
Selector: reward-header
Computed Signals:
bonusCards: Signal<BonusCard[]>
All bonus cards for the selected customer.
primaryBonusCard: Signal<BonusCard | undefined>
The primary bonus card (used for point balance and customer name).
Template Logic:
// Conditional rendering based on customer state
@if (!cardFetching) {
@if (card) {
<reward-customer-card></reward-customer-card> // Customer selected
} @else {
<reward-start-card></reward-start-card> // No customer
}
} @else {
// Loading skeleton
}
RewardCustomerCardComponent
Displays selected customer information, available points, and checkout options.
Selector: reward-customer-card
Computed Signals:
bonusCards: Signal<BonusCard[]>
Array of bonus cards for the customer.
primaryBonusCard: Signal<BonusCard | undefined>
The primary bonus card with customer details.
primaryBonusCardPoints: Signal<number>
Available reading points (Lesepunkte) for redemption.
Default: 0
cartItemsLength: Signal<number>
Number of items currently in the reward shopping cart.
Methods:
resetCustomerAndCart(): void
Clears the selected customer and reward shopping cart from tab metadata.
Effects:
- Removes customer selection
- Clears reward shopping cart ID
- Header switches to RewardStartCard
- User must re-select customer
Example:
// Called when user clicks "Zurücksetzen" button
resetCustomerAndCart();
// Customer: undefined
// Shopping cart: undefined
// Header: Shows "Kund*in auswählen" again
Host Classes:
h-[9.5rem] desktop:h-32 w-full flex flex-row justify-between rounded-2xl bg-isa-neutral-400 p-6
Template Features:
- Displays customer name from
primaryBonusCard.firstNameandlastName - Shows available points with bold styling
- Reset button (text button, subtle color, small size)
- Selected rewards count with skeleton loader during fetch
- "Prämienausgabe" CTA button (only shown when items selected and points > 0)
RewardStartCardComponent
Welcome card shown when no customer is selected, prompting customer selection.
Selector: reward-start-card
Computed Signals:
route: Signal<{ path: any[]; queryParams: any }>
Navigation route configuration for customer search.
Example value:
{
path: ['/kunde', 123, 'customer', {
outlets: { primary: 'search', side: 'search-customer-main' }
}],
queryParams: {
filter_customertype: 'webshop&loyalty;loyalty&!webshop'
}
}
Host Classes:
h-[9.5rem] desktop:h-32 w-full grid grid-cols-[1fr,auto] gap-6 rounded-2xl bg-isa-neutral-400 p-6 justify-between
Template Features:
- Title: "Prämienshop" (subtitle-1-regular)
- Description: Instructions for using the catalog
- CTA Button: "Kund*in auswählen" (Select Customer)
- Color: tertiary
- Size: large
- Links to customer search with loyalty filter
RewardListComponent
Displays the paginated list of reward items with infinite scroll and loading states.
Selector: reward-list
Inputs:
searchTrigger: ModelSignal<SearchTrigger | 'reload' | 'initial'>
Two-way binding signal that triggers searches and pagination.
Type: 'scan' | 'input' | 'filter' | 'orderBy' | 'reload' | 'initial'
Default: 'initial'
Example:
<reward-list
[(searchTrigger)]="searchTrigger"
></reward-list>
Computed Signals:
listFetching: Signal<boolean>
Whether the catalog resource is currently loading data.
items: Signal<Item[]>
Array of reward items to display.
itemsLength: Signal<number>
Number of items currently loaded.
hits: Signal<number>
Total number of items matching current filters.
renderSearchLoader: Signal<boolean>
Whether to show the initial search loader (when fetching with no items).
renderPageTrigger: Signal<boolean>
Whether to render the pagination trigger element (when more items exist and not currently loading).
Methods:
paging(inViewport: boolean): void
Handles pagination when the trigger element comes into viewport.
Parameters:
inViewport: boolean- Whether the trigger element is visible
Behavior:
// Called by (uiInViewport) directive
if (inViewport && itemsLength < hits) {
searchTrigger.set('reload'); // Triggers next page load
}
resetFilter(): void
Resets all filters to default values and triggers a new search.
Example:
// Called when user clicks "Filter zurücksetzen" in empty state
resetFilter();
// All filters cleared
// Query params updated
// New search triggered
Host Classes:
w-full flex flex-col gap-4
Template Features:
- Result count display:
{{ hits() }} Einträge - Item list with deferred rendering (viewport-based lazy loading)
- Horizontal dividers between items on desktop
- Loading spinner during pagination
- Viewport trigger for infinite scroll
- Empty state with reset button when no results
RewardListItemComponent
Individual reward item row with product information and selection checkbox.
Selector: reward-list-item
Inputs:
item: InputSignal<Item>
The reward item to display.
Required: Yes
Type: Item from @isa/catalogue/data-access
Example:
<reward-list-item
[item]="rewardItem"
></reward-list-item>
Computed Signals:
desktopBreakpoint: Signal<boolean>
Whether the current viewport matches desktop breakpoints (Desktop, DesktopL, or DesktopXL).
productInfoOrientation: Signal<'horizontal' | 'vertical'>
Product info layout orientation based on viewport size.
Returns: 'horizontal' on desktop, 'vertical' on tablet/mobile
Template Structure:
<ui-client-row>
<ui-client-row-content>
<checkout-product-info-redemption
[item]="item"
[orientation]="horizontal|vertical"
/>
</ui-client-row-content>
<ui-item-row-data>
<checkout-stock-info [item]="item" />
</ui-item-row-data>
<ui-item-row-data>
<reward-list-item-select [item]="item" />
</ui-item-row-data>
</ui-client-row>
E2E Attributes:
data-what="reward-list-item"[attr.data-which]="item.id"
RewardListItemSelectComponent
Checkbox component for selecting/deselecting a reward item.
Selector: reward-list-item-select
Inputs:
item: InputSignal<Item>
The reward item associated with this selection control.
Required: Yes
Computed Signals:
itemSelected: Signal<boolean>
Whether the current item is selected in the store.
Methods:
setSelected(selected: boolean): void
Updates the selection state in the RewardCatalogStore.
Parameters:
selected: boolean- New selection state
Behavior:
if (selected) {
store.selectItem(itemId, item); // Add to selection
} else {
store.removeItem(itemId); // Remove from selection
}
Example:
<ui-checkbox appearance="bullet">
<input
type="checkbox"
[ngModel]="itemSelected()"
(ngModelChange)="setSelected($event)"
data-what="reward-item-selection-checkbox"
[attr.data-which]="item.product.ean"
/>
</ui-checkbox>
RewardActionComponent
Fixed bottom action bar with the "Prämie auswählen" (Select Reward) button.
Selector: reward-action
Computed Signals:
bonusCards: Signal<BonusCard[]>
Customer's bonus cards.
primaryBonusCard: Signal<BonusCard | undefined>
Primary bonus card with customer details.
primaryBonusCardPoints: Signal<number>
Available reading points for redemption.
selectedItems: Signal<Record<number, Item>>
Dictionary of selected items keyed by item ID.
hasSelectedItems: Signal<boolean>
Whether any items are currently selected.
Methods:
async continueToPurchasingOptions(): Promise<void>
Opens the purchase options modal with selected items.
Workflow:
- Retrieves selected items from store
- Gets or creates reward shopping cart ID
- Opens purchase options modal with redemption points enabled
- Waits for modal result
- Clears selected items on success
- Navigates to customer search if no customer and user didn't choose "continue-shopping"
Example:
await continueToPurchasingOptions();
// Modal opens with selected items
// User configures purchase options
// On confirm: items added to cart, selections cleared
// On cancel: nothing happens, selections preserved
// If no customer: navigates to customer search
Button Disabled Conditions:
!hasSelectedItems() || // No items selected
(!!primaryBonusCard() && primaryBonusCardPoints() <= 0) // Customer has 0 points
Usage Examples
Navigating to Reward Catalog
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
import { injectTabId } from '@isa/core/tabs';
@Component({
selector: 'app-navigation-example',
template: `
<button (click)="openRewardCatalog()">
Browse Rewards
</button>
`
})
export class NavigationExampleComponent {
#router = inject(Router);
tabId = injectTabId();
async openRewardCatalog(): Promise<void> {
await this.#router.navigate(['/kunde', this.tabId(), 'reward']);
}
}
Accessing Reward Catalog with Deep Link
// Direct navigation with filter and search parameters
async navigateWithFilters(): Promise<void> {
await this.#router.navigate(
['/kunde', this.tabId(), 'reward'],
{
queryParams: {
search: 'book',
filter_category: 'literature',
orderBy: 'points_asc'
}
}
);
}
Programmatic Customer Selection
import { Component, inject } from '@angular/core';
import { CrmTabMetadataService } from '@isa/crm/data-access';
import { injectTabId } from '@isa/core/tabs';
@Component({
selector: 'app-customer-selection-example',
template: '...'
})
export class CustomerSelectionExampleComponent {
#crmMetadata = inject(CrmTabMetadataService);
tabId = injectTabId();
selectCustomerForRewards(customerId: number): void {
// Set customer in tab metadata
this.#crmMetadata.setSelectedCustomerId(this.tabId()!, customerId);
// Navigate to reward catalog
this.#router.navigate(['/kunde', this.tabId(), 'reward']);
// Catalog will automatically load customer's bonus cards
}
}
Resetting Reward Session
import { Component, inject } from '@angular/core';
import { CrmTabMetadataService } from '@isa/crm/data-access';
import { CheckoutMetadataService } from '@isa/checkout/data-access';
import { injectTabId } from '@isa/core/tabs';
@Component({
selector: 'app-reset-example',
template: '...'
})
export class ResetExampleComponent {
#crmMetadata = inject(CrmTabMetadataService);
#checkoutMetadata = inject(CheckoutMetadataService);
tabId = injectTabId();
resetRewardSession(): void {
const tabId = this.tabId()!;
// Clear customer selection
this.#crmMetadata.setSelectedCustomerId(tabId, undefined);
// Clear reward shopping cart
this.#checkoutMetadata.setRewardShoppingCartId(tabId, undefined);
// User returns to guest workflow with RewardStartCard
}
}
Checking Customer Points Before Navigation
import { Component, inject, signal } from '@angular/core';
import { SelectedCustomerBonusCardsResource } from '@isa/crm/data-access';
import { getPrimaryBonusCard } from '@isa/crm/data-access';
import { computed } from '@angular/core';
@Component({
selector: 'app-points-check-example',
template: `
@if (hasPoints()) {
<button (click)="navigateToRewards()">
Browse Rewards ({{ availablePoints() }} points)
</button>
} @else {
<p>Customer has no points available</p>
}
`
})
export class PointsCheckExampleComponent {
#bonusCardsResource = inject(SelectedCustomerBonusCardsResource).resource;
bonusCards = computed(() =>
this.#bonusCardsResource.value() ?? []
);
primaryCard = computed(() =>
getPrimaryBonusCard(this.bonusCards())
);
availablePoints = computed(() =>
this.primaryCard()?.totalPoints ?? 0
);
hasPoints = computed(() =>
this.availablePoints() > 0
);
navigateToRewards(): void {
if (this.hasPoints()) {
this.#router.navigate(['/kunde', this.tabId(), 'reward']);
}
}
}
Monitoring Shopping Cart State
import { Component, inject, computed, effect } from '@angular/core';
import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access';
@Component({
selector: 'app-cart-monitor-example',
template: `
<div class="cart-summary">
<span>Rewards in cart: {{ cartItemCount() }}</span>
<span>Total points: {{ totalPoints() }}</span>
</div>
`
})
export class CartMonitorExampleComponent {
#cartResource = inject(SelectedRewardShoppingCartResource).resource;
cart = computed(() => this.#cartResource.value());
cartItemCount = computed(() =>
this.cart()?.items?.length ?? 0
);
totalPoints = computed(() => {
const items = this.cart()?.items ?? [];
return items.reduce((sum, item) =>
sum + (item.product?.loyaltyPoints ?? 0), 0
);
});
constructor() {
effect(() => {
console.log('Cart updated:', this.cart());
});
}
}
Routing and Navigation
Route Structure
The library exports a single route configuration:
// libs/checkout/feature/reward-catalog/src/lib/routes.ts
export const routes: Routes = [
{
path: '',
component: RewardCatalogComponent,
resolve: {
querySettings: querySettingsResolverFn // Fetches filter configuration
},
data: {
scrollPositionRestoration: true // Enables scroll position restore
},
},
];
Integration in App Routing
// apps/isa-app/src/app/app-routing.module.ts
const routes: Routes = [
{
path: 'kunde/:tabId/reward',
loadChildren: () =>
import('@isa/checkout/feature/reward-catalog')
.then(m => m.routes),
},
// ... other routes
];
Query Settings Resolver
The querySettingsResolverFn fetches filter configuration before component initialization:
// What it does:
1. Calls CatalogueSearchService.fetchLoyaltyQuerySettings()
2. Returns QuerySettings with:
- Available filter options
- Sort options
- Default values
- Filter metadata
3. Makes data available via ActivatedRoute.snapshot.data['querySettings']
Usage in Component:
function querySettingsFactory() {
return inject(ActivatedRoute).snapshot.data['querySettings'];
}
// In providers array
provideFilter(
withQuerySettingsFactory(querySettingsFactory),
withQueryParamsSync()
)
Customer Search Navigation
Helper function for navigating to customer search from reward context:
// libs/checkout/feature/reward-catalog/src/lib/helpers/get-route-to-customer.helper.ts
export const getRouteToCustomer = (tabId: number | null) => {
return {
path: [
'/kunde',
tabId,
'customer',
{ outlets: { primary: 'search', side: 'search-customer-main' } }
].filter(Boolean),
queryParams: {
filter_customertype: 'webshop&loyalty;loyalty&!webshop'
}
};
};
Purpose:
- Navigates to customer search with loyalty customer filter pre-applied
- Uses auxiliary routes for search UI
- Filters out customers without loyalty cards
Scroll Position Restoration
The library uses @isa/utils/scroll-position for automatic scroll restoration:
// In RewardCatalogComponent
restoreScrollPosition = injectRestoreScrollPosition();
// Automatically:
1. Saves scroll position on navigation away
2. Restores position on return
3. Works with infinite scroll pagination
4. Respects route data configuration
Filtering and Search
Filter Service Integration
The library uses @isa/shared/filter with advanced configuration:
provideFilter(
withQuerySettingsFactory(querySettingsFactory), // Pre-loads filter config
withQueryParamsSync() // Syncs to URL query params
)
Filter Capabilities
1. Text Search
// URL: ?search=book
// Searches across:
- Item names
- Product descriptions
- EAN/ISBN codes
2. Category Filtering
// URL: ?filter_category=literature
// Filters by product category
3. Point Range Filtering
// URL: ?filter_points_min=100&filter_points_max=500
// Filters items by redemption point range
4. Sorting
// URL: ?orderBy=points_asc
// Available options:
- points_asc: Points ascending
- points_desc: Points descending
- name_asc: Name A-Z
- name_desc: Name Z-A
Search Triggers
The library supports multiple search trigger types:
type SearchTrigger = 'scan' | 'input' | 'filter' | 'orderBy';
// Additional internal triggers:
'reload' // Pagination (append to existing results)
'initial' // First load (check session storage first - currently disabled)
Trigger Flow:
// User action → FilterControlsPanel emits triggerSearch
FilterControlsPanel → search(trigger) → RewardCatalogComponent
↓
searchTrigger.set(trigger)
↓
filterService.commit()
↓
RewardListComponent detects change
↓
rewardCatalogResource reloads
↓
New items fetched and displayed
Filter State Management
Query Parameter Synchronization
// Filter state automatically synced to URL
// Example URL with filters:
/kunde/123/reward?search=book&filter_category=literature&orderBy=points_asc
// Benefits:
1. Shareable URLs with filter state
2. Browser back/forward support
3. Bookmark-friendly
4. Deep linking support
Filter Reset
// Reset all filters to default
resetFilter() {
this.#filterService.reset({ commit: true });
}
// Effects:
1. All filter values cleared
2. Search text cleared
3. Sort order reset to default
4. Query params updated
5. New search triggered
6. Items list refreshed
Empty State Handling
When no items match the current filters:
<ui-empty-state
title="Keine Suchergebnisse"
description="Überprüfen Sie die Lesepunkte, Filter und Suchanfrage."
>
<button uiButton (click)="resetFilter()">
Filter zurücksetzen
</button>
</ui-empty-state>
State Management
RewardCatalogStore
The library uses RewardCatalogStore from @isa/checkout/data-access for state management.
Store Schema:
interface RewardCatalogState {
items: Item[]; // Current catalog items
hits: number; // Total items matching filters
selectedItems: Record<number, Item>; // Selected items by ID
}
Store Methods:
setItems(items: Item[], hits: number): void
Replaces all items (used for new searches).
// Called when searchTrigger !== 'reload'
store.setItems(newItems, totalHits);
updateItems(items: Item[], hits: number): void
Appends items to existing list (used for pagination).
// Called when searchTrigger === 'reload'
store.updateItems(nextPageItems, totalHits);
selectItem(itemId: number, item: Item): void
Adds an item to the selection.
store.selectItem(123, rewardItem);
// selectedItems: { 123: rewardItem }
removeItem(itemId: number): void
Removes an item from the selection.
store.removeItem(123);
// selectedItems: {}
clearSelectedItems(): void
Clears all selected items.
// Called after successful purchase options
store.clearSelectedItems();
// selectedItems: {}
clearState(): void
Clears all catalog items and hits (preserves selectedItems).
// Called when starting a new search
store.clearState();
// items: [], hits: 0
Session Storage Persistence
The store includes session storage integration (currently disabled):
// TODO: Re-enable when storage configuration issue resolved (#5350, #5353)
// await rewardCatalogStore.loadFromStorage();
// if (rewardCatalogStore.items().length > 0) {
// return; // Use cached items
// }
Intended Behavior:
- Selected items persist across page reloads
- Catalog items cached in session storage
- Fast initial render from cache
- Background refresh ensures data freshness
Shopping Cart Resource
SelectedRewardShoppingCartResource manages the reward shopping cart:
// Provided at RewardCatalogComponent level
// Automatically fetches cart based on CheckoutMetadataService.rewardShoppingCartId
// Updates reactive signals when cart changes
Resource Signals:
resource.value() // Shopping cart data
resource.isLoading() // Loading state
resource.error() // Error state
Customer Bonus Cards Resource
SelectedCustomerBonusCardsResource manages customer bonus card data:
// Provided at RewardCatalogComponent level
// Automatically fetches based on CrmTabMetadataService.selectedCustomerId
// Returns all bonus cards for the customer
Resource Signals:
resource.value() // BonusCard[]
resource.isLoading() // Loading state
resource.error() // Error state
Architecture Notes
Component Hierarchy
RewardCatalogComponent (root, route component)
├── RewardHeaderComponent
│ ├── RewardStartCardComponent (guest workflow)
│ └── RewardCustomerCardComponent (customer workflow)
├── FilterControlsPanelComponent (from @isa/shared/filter)
├── RewardListComponent
│ └── RewardListItemComponent (repeated)
│ ├── ProductInfoRedemptionComponent
│ ├── StockInfoComponent
│ └── RewardListItemSelectComponent
└── RewardActionComponent
Data Flow
1. Route Resolved
↓
2. querySettingsResolverFn fetches filter config
↓
3. RewardCatalogComponent initializes
↓
4. Filter service initialized with query settings
↓
5. Resources initialize (bonus cards, shopping cart)
↓
6. RewardListComponent creates catalog resource
↓
7. Catalog resource fetches items
↓
8. Items stored in RewardCatalogStore
↓
9. UI updates reactively
Resource Pattern
The library uses Angular's resource() function for data fetching:
export const createRewardCatalogResource = (params: () => {
queryToken: QueryTokenInput;
searchTrigger: SearchTrigger | 'reload' | 'initial';
}) => {
return resource({
params,
loader: async ({ abortSignal, params }) => {
// Fetch logic with automatic abort signal handling
// Params changes trigger automatic reload
// Loading states managed automatically
}
});
};
Benefits:
- Automatic cancellation of in-flight requests
- Loading state management
- Error handling
- Reactive param tracking
- AbortSignal integration
Tab Context Pattern
The library uses tab metadata to track reward context:
// Set in constructor
this.#tabService.patchTabMetadata(tabId, { context: 'reward' });
// Read in other components
const metadata = this.#tabService.getTabMetadata(tabId);
if (metadata.context === 'reward') {
// Customize behavior for reward flow
}
Purpose:
- Customer search knows user came from reward catalog
- Can customize customer search filters
- Enables "return to rewards" navigation
- Tracks user intent across multi-step flows
Responsive Design Strategy
The library uses breakpoint service for responsive layouts:
import { breakpoint, Breakpoint } from '@isa/ui/layout';
desktopBreakpoint = breakpoint([
Breakpoint.Desktop,
Breakpoint.DesktopL,
Breakpoint.DesktopXL
]);
productInfoOrientation = linkedSignal(() => {
return this.desktopBreakpoint() ? 'horizontal' : 'vertical';
});
Breakpoint Usage:
- Product info switches orientation (horizontal/vertical)
- Row layouts change (grid on desktop, flex on tablet)
- Component spacing adjusts
- Font sizes and padding responsive
Known Architectural Considerations
1. Session Storage Integration (High Priority)
Current State:
- Session storage loading disabled due to configuration timing issue
- Related to issues #5350, #5353
- Store loads items too late, resource triggers before items available
Impact:
- Initial page load always fetches from server
- No cache benefits
- Slower perceived performance
Proposed Solution:
- Adjust storage loading timing
- Ensure store hydration before resource initialization
- Add configuration support for custom storage keys
2. Infinite Scroll Performance (Medium Priority)
Current State:
- Uses
@defer (on viewport)for item rendering - Intersection observer for pagination trigger
- All items rendered in single list
Potential Issues:
- Large lists (>1000 items) may impact performance
- No virtual scrolling
- Memory usage grows with item count
Proposed Solution:
- Implement virtual scrolling with CDK
- Render only visible items plus buffer
- Recycle DOM elements
3. Purchase Options Modal Coupling (Low Priority)
Current State:
- Direct dependency on
@modal/purchase-options - Modal opened imperatively in component
Consideration:
- Could be abstracted to a service
- Better testability
- Easier to replace modal implementation
4. Customer Selection Navigation (Low Priority)
Current State:
- Hard-coded route to customer search
- Uses auxiliary routes (primary/side outlets)
Consideration:
- Could use navigation service for flexibility
- Easier to customize per deployment
- Better separation of concerns
Performance Optimizations
- OnPush Change Detection - All components use OnPush for minimal re-renders
- Deferred Rendering - List items use
@defer (on viewport)for lazy loading - Linked Signals - Computed values only recalculate when dependencies change
- Resource Caching - Shopping cart and bonus cards fetched once per session
- Query Param Debouncing - Filter changes debounced before URL update
Dependencies
Required Libraries
Angular Core
@angular/core- Angular framework@angular/router- Routing and navigation@angular/forms- FormsModule for checkbox binding
ISA Feature Libraries
@isa/checkout/data-access- RewardCatalogStore, ShoppingCartFacade, CheckoutMetadataService@isa/checkout/shared/product-info- ProductInfoRedemptionComponent, StockInfoComponent@isa/catalogue/data-access- CatalogueSearchService, Item type, QuerySettings@isa/crm/data-access- Customer bonus cards, getPrimaryBonusCard, CrmTabMetadataService@isa/shared/filter- Filter service, query param sync, FilterControlsPanelComponent@isa/core/tabs- Tab service, injectTabId@isa/utils/scroll-position- Scroll position restoration
ISA UI Libraries
@isa/ui/buttons- ButtonComponent, TextButtonComponent, IconButtonComponent@isa/ui/input-controls- CheckboxComponent@isa/ui/item-rows- ClientRowImports, ItemRowDataImports@isa/ui/empty-state- EmptyStateComponent@isa/ui/layout- InViewportDirective, breakpoint service@isa/ui/skeleton-loader- SkeletonLoaderDirective
Modal Services
@modal/purchase-options- PurchaseOptionsModalService
Third-party
rxjs- firstValueFrom for observable handling
Path Alias
Import from: @isa/checkout/feature/reward-catalog
// Import routes
import { routes } from '@isa/checkout/feature/reward-catalog';
// Import helpers (if needed)
import { getRouteToCustomer } from '@isa/checkout/feature/reward-catalog';
Testing
The library uses Vitest with Angular Testing Utilities for testing.
Running Tests
# Run tests for this library
npx nx test reward-catalog --skip-nx-cache
# Run tests with coverage
npx nx test reward-catalog --code-coverage --skip-nx-cache
# Run tests in watch mode
npx nx test reward-catalog --watch
Test Coverage
Current test files:
get-route-to-customer.helper.spec.ts- Helper function unit tests
Test Coverage:
describe('getRouteToCustomer', () => {
it('should return route with tabId when provided');
it('should filter out null tabId from path');
it('should always include customer type filter in query params');
it('should have consistent structure regardless of tabId');
it('should include outlets configuration in path');
});
Testing Recommendations
Component Testing
import { TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { RewardCatalogComponent } from './reward-catalog.component';
import { ActivatedRoute } from '@angular/router';
describe('RewardCatalogComponent', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [RewardCatalogComponent],
providers: [
{
provide: ActivatedRoute,
useValue: {
snapshot: {
data: {
querySettings: {
// Mock query settings
}
}
}
}
}
]
});
});
it('should initialize reward context', () => {
const component = TestBed.createComponent(RewardCatalogComponent).componentInstance;
// Assert tab metadata set to 'reward'
});
});
Resource Testing
import { TestBed } from '@angular/core/testing';
import { describe, it, expect, vi } from 'vitest';
import { createRewardCatalogResource } from './reward-catalog.resource';
describe('createRewardCatalogResource', () => {
it('should fetch items on initial load', async () => {
const mockService = {
searchLoyaltyItems: vi.fn().mockResolvedValue({
result: [/* mock items */],
hits: 10
})
};
// Test resource creation and loading
});
it('should append items on reload trigger', async () => {
// Test pagination behavior
});
});
Store Integration Testing
import { TestBed } from '@angular/core/testing';
import { describe, it, expect } from 'vitest';
import { RewardCatalogStore } from '@isa/checkout/data-access';
describe('RewardCatalogStore Integration', () => {
it('should select and deselect items', () => {
const store = TestBed.inject(RewardCatalogStore);
store.selectItem(123, mockItem);
expect(store.selectedItems()[123]).toBeDefined();
store.removeItem(123);
expect(store.selectedItems()[123]).toBeUndefined();
});
});
E2E Attribute Requirements
All interactive elements include data-what and data-which attributes for E2E testing:
<!-- List item -->
<ui-client-row
data-what="reward-list-item"
[attr.data-which]="item.id">
</ui-client-row>
<!-- Selection checkbox -->
<input
type="checkbox"
data-what="reward-item-selection-checkbox"
[attr.data-which]="item.product.ean"
/>
<!-- Customer selection CTA -->
<a
data-what="select-customer"
data-which="select-customer"
uiButton
[routerLink]="route().path">
</a>
<!-- Checkout CTA -->
<a
data-what="continue-to-reward-checkout"
data-which="continue-to-reward-checkout"
uiButton
routerLink="./cart">
</a>
<!-- Select rewards button -->
<button
data-what="select-rewards"
data-which="select-rewards"
uiButton
(click)="continueToPurchasingOptions()">
</button>
Best Practices
1. Always Use Tab Context
When navigating to the reward catalog, ensure tab context is available:
// Good
const tabId = this.#tabService.activatedTabId();
if (tabId) {
this.#router.navigate(['/kunde', tabId, 'reward']);
}
// Bad (missing tab context)
this.#router.navigate(['/reward']);
2. Check Customer Points Before Enabling Features
Always verify customer has points before allowing reward selection:
// Good
const hasPoints = computed(() => {
const points = this.primaryBonusCard()?.totalPoints ?? 0;
return points > 0;
});
<button [disabled]="!hasPoints()">Browse Rewards</button>
// Bad (no validation)
<button>Browse Rewards</button>
3. Clean Up on Customer Change
Always clear reward context when customer changes:
// Good
resetCustomerAndCart() {
this.#crmMetadata.setSelectedCustomerId(tabId, undefined);
this.#checkoutMetadata.setRewardShoppingCartId(tabId, undefined);
}
// Bad (leaves stale cart)
resetCustomer() {
this.#crmMetadata.setSelectedCustomerId(tabId, undefined);
// Forgot to clear shopping cart!
}
4. Use Computed Signals for Derived State
Leverage Angular signals for reactive derived values:
// Good
primaryBonusCard = computed(() =>
getPrimaryBonusCard(this.bonusCards())
);
availablePoints = computed(() =>
this.primaryBonusCard()?.totalPoints ?? 0
);
// Bad (manual tracking)
availablePoints = 0;
ngOnInit() {
effect(() => {
const card = getPrimaryBonusCard(this.bonusCards());
this.availablePoints = card?.totalPoints ?? 0;
});
}
5. Handle Modal Results Properly
Always handle both success and cancel cases from purchase options modal:
// Good
const result = await firstValueFrom(modalRef.afterClosed$);
if (!result?.data) {
return; // User cancelled, do nothing
}
this.#store.clearSelectedItems();
if (result.data !== 'continue-shopping') {
await this.#navigation(tabId);
}
// Bad (doesn't handle cancel)
const result = await firstValueFrom(modalRef.afterClosed$);
this.#store.clearSelectedItems(); // Clears even on cancel!
6. Use Breakpoint Service for Responsive Layouts
Use the breakpoint service instead of CSS-only solutions:
// Good
desktopBreakpoint = breakpoint([
Breakpoint.Desktop,
Breakpoint.DesktopL,
Breakpoint.DesktopXL
]);
orientation = linkedSignal(() =>
this.desktopBreakpoint() ? 'horizontal' : 'vertical'
);
// Bad (CSS hidden classes)
<div class="hidden desktop:block">...</div>
<div class="block desktop:hidden">...</div>
7. Preserve Filter State in URLs
Always use query parameters for filter state to enable bookmarking and sharing:
// Good (automatic via withQueryParamsSync)
provideFilter(
withQuerySettingsFactory(querySettingsFactory),
withQueryParamsSync()
)
// Bad (component-local state)
filters = signal({ category: null, points: null });
8. Test E2E Attributes
Verify E2E attributes are present in component tests:
// Good
it('should have E2E attributes on list items', () => {
const listItem = fixture.nativeElement.querySelector('[data-what="reward-list-item"]');
expect(listItem).toBeTruthy();
expect(listItem.getAttribute('data-which')).toBe('123');
});
// Bad (no E2E attribute verification)
it('should render list items', () => {
const listItem = fixture.nativeElement.querySelector('ui-client-row');
expect(listItem).toBeTruthy();
});
9. Use Resource Pattern for Data Fetching
Prefer resource pattern over manual service calls for better loading state management:
// Good
rewardCatalogResource = createRewardCatalogResource(() => ({
queryToken: this.#filterService.query(),
searchTrigger: this.searchTrigger()
}));
isLoading = linkedSignal(() =>
this.rewardCatalogResource.status() === 'loading'
);
// Bad (manual loading state)
isLoading = signal(false);
async loadItems() {
this.isLoading.set(true);
try {
const items = await this.#service.searchItems();
} finally {
this.isLoading.set(false);
}
}
10. Follow OnPush Change Detection
Always use OnPush change detection for performance:
// Good (all components in this library)
@Component({
selector: 'reward-catalog',
changeDetection: ChangeDetectionStrategy.OnPush,
// ...
})
// Bad (default change detection)
@Component({
selector: 'reward-catalog',
// Missing changeDetection - uses Default
})
License
Internal ISA Frontend library - not for external distribution.