# @isa/oms/feature/return-search

A comprehensive return search feature library for Angular applications, providing intelligent receipt search, filtering, and navigation capabilities for the Order Management System (OMS).

## Overview

The Return Search Feature library provides a complete user interface for searching and browsing receipt records in the return process workflow. It offers advanced filtering, sorting, and pagination capabilities with intelligent navigation patterns, automatic single-result handling, and responsive design. The library integrates with the OMS data access layer and shared filter system to deliver a seamless search experience.

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [Component Reference](#component-reference)
- [Routing Configuration](#routing-configuration)
- [Usage Examples](#usage-examples)
- [State Management](#state-management)
- [Filter Integration](#filter-integration)
- [Responsive Design](#responsive-design)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)
- [Dependencies](#dependencies)

## Features

- **Multi-criteria search** - Search by receipt number, email, customer name, or QR code
- **Advanced filtering** - Dynamic filter menu with configurable filter inputs
- **Flexible sorting** - Order-by toolbar with customizable sort options
- **Infinite scrolling** - Automatic pagination with viewport-based loading
- **Intelligent navigation** - Auto-redirect to receipt details when only one result found
- **Responsive design** - Adaptive layouts for mobile, tablet, and desktop screens
- **State persistence** - Filter state synchronized with query parameters
- **Scroll restoration** - Automatic scroll position restoration when navigating back
- **Task list integration** - Display ongoing return tasks alongside search interface
- **Empty state handling** - User-friendly messages for no results scenarios
- **Loading indicators** - Progressive loading states for search, pagination, and items
- **QR code scanning** - Support for scanning receipt QR codes from emails

## Quick Start

### 1. Import Routes

```typescript
import { Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'returns',
    loadChildren: () =>
      import('@isa/oms/feature/return-search').then((m) => m.routes),
  },
];
```

### 2. Navigate to Search

```typescript
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-returns-dashboard',
  template: `
    <button (click)="startReturnSearch()">Start Return Search</button>
  `
})
export class ReturnsDashboardComponent {
  #router = inject(Router);

  startReturnSearch() {
    this.#router.navigate(['/returns']);
  }
}
```

### 3. Basic Search Flow

The search flow is automatically handled by the library:

1. User enters search term (receipt number, email, or customer name)
2. User clicks search or presses Enter
3. If exactly 1 result: Navigate directly to receipt details
4. If multiple results: Display results list with filters and sorting
5. User clicks result: Navigate to receipt details

## Core Concepts

### Search Process

The return search follows a multi-step process managed by the `ReturnSearchStore`:

1. **Query Building** - Filter inputs are collected and combined into a search query
2. **Query Execution** - The query is sent to the backend via `ReturnSearchService`
3. **Result Handling** - Results are stored in the NgRx Signals store per process ID
4. **Navigation Decision** - Single results trigger automatic navigation
5. **Pagination** - Additional results loaded via infinite scroll

### Process ID and Tabs

The library uses a **tab-based architecture** where each search session is identified by a unique `processId`:

```typescript
// Each browser tab gets a unique process ID
private _processId = injectTabId();

// Search results are stored per process ID
this._returnSearchStore.search({
  processId,
  query: this._filterService.query(),
  clear: true
});

// Entity retrieval uses the process ID
const entity = this._returnSearchStore.getEntity(processId);
```

This allows multiple concurrent search sessions across different browser tabs without data conflicts.

### Filter System Integration

The library integrates with `@isa/shared/filter` for advanced filtering:

- **Query Settings Resolver** - Loads filter configuration from backend
- **Query Params Sync** - Synchronizes filter state with URL query parameters
- **Filter Groups** - Separates search input from filter inputs
- **Commit Pattern** - Changes are committed before search execution

### Search Status States

Search operations progress through defined states:

```typescript
enum ReturnSearchStatus {
  Idle = 'idle',           // No search performed yet
  Pending = 'pending',     // Search in progress
  Success = 'success',     // Search completed successfully
  Error = 'error'          // Search failed
}
```

## Component Reference

### ReturnSearchComponent

Root component providing filter context and routing outlet.

**Selector:** `oms-feature-return-search`

**Template:** Simple router outlet for child routes

**Providers:**
- `provideFilter()` - Filter service with query settings factory and params sync

**Host Classes:**
```css
flex flex-col gap-5 isa-desktop:gap-6 items-center overflow-x-hidden
```

**Usage:**
```typescript
// Automatically used when routes are loaded
// No direct usage required
```

---

### ReturnSearchMainComponent

Main search interface with search bar, filters, and task list.

**Selector:** `oms-feature-return-search-main`

**Inputs:** None (uses injected services)

**Outputs:** None (navigation-based)

**Key Features:**
- Search bar with QR code scanning support
- Dynamic filter input buttons
- Pending state indicator
- Task list integration
- Automatic navigation based on result count

**Template Structure:**
```html
<div class="search-header">
  <h1>Rückgabe starten</h1>
  <filter-search-bar-input (triggerSearch)="onSearch()">
  <filter-input-menu-button *ngFor="let filter of filterInputs()">
</div>
<oms-shared-return-task-list>
```

**Computed Signals:**

| Signal | Type | Description |
|--------|------|-------------|
| `entityPending` | `boolean` | True when search is in pending state |
| `filterInputs` | `FilterInput[]` | Filtered list of filter inputs (group='filter') |

**Methods:**

#### `onSearch(): void`

Initiates a new search with current filter values.

**Behavior:**
- Commits current filter state
- Clears previous results
- Executes search with callback
- Automatically navigates based on result count

**Example:**
```typescript
// Triggered by search bar or filter changes
onSearch() {
  const processId = this._processId();
  if (processId) {
    this._filterService.commit();
    this._returnSearchStore.search({
      processId,
      query: this._filterService.query(),
      clear: true,
      cb: this.onSearchCb
    });
  }
}
```

#### `onSearchCb(result: CallbackResult<ListResponseArgs<ReceiptListItem>>): void`

Callback for search results, handles automatic navigation.

**Navigation Rules:**
- **1 result**: Navigate to `receipt/:id`
- **Multiple results**: Navigate to `receipts` list
- **0 results**: Stay on search page (handled by result component)

---

### ReturnSearchResultComponent

Results list with filtering, sorting, and infinite scroll pagination.

**Selector:** `oms-feature-return-search-result`

**Inputs:** None (uses route data and injected services)

**Outputs:** None (navigation-based)

**Key Features:**
- Responsive filter and sort controls
- Infinite scroll pagination with viewport detection
- Scroll position restoration
- Empty state handling
- Progressive loading indicators
- Mobile-optimized layout

**Template Structure:**
```html
<filter-search-bar-input (triggerSearch)="search()">
<filter-filter-menu-button>
<filter-order-by-toolbar>

<span>{{ entityHits() }} Einträge</span>

@for (item of entityItems(); track item.id) {
  <a [routerLink]="['../', 'receipt', item.id]">
    <oms-feature-return-search-result-item [item]="item">
  </a>
}

@if (renderPageTrigger()) {
  <div (uiInViewport)="paging($event)"></div>
}
```

**Computed Signals:**

| Signal | Type | Description |
|--------|------|-------------|
| `entity` | `ReturnSearchEntity \| undefined` | Current search entity for this process |
| `entityItems` | `ReceiptListItem[]` | List of receipt items in current search |
| `entityHits` | `number` | Total number of search results |
| `entityStatus` | `ReturnSearchStatus` | Current status of search operation |
| `renderItemList` | `boolean` | Whether to render the item list |
| `renderPagingLoader` | `boolean` | Whether to show pagination loading indicator |
| `renderSearchLoader` | `boolean` | Whether to show initial search loader |
| `renderPageTrigger` | `boolean` | Whether to render pagination trigger element |
| `mobileBreakpoint` | `boolean` | True when viewport is tablet size or below |
| `showOrderByToolbarMobile` | `boolean` | Controls order-by toolbar visibility on mobile |

**Methods:**

#### `search(): void`

Initiates a new search, clearing previous results.

**Behavior:**
- Commits filter changes
- Clears existing results
- Navigates to receipt if only 1 result

#### `paging(inViewport: boolean): void`

Handles infinite scroll pagination when trigger enters viewport.

**Parameters:**
- `inViewport: boolean` - Whether pagination trigger is visible

**Behavior:**
- Only triggers when element is in viewport
- Appends results to existing list (clear: false)
- Uses same filter query as original search

**Example:**
```html
<!-- Pagination trigger -->
@if (renderPageTrigger()) {
  <div (uiInViewport)="paging($event)"></div>
}
```

#### `navigate(path: (string | number)[]): void`

Navigates to a route while preserving query parameters.

**Parameters:**
- `path: (string | number)[]` - Route segments

**Behavior:**
- Navigates relative to current route
- Preserves all filter query parameters

#### `ngAfterViewInit(): void`

Lifecycle hook that restores scroll position after view initialization.

---

### ReturnSearchResultItemComponent

Individual result item displaying receipt information.

**Selector:** `oms-feature-return-search-result-item`

**Inputs:**

| Input | Type | Required | Description |
|-------|------|----------|-------------|
| `item` | `ReceiptListItem` | Yes | Receipt data to display |

**Outputs:** None (clickable container handled by parent)

**Template Structure:**
```html
<ui-client-row>
  <ui-client-row-content>
    <h3>{{ name() }}</h3>
  </ui-client-row-content>

  <ui-item-row-data>
    <ui-item-row-data-row>
      <ui-item-row-data-label>Belegdatum</ui-item-row-data-label>
      <ui-item-row-data-value>{{ receiptDate() | date }}</ui-item-row-data-value>
    </ui-item-row-data-row>
    <!-- Receipt number, order number -->
  </ui-item-row-data>

  <ui-item-row-data>
    <!-- Email, address -->
  </ui-item-row-data>
</ui-client-row>
```

**Computed Signals:**

| Signal | Type | Description |
|--------|------|-------------|
| `name` | `string` | Formatted customer name (person or organization) |
| `receiptDate` | `string \| undefined` | Date when receipt was printed |
| `receiptNumber` | `string \| undefined` | Receipt number identifier |
| `orderNumber` | `string \| undefined` | Order number identifier |
| `email` | `string \| undefined` | Customer email from billing info |
| `address` | `string` | Formatted address (ZIP + City) from billing or shipping |

**Address Resolution Logic:**
```typescript
// Prefers billing address, falls back to shipping address
address = computed(() => {
  const address = this.item()?.billing?.address ?? this.item()?.shipping?.address;
  return address ? [address.zipCode, address.city].join(' ') : '';
});
```

**E2E Attributes:**
```html
<ui-client-row
  data-what="search-result-item"
  [attr.data-which]="receiptNumber()"
>
```

**Usage:**
```typescript
@for (item of items; track item.id) {
  <a [routerLink]="['receipt', item.id]">
    <oms-feature-return-search-result-item [item]="item" />
  </a>
}
```

---

### querySettingsResolverFn

Route resolver that loads filter configuration from backend.

**Type:** `ResolveFn<QuerySettingsDTO>`

**Purpose:** Fetches query settings before route activation to configure filter system

**Returns:** `QuerySettingsDTO` with filter field definitions

**Usage:**
```typescript
{
  path: '',
  component: ReturnSearchComponent,
  resolve: { querySettings: querySettingsResolverFn }
}
```

**Data Flow:**
1. Router activates route
2. Resolver fetches query settings via `ReturnSearchService.querySettings()`
3. Query settings passed to component via `ActivatedRoute.snapshot.data`
4. Filter system configured with settings

## Routing Configuration

### Route Structure

```typescript
export const routes: Routes = [
  {
    path: '',
    component: ReturnSearchComponent,
    resolve: { querySettings: querySettingsResolverFn },
    children: [
      {
        path: '',
        component: ReturnSearchMainComponent
      },
      {
        path: 'receipts',
        component: ReturnSearchResultComponent,
        data: { scrollPositionRestoration: true }
      }
    ]
  },
  {
    path: 'receipt',
    loadChildren: () =>
      import('@isa/oms/feature/return-details').then((feat) => feat.routes)
  },
  {
    path: 'process',
    loadChildren: () =>
      import('@isa/oms/feature/return-process').then((feat) => feat.routes)
  }
];
```

### Route Hierarchy

```
/returns
├── ''                    → ReturnSearchMainComponent (search page)
├── receipts              → ReturnSearchResultComponent (results list)
├── receipt/:id           → Lazy-loaded return-details feature
└── process/:id           → Lazy-loaded return-process feature
```

### Navigation Patterns

#### From Search to Results

```typescript
// Automatic navigation based on result count
onSearchCb = ({ data }) => {
  if (data) {
    if (data.result.length === 1) {
      // Direct navigation to receipt details
      return this.navigate(['receipt', data.result[0].id]);
    }
    // Multiple results - show list
    return this.navigate(['receipts']);
  }
};
```

#### From Results to Receipt Details

```html
<!-- User clicks on result item -->
<a [routerLink]="['../', 'receipt', item.id]">
  <oms-feature-return-search-result-item [item]="item">
</a>
```

#### Query Parameter Preservation

All navigation preserves filter state:

```typescript
navigate(path: (string | number)[]) {
  this.#router.navigate(path, {
    relativeTo: this.#route,
    queryParams: this._filterService.queryParams()  // Preserves filters
  });
}
```

### Scroll Position Restoration

The results component uses scroll restoration:

```typescript
// Route data enables restoration
{
  path: 'receipts',
  component: ReturnSearchResultComponent,
  data: { scrollPositionRestoration: true }
}

// Component restores position after view init
ngAfterViewInit(): void {
  this.restoreScrollPosition();
}
```

## Usage Examples

### Basic Search Integration

```typescript
import { Component } from '@angular/core';

@Component({
  selector: 'app-returns-page',
  template: `
    <router-outlet></router-outlet>
  `
})
export class ReturnsPageComponent {}

// In app routing:
const routes: Routes = [
  {
    path: 'returns',
    component: ReturnsPageComponent,
    loadChildren: () =>
      import('@isa/oms/feature/return-search').then((m) => m.routes)
  }
];
```

### Triggering Search Programmatically

While the library handles search internally, you can navigate to it with pre-filled filters:

```typescript
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-dashboard',
  template: `
    <button (click)="searchByEmail('customer@example.com')">
      Search by Email
    </button>
  `
})
export class DashboardComponent {
  #router = inject(Router);

  searchByEmail(email: string) {
    this.#router.navigate(['/returns'], {
      queryParams: { qs: email }  // Pre-fill search query
    });
  }
}
```

### Handling Search Results

The library automatically handles navigation, but you can monitor state:

```typescript
import { Component, inject, computed } from '@angular/core';
import { ReturnSearchStore, ReturnSearchStatus } from '@isa/oms/data-access';
import { injectTabId } from '@isa/core/tabs';

@Component({
  selector: 'app-custom-search',
  template: `
    @if (isSearching()) {
      <p>Searching...</p>
    }
    @if (resultCount() > 0) {
      <p>Found {{ resultCount() }} results</p>
    }
  `
})
export class CustomSearchComponent {
  #returnSearchStore = inject(ReturnSearchStore);
  #processId = injectTabId();

  entity = computed(() => {
    const processId = this.#processId();
    return processId ? this.#returnSearchStore.getEntity(processId) : undefined;
  });

  isSearching = computed(() =>
    this.entity()?.status === ReturnSearchStatus.Pending
  );

  resultCount = computed(() =>
    this.entity()?.hits ?? 0
  );
}
```

### Custom Filter Configuration

The filter system is configured via backend query settings:

```typescript
// Backend returns QuerySettingsDTO
interface QuerySettingsDTO {
  fields: Array<{
    key: string;
    label: string;
    type: 'text' | 'date' | 'select';
    group?: 'filter' | 'search';
    options?: Array<{ value: string; label: string }>;
  }>;
  orderBy: Array<{
    key: string;
    label: string;
    direction: 'asc' | 'desc';
  }>;
}

// Example usage in ReturnSearchService:
export class ReturnSearchService {
  querySettings(): Promise<QuerySettingsDTO> {
    return this.#http.get('/api/returns/query-settings').toPromise();
  }
}
```

### Implementing Custom Result Item

You can extend the result item component:

```typescript
import { Component, input, computed } from '@angular/core';
import { ReturnSearchResultItemComponent } from '@isa/oms/feature/return-search';

@Component({
  selector: 'app-custom-result-item',
  template: `
    <oms-feature-return-search-result-item [item]="item()">
    </oms-feature-return-search-result-item>

    <!-- Add custom content -->
    <div class="custom-badge">
      {{ customStatus() }}
    </div>
  `,
  imports: [ReturnSearchResultItemComponent]
})
export class CustomResultItemComponent {
  item = input.required<ReceiptListItem>();

  customStatus = computed(() => {
    const returnCount = this.item()?.returnCount ?? 0;
    return returnCount > 0 ? 'Has Returns' : 'No Returns';
  });
}
```

### Responsive Breakpoint Handling

The library uses responsive breakpoints for adaptive layouts:

```typescript
import { Component } from '@angular/core';
import { breakpoint, Breakpoint } from '@isa/ui/layout';

@Component({
  selector: 'app-return-search-custom',
  template: `
    @if (isMobile()) {
      <!-- Mobile layout -->
      <ui-icon-button (click)="toggleSort()"></ui-icon-button>
    } @else {
      <!-- Desktop layout -->
      <filter-order-by-toolbar></filter-order-by-toolbar>
    }
  `
})
export class ReturnSearchCustomComponent {
  isMobile = breakpoint([Breakpoint.Tablet]);
}
```

### Infinite Scroll Pagination

Pagination is handled automatically via viewport detection:

```html
<!-- Pagination trigger in template -->
@if (renderPageTrigger()) {
  <div (uiInViewport)="paging($event)"></div>
}
```

```typescript
// Handling pagination
paging(inViewport: boolean) {
  if (!inViewport) return;

  const processId = this.processId();
  if (processId) {
    this.returnSearchStore.search({
      processId,
      clear: false,          // Append results
      query: this.#filterService.query()
    });
  }
}
```

### Empty State Customization

The library provides a default empty state, but you can customize it:

```html
@if (entityItems().length === 0 && !isSearching()) {
  <ui-empty-state
    title="Keine Rückgaben gefunden"
    description="Versuchen Sie es mit einer anderen Suchbegriff."
  >
    <button (click)="clearFilters()">Filter zurücksetzen</button>
  </ui-empty-state>
}
```

## State Management

### ReturnSearchStore

The library uses `ReturnSearchStore` from `@isa/oms/data-access` for state management:

```typescript
interface ReturnSearchEntity {
  processId: string;
  status: ReturnSearchStatus;
  query: Record<string, any>;
  items: ReceiptListItem[];
  hits: number;
  page: number;
  error?: string;
}

// Store methods
class ReturnSearchStore {
  // Search for receipts
  search(params: {
    processId: string;
    query: Record<string, any>;
    clear: boolean;
    cb?: (result: CallbackResult) => void;
  }): void;

  // Get entity by process ID
  getEntity(processId: string): ReturnSearchEntity | undefined;

  // Clear entity
  clearEntity(processId: string): void;
}
```

### State Flow

```
User Action → Filter Service → Search Store → Backend API
                                    ↓
                              Store Update
                                    ↓
                          Component Re-render
                                    ↓
                         Navigation Decision
```

### Process ID Scoping

Each browser tab maintains its own search state:

```typescript
// Tab 1: processId = "tab-123"
// Search results stored in: entities['tab-123']

// Tab 2: processId = "tab-456"
// Search results stored in: entities['tab-456']

// No collision between tabs
```

### Query State Synchronization

Filter state is synchronized with URL query parameters:

```
URL: /returns/receipts?qs=john&date=2024-01-01&status=open

Filter State:
{
  qs: 'john',
  date: '2024-01-01',
  status: 'open'
}
```

**Benefits:**
- Shareable search URLs
- Browser back/forward navigation
- Tab refresh preserves search
- Bookmark support

## Filter Integration

### Filter System Architecture

The library integrates with `@isa/shared/filter`:

```typescript
// Provider setup in root component
providers: [
  provideFilter(
    withQuerySettingsFactory(querySettingsFactory),  // Load config
    withQueryParamsSync()                            // Sync with URL
  )
]

// Resolver loads configuration
function querySettingsFactory() {
  return inject(ActivatedRoute).snapshot.data['querySettings'];
}
```

### Filter Input Types

The filter system supports multiple input types:

```typescript
interface FilterInput {
  key: string;          // Query parameter key
  label: string;        // Display label
  type: FilterInputType;
  group?: 'search' | 'filter';
  options?: FilterOption[];
}

type FilterInputType =
  | 'text'              // Text input
  | 'date'              // Date picker
  | 'dateRange'         // Date range picker
  | 'select'            // Dropdown
  | 'multiSelect'       // Multi-select dropdown
  | 'checkbox'          // Checkbox group
  | 'radio';            // Radio group
```

### Search Bar Integration

The search bar is configured with a specific input key:

```html
<filter-search-bar-input
  inputKey="qs"                    <!-- Query string parameter -->
  (triggerSearch)="onSearch()"     <!-- Search trigger -->
  [appearance]="'results'"         <!-- Visual style -->
></filter-search-bar-input>
```

**Query Parameter Mapping:**
```
URL: /returns?qs=john+doe
       ↓
Filter State: { qs: 'john doe' }
       ↓
Search Query: { qs: 'john doe' }
```

### Filter Menu Button

Dynamic filter buttons are generated from query settings:

```typescript
// Component extracts filter-group inputs
filterInputs = computed(() =>
  this._filterService.inputs().filter(input => input.group === 'filter')
);
```

```html
<!-- Render filter buttons -->
@for (filterInput of filterInputs(); track filterInput.key) {
  <filter-input-menu-button
    [filterInput]="filterInput"
    (applied)="onSearch()"
  ></filter-input-menu-button>
}
```

### Order-By (Sort) Integration

Sorting is handled by the order-by toolbar:

```html
<!-- Desktop: Always visible -->
<filter-order-by-toolbar
  (toggled)="search()"
></filter-order-by-toolbar>

<!-- Mobile: Toggle visibility -->
@if (mobileBreakpoint() && showOrderByToolbarMobile()) {
  <filter-order-by-toolbar
    (toggled)="search()"
  ></filter-order-by-toolbar>
}
```

**Sort Query Parameters:**
```
URL: /returns/receipts?orderBy=receiptDate&direction=desc

Sort State:
{
  orderBy: 'receiptDate',
  direction: 'desc'
}
```

### Commit Pattern

Filters use a commit pattern to control when changes are applied:

```typescript
onSearch() {
  // 1. Commit pending filter changes
  this._filterService.commit();

  // 2. Get committed query
  const query = this._filterService.query();

  // 3. Execute search
  this._returnSearchStore.search({ processId, query, clear: true });
}
```

**Benefits:**
- User can modify multiple filters
- Changes don't trigger search until committed
- Rollback support for filter menu

## Responsive Design

### Breakpoint Strategy

The library uses `@isa/ui/layout` breakpoints:

```typescript
import { breakpoint, Breakpoint } from '@isa/ui/layout';

// Mobile breakpoint (tablet and below)
mobileBreakpoint = breakpoint([Breakpoint.Tablet]);
```

**Breakpoint Definitions:**
- `Tablet`: `(max-width: 1279px)` - Mobile and tablet
- `Desktop`: `(min-width: 1280px) and (max-width: 1439px)` - Standard desktop
- `DesktopL`: `(min-width: 1440px) and (max-width: 1919px)` - Large desktop
- `DesktopXL`: `(min-width: 1920px)` - Extra large desktop

### Mobile Adaptations

#### Sort Toolbar Toggle

```html
<!-- Mobile: Icon button to toggle toolbar -->
@if (mobileBreakpoint()) {
  <ui-icon-button
    (click)="showOrderByToolbarMobile.set(!showOrderByToolbarMobile())"
    [class.active]="showOrderByToolbarMobile()"
    name="isaActionSort"
  ></ui-icon-button>
}

<!-- Mobile: Collapsible toolbar -->
@if (mobileBreakpoint() && showOrderByToolbarMobile()) {
  <filter-order-by-toolbar></filter-order-by-toolbar>
}

<!-- Desktop: Always visible inline toolbar -->
@else {
  <filter-order-by-toolbar></filter-order-by-toolbar>
}
```

#### Responsive Grid Layout

The main search component uses responsive grids:

```scss
:host {
  @apply w-full
         grid gap-16 grid-flow-row justify-center
         desktop:grid-cols-[1fr,auto] desktop:gap-6;
}
```

**Mobile:** Single column, vertical flow, 16px gap
**Desktop:** Two-column layout, 6px gap

### Tailwind Responsive Classes

The library uses Tailwind CSS responsive utilities:

```html
<!-- Gap changes by breakpoint -->
<div class="flex flex-col gap-5 isa-desktop:gap-6">

<!-- Grid layout changes -->
<div class="grid-flow-row desktop:grid-cols-[1fr,auto]">
```

### Deferred Rendering

List items use Angular's `@defer` for performance:

```html
@for (item of entityItems(); track item.id) {
  @defer (on viewport) {
    <oms-feature-return-search-result-item [item]="item">
  } @placeholder {
    <ui-icon-button [pending]="true"></ui-icon-button>
  }
}
```

**Benefits:**
- Only renders items when in viewport
- Reduces initial render time
- Improves scroll performance
- Shows loading placeholder

## Testing

The library uses **Jest** with **Spectator** for testing.

### Running Tests

```bash
# Run tests for this library
npx nx test oms-feature-return-search --skip-nx-cache

# Run tests with coverage
npx nx test oms-feature-return-search --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test oms-feature-return-search --watch
```

### Test Structure

Tests cover:

- **Component rendering** - Verifies components render correctly
- **Signal computations** - Tests computed signal logic
- **User interactions** - Tests button clicks, form inputs, navigation
- **Empty states** - Tests no-data scenarios
- **Responsive behavior** - Tests breakpoint-dependent rendering
- **Navigation logic** - Tests automatic navigation based on result count
- **Filter integration** - Tests filter service interaction

### Example Test

```typescript
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { ReturnSearchResultItemComponent } from './return-search-result-item.component';
import { ReceiptListItem } from '@isa/oms/data-access';

describe('ReturnSearchResultItemComponent', () => {
  let spectator: Spectator<ReturnSearchResultItemComponent>;
  const createComponent = createComponentFactory(
    ReturnSearchResultItemComponent
  );

  const mockItem: ReceiptListItem = {
    billing: {
      person: { firstName: 'John', lastName: 'Doe' },
      address: { zipCode: '12345', city: 'Berlin' },
      communicationDetails: { email: 'john@example.com' }
    },
    receiptNumber: 'R-12345',
    orderNumber: 'O-67890',
    printedDate: '2024-01-15T10:00:00Z'
  } as ReceiptListItem;

  it('should render receipt information', () => {
    spectator = createComponent({ props: { item: mockItem } });

    expect(spectator.component.receiptNumber()).toBe('R-12345');
    expect(spectator.component.orderNumber()).toBe('O-67890');
    expect(spectator.component.email()).toBe('john@example.com');
  });

  it('should format address from billing', () => {
    spectator = createComponent({ props: { item: mockItem } });

    expect(spectator.component.address()).toBe('12345 Berlin');
  });

  it('should fallback to shipping address', () => {
    const item = {
      ...mockItem,
      billing: { ...mockItem.billing, address: undefined },
      shipping: { address: { zipCode: '54321', city: 'Munich' } }
    } as ReceiptListItem;

    spectator = createComponent({ props: { item } });

    expect(spectator.component.address()).toBe('54321 Munich');
  });
});
```

### E2E Testing Attributes

The library includes E2E testing attributes:

```html
<!-- Search input -->
<filter-search-bar-input data-what="search-input">

<!-- Result count -->
<span data-what="result-count">{{ entityHits() }} Einträge</span>

<!-- Result item -->
<ui-client-row
  data-what="search-result-item"
  [attr.data-which]="receiptNumber()"
>

<!-- Loading indicators -->
<ui-icon-button
  data-what="load-spinner"
  data-which="search"
>

<!-- Sort button (mobile) -->
<ui-icon-button data-what="sort-button-mobile">
```

**E2E Test Example:**
```typescript
// Find and interact with search
await page.locator('[data-what="search-input"]').fill('john doe');
await page.locator('[data-what="search-input"]').press('Enter');

// Verify results
const resultCount = await page.locator('[data-what="result-count"]').textContent();
expect(resultCount).toContain('Einträge');

// Click first result
await page.locator('[data-what="search-result-item"]').first().click();
```

## Architecture Notes

### Current Architecture

The library follows a feature-based architecture:

```
ReturnSearchComponent (Root)
    ↓ provides Filter Context
    ├─→ ReturnSearchMainComponent (Search Page)
    │   ├─→ FilterService (injected)
    │   ├─→ ReturnSearchStore (injected)
    │   └─→ ReturnTaskListComponent (shared)
    │
    └─→ ReturnSearchResultComponent (Results Page)
        ├─→ FilterService (injected)
        ├─→ ReturnSearchStore (injected)
        └─→ ReturnSearchResultItemComponent (list items)
```

### Dependency Flow

```
Feature Components
       ↓
  Filter Service (shared/filter)
       ↓
ReturnSearchStore (oms/data-access)
       ↓
ReturnSearchService (oms/data-access)
       ↓
  OMS API Client (generated/swagger)
```

### State Management Pattern

The library uses **NgRx Signals** with entity-based state:

```typescript
// Store structure (conceptual)
{
  entities: {
    'tab-123': {
      processId: 'tab-123',
      status: 'success',
      items: [...],
      hits: 42,
      page: 1
    },
    'tab-456': {
      processId: 'tab-456',
      status: 'pending',
      items: [],
      hits: 0,
      page: 0
    }
  }
}
```

**Benefits:**
- Multi-tab support
- Automatic entity cleanup
- Reactive updates via signals
- Session persistence (if configured)

### Filter Provider Pattern

The library uses Angular's hierarchical dependency injection for filters:

```typescript
// Root component provides filter context
@Component({
  providers: [
    provideFilter(
      withQuerySettingsFactory(querySettingsFactory),
      withQueryParamsSync()
    )
  ]
})
export class ReturnSearchComponent {}

// Child components inject FilterService
export class ReturnSearchMainComponent {
  #filterService = inject(FilterService);  // Same instance
}

export class ReturnSearchResultComponent {
  #filterService = inject(FilterService);  // Same instance
}
```

### Navigation Strategy

The library uses **intelligent navigation** based on result count:

```typescript
// Decision tree
searchResults.length === 1
  ? navigate(['receipt', results[0].id])      // Direct to details
  : navigate(['receipts'])                    // Show list
```

**Benefits:**
- Faster workflow for single results
- No extra click required
- Better user experience
- Consistent behavior

### Known Architectural Considerations

#### 1. TODO: Extract Search Logic to Provider (Medium Priority)

**Current State:**
- Search logic embedded in component methods
- No centralized search orchestration
- Manual handling of cancel and fetching status

**Proposed Refactoring:**
```typescript
// Create SearchProvider in FilterService
class FilterService {
  search(params: SearchParams): Observable<SearchResult> {
    // Centralized search orchestration
    // Automatic cancel previous search
    // Loading state management
  }

  cancelSearch(): void {
    // Cancel in-flight search
  }
}

// Simplified component usage
onSearch() {
  this.#filterService.search({
    query: this.#filterService.query()
  }).subscribe(result => {
    // Handle result
  });
}
```

**Impact:** Reduces component complexity, improves testability

#### 2. Skeleton Loaders for Item Placeholders (Low Priority)

**Current State:**
- Loading spinner for deferred item placeholders
- No visual structure hint

**Proposed Enhancement:**
```html
@defer (on viewport) {
  <oms-feature-return-search-result-item [item]="item">
} @placeholder {
  <!-- Replace spinner with skeleton -->
  <ui-skeleton-loader [layout]="'client-row'">
</ui-skeleton-loader>
}
```

**Impact:** Better perceived performance, clearer loading state

#### 3. Scroll Restoration Configuration (Low Priority)

**Current State:**
- Scroll restoration hardcoded in route data
- No component-level control

**Proposed Enhancement:**
```typescript
// Make restoration configurable
@Input() scrollRestoration: boolean = true;

ngAfterViewInit(): void {
  if (this.scrollRestoration) {
    this.restoreScrollPosition();
  }
}
```

**Impact:** More flexible for different use cases

### Performance Considerations

1. **Deferred Rendering** - List items load on viewport entry
2. **Infinite Scroll** - Pagination reduces initial load time
3. **Computed Signals** - Efficient reactive updates
4. **Track By** - Angular change detection optimization
5. **OnPush Change Detection** - All components use OnPush
6. **Lazy Loaded Routes** - Receipt details and process routes lazy loaded

### Future Enhancements

Potential improvements identified:

1. **Search Debouncing** - Debounce search input to reduce API calls
2. **Result Caching** - Cache search results per query
3. **Advanced Filtering** - Multi-select, date range, numeric range filters
4. **Export Results** - Export search results to CSV/Excel
5. **Saved Searches** - Allow users to save common searches
6. **Search History** - Show recent searches
7. **Keyboard Navigation** - Arrow keys to navigate results
8. **Bulk Actions** - Select multiple results for bulk operations

## Dependencies

### Required Libraries

- `@angular/core` - Angular framework (v20.1.2)
- `@angular/common` - Common Angular utilities
- `@angular/router` - Angular routing
- `@isa/oms/data-access` - OMS data access layer with `ReturnSearchStore` and `ReturnSearchService`
- `@isa/oms/shared/task-list` - Return task list component
- `@isa/shared/filter` - Filter system with search bar and filter inputs
- `@isa/ui/buttons` - Button components
- `@isa/ui/empty-state` - Empty state component
- `@isa/ui/item-rows` - Client row and item row data components
- `@isa/ui/layout` - Layout utilities with breakpoint service
- `@isa/ui/tooltip` - Tooltip components
- `@isa/core/tabs` - Tab ID management
- `@isa/utils/scroll-position` - Scroll position restoration utility
- `@isa/icons` - Icon library
- `@ng-icons/core` - Icon provider
- `@generated/swagger/oms-api` - Generated OMS API client

### Development Dependencies

- `@nx/jest` - Jest test executor
- `@ngneat/spectator` - Testing utilities
- `jest` - Test framework
- `typescript` - TypeScript compiler

### Path Alias

Import from: `@isa/oms/feature/return-search`

**Example:**
```typescript
import { routes } from '@isa/oms/feature/return-search';
```

## License

Internal ISA Frontend library - not for external distribution.
