# @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](#features) - [Quick Start](#quick-start) - [Core Concepts](#core-concepts) - [Component API Reference](#component-api-reference) - [Usage Examples](#usage-examples) - [Routing and Navigation](#routing-and-navigation) - [Filtering and Search](#filtering-and-search) - [State Management](#state-management) - [Architecture Notes](#architecture-notes) - [Dependencies](#dependencies) - [Testing](#testing) - [Best Practices](#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 ```typescript 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: ```typescript // 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 ```typescript 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**: ```typescript // 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**: ```typescript // 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`: ```typescript // 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: ```typescript // 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: ```typescript // 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 cart - `SelectedCustomerBonusCardsResource` - Resource for fetching customer bonus cards - `provideFilter()` - 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:** ```typescript // 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:** ```typescript // Automatically called on component initialization // Sets: TabMetadata.context = 'reward' // Used by customer search to customize behavior ``` **Host Classes:** ```css /* 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` All bonus cards for the selected customer. #### `primaryBonusCard: Signal` The primary bonus card (used for point balance and customer name). **Template Logic:** ```typescript // Conditional rendering based on customer state @if (!cardFetching) { @if (card) { // Customer selected } @else { // No customer } } @else { // Loading skeleton } ``` --- ### RewardCustomerCardComponent Displays selected customer information, available points, and checkout options. **Selector:** `reward-customer-card` **Computed Signals:** #### `bonusCards: Signal` Array of bonus cards for the customer. #### `primaryBonusCard: Signal` The primary bonus card with customer details. #### `primaryBonusCardPoints: Signal` Available reading points (Lesepunkte) for redemption. **Default:** `0` #### `cartItemsLength: Signal` 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:** ```typescript // Called when user clicks "Zurücksetzen" button resetCustomerAndCart(); // Customer: undefined // Shopping cart: undefined // Header: Shows "Kund*in auswählen" again ``` **Host Classes:** ```css 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.firstName` and `lastName` - 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:** ```typescript { path: ['/kunde', 123, 'customer', { outlets: { primary: 'search', side: 'search-customer-main' } }], queryParams: { filter_customertype: 'webshop&loyalty;loyalty&!webshop' } } ``` **Host Classes:** ```css 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` Two-way binding signal that triggers searches and pagination. **Type:** `'scan' | 'input' | 'filter' | 'orderBy' | 'reload' | 'initial'` **Default:** `'initial'` **Example:** ```typescript ``` **Computed Signals:** #### `listFetching: Signal` Whether the catalog resource is currently loading data. #### `items: Signal` Array of reward items to display. #### `itemsLength: Signal` Number of items currently loaded. #### `hits: Signal` Total number of items matching current filters. #### `renderSearchLoader: Signal` Whether to show the initial search loader (when fetching with no items). #### `renderPageTrigger: Signal` 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:** ```typescript // 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:** ```typescript // Called when user clicks "Filter zurücksetzen" in empty state resetFilter(); // All filters cleared // Query params updated // New search triggered ``` **Host Classes:** ```css 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` The reward item to display. **Required:** Yes **Type:** `Item` from `@isa/catalogue/data-access` **Example:** ```typescript ``` **Computed Signals:** #### `desktopBreakpoint: Signal` 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:** ```html ``` **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` The reward item associated with this selection control. **Required:** Yes **Computed Signals:** #### `itemSelected: Signal` 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:** ```typescript if (selected) { store.selectItem(itemId, item); // Add to selection } else { store.removeItem(itemId); // Remove from selection } ``` **Example:** ```typescript ``` --- ### RewardActionComponent Fixed bottom action bar with the "Prämie auswählen" (Select Reward) button. **Selector:** `reward-action` **Computed Signals:** #### `bonusCards: Signal` Customer's bonus cards. #### `primaryBonusCard: Signal` Primary bonus card with customer details. #### `primaryBonusCardPoints: Signal` Available reading points for redemption. #### `selectedItems: Signal>` Dictionary of selected items keyed by item ID. #### `hasSelectedItems: Signal` Whether any items are currently selected. **Methods:** #### `async continueToPurchasingOptions(): Promise` Opens the purchase options modal with selected items. **Workflow:** 1. Retrieves selected items from store 2. Gets or creates reward shopping cart ID 3. Opens purchase options modal with redemption points enabled 4. Waits for modal result 5. Clears selected items on success 6. Navigates to customer search if no customer and user didn't choose "continue-shopping" **Example:** ```typescript 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:** ```typescript !hasSelectedItems() || // No items selected (!!primaryBonusCard() && primaryBonusCardPoints() <= 0) // Customer has 0 points ``` ## Usage Examples ### Navigating to Reward Catalog ```typescript import { Component, inject } from '@angular/core'; import { Router } from '@angular/router'; import { injectTabId } from '@isa/core/tabs'; @Component({ selector: 'app-navigation-example', template: ` ` }) export class NavigationExampleComponent { #router = inject(Router); tabId = injectTabId(); async openRewardCatalog(): Promise { await this.#router.navigate(['/kunde', this.tabId(), 'reward']); } } ``` ### Accessing Reward Catalog with Deep Link ```typescript // Direct navigation with filter and search parameters async navigateWithFilters(): Promise { await this.#router.navigate( ['/kunde', this.tabId(), 'reward'], { queryParams: { search: 'book', filter_category: 'literature', orderBy: 'points_asc' } } ); } ``` ### Programmatic Customer Selection ```typescript 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 ```typescript 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 ```typescript 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()) { } @else {

Customer has no points available

} ` }) 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 ```typescript import { Component, inject, computed, effect } from '@angular/core'; import { SelectedRewardShoppingCartResource } from '@isa/checkout/data-access'; @Component({ selector: 'app-cart-monitor-example', template: `
Rewards in cart: {{ cartItemCount() }} Total points: {{ totalPoints() }}
` }) 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: ```typescript // 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 ```typescript // 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: ```typescript // 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:** ```typescript 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: ```typescript // 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: ```typescript // 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: ```typescript provideFilter( withQuerySettingsFactory(querySettingsFactory), // Pre-loads filter config withQueryParamsSync() // Syncs to URL query params ) ``` ### Filter Capabilities #### 1. Text Search ```typescript // URL: ?search=book // Searches across: - Item names - Product descriptions - EAN/ISBN codes ``` #### 2. Category Filtering ```typescript // URL: ?filter_category=literature // Filters by product category ``` #### 3. Point Range Filtering ```typescript // URL: ?filter_points_min=100&filter_points_max=500 // Filters items by redemption point range ``` #### 4. Sorting ```typescript // 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: ```typescript 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:** ```typescript // 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 ```typescript // 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 ```typescript // 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: ```html ``` ## State Management ### RewardCatalogStore The library uses `RewardCatalogStore` from `@isa/checkout/data-access` for state management. **Store Schema:** ```typescript interface RewardCatalogState { items: Item[]; // Current catalog items hits: number; // Total items matching filters selectedItems: Record; // Selected items by ID } ``` **Store Methods:** #### `setItems(items: Item[], hits: number): void` Replaces all items (used for new searches). ```typescript // Called when searchTrigger !== 'reload' store.setItems(newItems, totalHits); ``` #### `updateItems(items: Item[], hits: number): void` Appends items to existing list (used for pagination). ```typescript // Called when searchTrigger === 'reload' store.updateItems(nextPageItems, totalHits); ``` #### `selectItem(itemId: number, item: Item): void` Adds an item to the selection. ```typescript store.selectItem(123, rewardItem); // selectedItems: { 123: rewardItem } ``` #### `removeItem(itemId: number): void` Removes an item from the selection. ```typescript store.removeItem(123); // selectedItems: {} ``` #### `clearSelectedItems(): void` Clears all selected items. ```typescript // Called after successful purchase options store.clearSelectedItems(); // selectedItems: {} ``` #### `clearState(): void` Clears all catalog items and hits (preserves selectedItems). ```typescript // Called when starting a new search store.clearState(); // items: [], hits: 0 ``` ### Session Storage Persistence The store includes session storage integration (currently disabled): ```typescript // 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: ```typescript // Provided at RewardCatalogComponent level // Automatically fetches cart based on CheckoutMetadataService.rewardShoppingCartId // Updates reactive signals when cart changes ``` **Resource Signals:** ```typescript resource.value() // Shopping cart data resource.isLoading() // Loading state resource.error() // Error state ``` ### Customer Bonus Cards Resource `SelectedCustomerBonusCardsResource` manages customer bonus card data: ```typescript // Provided at RewardCatalogComponent level // Automatically fetches based on CrmTabMetadataService.selectedCustomerId // Returns all bonus cards for the customer ``` **Resource Signals:** ```typescript 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: ```typescript 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: ```typescript // 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: ```typescript 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 1. **OnPush Change Detection** - All components use OnPush for minimal re-renders 2. **Deferred Rendering** - List items use `@defer (on viewport)` for lazy loading 3. **Linked Signals** - Computed values only recalculate when dependencies change 4. **Resource Caching** - Shopping cart and bonus cards fetched once per session 5. **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` ```typescript // 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 ```bash # 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:** ```typescript 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 ```typescript 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 ```typescript 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 ```typescript 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: ```html ``` ## Best Practices ### 1. Always Use Tab Context When navigating to the reward catalog, ensure tab context is available: ```typescript // 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: ```typescript // Good const hasPoints = computed(() => { const points = this.primaryBonusCard()?.totalPoints ?? 0; return points > 0; }); // Bad (no validation) ``` ### 3. Clean Up on Customer Change Always clear reward context when customer changes: ```typescript // 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: ```typescript // 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: ```typescript // 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: ```typescript // Good desktopBreakpoint = breakpoint([ Breakpoint.Desktop, Breakpoint.DesktopL, Breakpoint.DesktopXL ]); orientation = linkedSignal(() => this.desktopBreakpoint() ? 'horizontal' : 'vertical' ); // Bad (CSS hidden classes)
...
``` ### 7. Preserve Filter State in URLs Always use query parameters for filter state to enable bookmarking and sharing: ```typescript // 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: ```typescript // 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: ```typescript // 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: ```typescript // 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.