# @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<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:**

```typescript
// 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:**
```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<SearchTrigger | 'reload' | 'initial'>`

Two-way binding signal that triggers searches and pagination.

**Type:** `'scan' | 'input' | 'filter' | 'orderBy' | 'reload' | 'initial'`

**Default:** `'initial'`

**Example:**
```typescript
<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:**
```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<Item>`

The reward item to display.

**Required:** Yes

**Type:** `Item` from `@isa/catalogue/data-access`

**Example:**
```typescript
<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:**

```html
<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:**
```typescript
if (selected) {
  store.selectItem(itemId, item);  // Add to selection
} else {
  store.removeItem(itemId);        // Remove from selection
}
```

**Example:**
```typescript
<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:**
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: `
    <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

```typescript
// 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

```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()) {
      <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

```typescript
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:

```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
<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:**

```typescript
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).

```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
<!-- 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:

```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;
});

<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:

```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)
<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:

```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.
