Files

@isa/oms/shared/task-list

A specialized Angular component library for displaying and managing return receipt item tasks in the OMS (Order Management System) domain.

Overview

The Task List library provides a complete task management interface for return processes, allowing users to view, filter, and complete receipt item tasks. It supports two distinct visual modes (main workflow and review workflow) and integrates tightly with the OMS data-access layer through NgRx Signals stores. The library handles complex task interactions including task completion, status updates (OK/NOK), and Tolino return receipt printing.

Table of Contents

Features

  • Dual appearance modes - Main workflow (card-based) and review workflow (table-based) layouts
  • Task filtering - Automatic filtering of completed tasks in main view
  • Task sorting - Open tasks displayed first, completed tasks at the end
  • Task completion - One-click task completion with optimistic store updates
  • Task status updates - Update task type to OK (verkaufsfähig) or NOK (defekt)
  • Tolino receipt printing - Integrated Tolino return receipt generation
  • NgRx Signals integration - Reactive state management with entity store
  • Tab-based isolation - Process ID derived from tab context for multi-tab support
  • Deferred rendering - Viewport-based lazy loading for performance
  • Product information display - Rich product details for each task item
  • E2E testing attributes - Comprehensive data attributes for automated testing
  • Error handling - Comprehensive logging and error recovery

Quick Start

1. Import and Use in Template

import { Component } from '@angular/core';
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';

@Component({
  selector: 'app-return-search',
  standalone: true,
  imports: [ReturnTaskListComponent],
  template: `
    <oms-shared-return-task-list
      [appearance]="'main'"
    ></oms-shared-return-task-list>
  `
})
export class ReturnSearchComponent {}

2. Main Workflow Usage

<!-- Use in return search workflow - shows incomplete tasks in card layout -->
<oms-shared-return-task-list
  [appearance]="'main'"
></oms-shared-return-task-list>

3. Review Workflow Usage

<!-- Use in return review workflow - shows all tasks in table layout -->
<oms-shared-return-task-list
  [appearance]="'review'"
></oms-shared-return-task-list>

Core Concepts

Task List Workflow

The task list component manages the complete lifecycle of receipt item tasks:

  1. Automatic Data Fetching - Component fetches tasks on initialization using tab-based process ID
  2. Task Display - Tasks rendered with product information and action buttons
  3. User Interaction - Users complete tasks, update task status, or print receipts
  4. Store Updates - Optimistic UI updates via NgRx Signals store
  5. Re-filtering - UI automatically updates based on task completion state

Appearance Modes

The component supports two distinct visual modes optimized for different workflows:

Main Appearance (appearance="main")

  • Purpose: Primary workflow for processing return tasks
  • Layout: Card-based vertical layout (24.25rem width cards)
  • Filtering: Shows only incomplete tasks
  • Styling: White cards with rounded corners and shadows
  • Scrolling: Vertical scroll container for desktop (max-height: calc(100vh - 13rem))
  • Use Case: Return search main view where staff process tasks one by one

Review Appearance (appearance="review")

  • Purpose: Review completed and pending tasks
  • Layout: Grid-based table layout (responsive columns)
  • Filtering: Shows all tasks (completed and incomplete)
  • Styling: Minimal borders, table-row style
  • Scrolling: Natural page scroll
  • Use Case: Return review page where staff review all task states

Task Action Types

Tasks can have three distinct action types that determine available user interactions:

const TaskActionTypes = {
  OK: 'OK',         // Task marked as OK (product sellable)
  NOK: 'NOK',       // Task marked as NOK (product defective)
  UNKNOWN: 'UNKNOWN' // Task needs classification (OK or NOK)
} as const;

OK Tasks

  • Display: "Als erledigt markieren" button
  • Action: Complete task
  • Post-completion: Shows "Abgeschlossen" with green checkmark

NOK Tasks

  • Display: "Als erledigt markieren" button
  • Action: Complete task
  • Post-completion: Shows "Abgeschlossen" with green checkmark

UNKNOWN Tasks

  • Display: Two action buttons (Ja verkaufsfähig / Nein, defekt)
  • Action: Update task type to OK or NOK
  • Post-update: Task re-fetched with new type, becomes completable

Store Integration

The component integrates with ReturnTaskListStore (NgRx Signals) for reactive state management:

// Store structure
type ReturnTaskListItems = AsyncResult<ReceiptItemTaskListItem[]> & {
  id: number;  // Process ID (from tab context)
};

// Entity map structure
{
  [processId: number]: {
    id: number,
    status: AsyncResultStatus,  // Idle | Pending | Success | Error
    data: ReceiptItemTaskListItem[] | undefined,
    error?: unknown
  }
}

Key Store Operations:

  • fetchTaskListItems() - Fetches tasks with query filter
  • updateTaskListItem() - Updates individual task after completion/update
  • beforeFetch() - Sets status to Pending
  • fetchSuccess() - Updates data and sets status to Success
  • fetchError() - Logs error and sets status to Error

Tab-Based Process Isolation

The component uses injectTabId() from @isa/core/tabs to derive the process ID:

processId = injectTabId();  // Returns Signal<number | undefined>

Benefits:

  • Multiple return processes can run simultaneously in different tabs
  • Each tab maintains independent task list state in the store
  • Process ID automatically updates when tab context changes
  • No manual process ID management required

Component API Reference

ReturnTaskListComponent

Main container component that manages task list display and orchestrates task actions.

Selector: oms-shared-return-task-list

Inputs

appearance
  • Type: 'main' | 'review'
  • Default: 'main'
  • Description: Visual mode controlling layout and filtering behavior
  • Required: No

Example:

<oms-shared-return-task-list
  [appearance]="'review'"
></oms-shared-return-task-list>

Outputs

This component has no outputs. Task actions are handled internally via services and store updates.

Host Bindings

CSS Classes:

  • oms-shared-return-task-list - Always applied base class
  • oms-shared-return-task-list__main - Applied when appearance="main"
  • oms-shared-return-task-list__review - Applied when appearance="review"

Computed Signals

taskListItems()
  • Type: Signal<ReceiptItemTaskListItem[]>
  • Description: Filtered and sorted task list based on appearance mode
  • Filtering Logic:
    • Main mode: Excludes completed tasks
    • Review mode: Shows all tasks
  • Sorting Logic: Open tasks first, completed tasks last

Example:

// In main mode
taskListItems() // Returns only incomplete tasks

// In review mode
taskListItems() // Returns all tasks (incomplete + completed)
appearanceClass()
  • Type: Signal<string>
  • Description: Dynamic CSS class based on appearance input
  • Values: 'oms-shared-return-task-list__main' | 'oms-shared-return-task-list__review'

Public Methods

handleAction(action: TaskActionType): Promise<void>

Handles task actions emitted from child task list items.

Parameters:

  • action: TaskActionType - Action object containing task details

Task Action Structure:

interface TaskActionType {
  type: TaskActionTypeType;           // 'OK' | 'NOK' | 'UNKNOWN'
  taskId: number;                     // Task identifier
  receiptItemId?: number;             // Receipt item identifier (for Tolino)
  updateTo?: 'OK' | 'NOK';            // Update target type (for UNKNOWN)
  actions?: Array<KeyValue>;          // Action metadata (for Tolino)
}

Behavior:

  1. Tolino Receipt Printing: If action contains PRINT_TOLINO_RETURN_RECEIPT key
  2. UNKNOWN Task Update: If type is UNKNOWN with updateTo value
  3. Standard Completion: All other cases

Example:

// Handled automatically by child components
// Called internally when user clicks action buttons
completeTask(taskId: number): Promise<void>

Marks a task as completed and updates the store.

Parameters:

  • taskId: number - Unique task identifier

Side Effects:

  • Calls ReturnTaskListService.completeTask()
  • Updates store via ReturnTaskListStore.updateTaskListItem()
  • Logs errors if completion fails

Error Handling:

try {
  await completeTask(123);
} catch (error) {
  // Automatically logged with context
  // UI remains stable - no error display to user
}
updateTask({ taskId, updateTo }): Promise<void>

Updates a task's type (OK or NOK) for UNKNOWN tasks.

Parameters:

  • taskId: number - Unique task identifier
  • updateTo: 'OK' | 'NOK' - Target task type

Side Effects:

  • Calls ReturnTaskListService.updateTaskType()
  • Updates store via ReturnTaskListStore.updateTaskListItem()
  • Logs errors if update fails

Example:

// Handled automatically when user clicks "Ja verkaufsfähig"
await updateTask({ taskId: 123, updateTo: 'OK' });

// Handled automatically when user clicks "Nein, defekt"
await updateTask({ taskId: 123, updateTo: 'NOK' });

ReturnTaskListItemComponent

Individual task list item component rendering product info and action buttons.

Selector: oms-shared-return-task-list-item

Inputs

appearance
  • Type: 'main' | 'review'
  • Default: 'main'
  • Description: Visual mode inherited from parent component
  • Required: No
item
  • Type: ReceiptItemTaskListItem
  • Required: Yes
  • Description: Task item data including product, status, and actions

Item Structure:

interface ReceiptItemTaskListItem {
  id: number;                         // Task ID
  type: TaskActionTypeType;           // 'OK' | 'NOK' | 'UNKNOWN'
  completed: boolean;                 // Completion status
  processingComment?: string;         // Task description/comment
  product?: Product;                  // Product information
  receiptItem?: { id: number };       // Receipt item reference
  actions?: Array<KeyValue>;          // Available actions (e.g., Tolino print)
}

Outputs

action
  • Type: EventEmitter<TaskActionType>
  • Description: Emits task action when user interacts with buttons
  • Payload: TaskActionType object with task details

Example:

// Emitted when user clicks completion button
{ type: 'OK', taskId: 123 }

// Emitted when user clicks "Ja verkaufsfähig"
{ type: 'UNKNOWN', taskId: 123, updateTo: 'OK' }

// Emitted when user clicks print receipt
{
  type: 'OK',
  taskId: 123,
  receiptItemId: 456,
  actions: [{ key: 'PRINT_TOLINO_RETURN_RECEIPT', value: '...' }]
}

Computed Signals

product()
  • Type: Signal<Product | undefined>
  • Description: Extracted product information from task item
processingComment()
  • Type: Signal<string | undefined>
  • Description: Task description/comment text
displayPrintTolino()
  • Type: Signal<boolean>
  • Description: Whether to show Tolino print button (checks for PRINT_TOLINO_RETURN_RECEIPT action)
type()
  • Type: Signal<TaskActionTypeType>
  • Description: Task action type (OK | NOK | UNKNOWN)

Host Bindings

CSS Classes:

  • oms-shared-return-task-list-item - Always applied base class
  • oms-shared-return-task-list-item__main - Applied when appearance="main"
  • oms-shared-return-task-list-item__review - Applied when appearance="review"

Usage Examples

Basic Main Workflow

import { Component } from '@angular/core';
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';

@Component({
  selector: 'app-return-search-main',
  standalone: true,
  imports: [ReturnTaskListComponent],
  template: `
    <div class="return-search-page">
      <h1>Rückgabe starten</h1>

      <!-- Search and filter components here -->

      <oms-shared-return-task-list
        [appearance]="'main'"
      ></oms-shared-return-task-list>
    </div>
  `
})
export class ReturnSearchMainComponent {}

Review Workflow with Header

import { Component } from '@angular/core';
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';

@Component({
  selector: 'app-return-review',
  standalone: true,
  imports: [ReturnTaskListComponent],
  template: `
    <div class="return-review-page">
      <header>
        <h1>Aufgaben überprüfen</h1>
        <button (click)="printReceipt()">Beleg drucken</button>
      </header>

      <oms-shared-return-task-list
        [appearance]="'review'"
      ></oms-shared-return-task-list>
    </div>
  `
})
export class ReturnReviewComponent {
  printReceipt(): void {
    // Receipt printing logic
  }
}

Dynamic Appearance Mode

import { Component, signal } from '@angular/core';
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';

@Component({
  selector: 'app-task-manager',
  standalone: true,
  imports: [ReturnTaskListComponent],
  template: `
    <div>
      <button (click)="toggleMode()">
        {{ viewMode() === 'main' ? 'Zur Übersicht' : 'Zur Bearbeitung' }}
      </button>

      <oms-shared-return-task-list
        [appearance]="viewMode()"
      ></oms-shared-return-task-list>
    </div>
  `
})
export class TaskManagerComponent {
  viewMode = signal<'main' | 'review'>('main');

  toggleMode(): void {
    this.viewMode.update(mode => mode === 'main' ? 'review' : 'main');
  }
}

Integration with Custom Layout

import { Component } from '@angular/core';
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';

@Component({
  selector: 'app-custom-return-layout',
  standalone: true,
  imports: [ReturnTaskListComponent],
  template: `
    <div class="grid grid-cols-3 gap-6">
      <!-- Left sidebar with filters -->
      <aside class="col-span-1">
        <h2>Filter</h2>
        <!-- Filter components -->
      </aside>

      <!-- Main content area with task list -->
      <main class="col-span-2">
        <oms-shared-return-task-list
          [appearance]="'main'"
        ></oms-shared-return-task-list>
      </main>
    </div>
  `
})
export class CustomReturnLayoutComponent {}

Accessing Store Directly (Advanced)

import { Component, inject, computed } from '@angular/core';
import { ReturnTaskListStore } from '@isa/oms/data-access';
import { injectTabId } from '@isa/core/tabs';
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';

@Component({
  selector: 'app-advanced-task-view',
  standalone: true,
  imports: [ReturnTaskListComponent],
  template: `
    <div>
      @if (taskCount() > 0) {
        <p>{{ taskCount() }} offene Aufgaben</p>
      }

      <oms-shared-return-task-list
        [appearance]="'main'"
      ></oms-shared-return-task-list>
    </div>
  `
})
export class AdvancedTaskViewComponent {
  #store = inject(ReturnTaskListStore);
  processId = injectTabId();

  taskCount = computed(() => {
    const processId = this.processId();
    if (!processId) return 0;

    const entity = this.#store.entityMap()[processId];
    return entity?.data?.filter(item => !item.completed).length ?? 0;
  });
}

Manual Task Refresh (Advanced)

import { Component, inject } from '@angular/core';
import { ReturnTaskListStore, QueryTokenInput } from '@isa/oms/data-access';
import { injectTabId } from '@isa/core/tabs';
import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';

@Component({
  selector: 'app-refreshable-task-list',
  standalone: true,
  imports: [ReturnTaskListComponent],
  template: `
    <div>
      <button (click)="refreshTasks()">Aktualisieren</button>

      <oms-shared-return-task-list
        [appearance]="'main'"
      ></oms-shared-return-task-list>
    </div>
  `
})
export class RefreshableTaskListComponent {
  #store = inject(ReturnTaskListStore);
  processId = injectTabId();

  refreshTasks(): void {
    const processId = this.processId();
    if (!processId) return;

    const queryToken: QueryTokenInput = {
      filter: { completed: false }
    };

    this.#store.fetchTaskListItems({ processId, queryToken });
  }
}

Styling and Customization

Component Structure

The component emits a host class that controls the layout mode:

// Main appearance - card layout
.oms-shared-return-task-list__main {
  @apply flex flex-col items-center gap-6 justify-self-center;
}

// Review appearance - table layout
.oms-shared-return-task-list__review {
  // Layout controlled by child items
}

Task List Container (Main Mode)

.return-search-main-task-list-styles {
  @apply gap-6
         desktop:overflow-y-scroll
         desktop:max-h-[calc(100vh-13rem)]
         justify-start;
}

Customization:

  • Adjust max-h-[calc(100vh-13rem)] for different header heights
  • Modify gap-6 (24px) for different card spacing
  • Override overflow-y-scroll behavior for custom scroll containers

Task Item Cards (Main Mode)

.oms-shared-return-task-list-item__main {
  @apply bg-white
         rounded-2xl
         p-6
         flex
         flex-col
         gap-6
         w-[24.25rem];
}

Customization:

// Custom card width
.oms-shared-return-task-list-item__main {
  width: 20rem; // Override default 24.25rem
}

// Custom card padding
.oms-shared-return-task-list-item__main {
  padding: 2rem; // Override default 1.5rem (p-6)
}

// Custom card shadow
.oms-shared-return-task-list-item__main {
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
}

Task Item Table Rows (Review Mode)

.oms-shared-return-task-list-item__review {
  @apply grid
         grid-cols-[1fr,1fr]
         desktop:grid-cols-[1fr,1fr,minmax(20rem,auto)]
         gap-x-6
         desktop:gap-y-6
         py-6
         text-isa-secondary-900
         items-center
         border-b
         border-solid
         border-isa-neutral-300
         last:pb-0
         last:border-none;
}

Responsive Grid Areas (Tablet/Mobile):

@media screen and (max-width: 1024px) {
  grid-template-areas:
    "product infos"
    "unknown-comment actions";
}

Customization:

// Custom column proportions
.oms-shared-return-task-list-item__review {
  grid-template-columns: 2fr 1fr minmax(15rem, auto);
}

// Custom row padding
.oms-shared-return-task-list-item__review {
  padding-top: 2rem;
  padding-bottom: 2rem;
}

// Custom border color
.oms-shared-return-task-list-item__review {
  border-bottom-color: #e5e7eb; // Override isa-neutral-300
}

Processing Comment Styling

// Standard tasks (OK/NOK)
.processing-comment {
  @apply isa-text-body-2-bold;
}

// UNKNOWN tasks (larger emphasis)
.processing-comment-unknown {
  @apply isa-text-body-1-regular;
}

Task Action Buttons

// UNKNOWN task action buttons container
.task-unknown-actions {
  @apply flex
         flex-row
         gap-3
         h-full
         py-2
         items-center
         justify-self-end;
}

// Tolino print button positioning
.tolino-print-cta {
  @apply w-fit
         self-end
         justify-self-end
         desktop:self-start;
}

Task Info Container

.task-item-infos {
  @apply h-full
         p-4
         flex
         flex-col
         gap-4
         rounded-lg
         bg-isa-secondary-100;
}

Customization:

// Custom background color
.task-item-infos {
  background-color: #f9fafb; // Override isa-secondary-100
}

// Custom padding
.task-item-infos {
  padding: 1rem; // Override p-4 (1rem)
}

Responsive Behavior

Main Mode Breakpoints

// Desktop: Scrollable container
@media (min-width: 1024px) {
  .return-search-main-task-list-styles {
    overflow-y: scroll;
    max-height: calc(100vh - 13rem);
  }
}

Review Mode Breakpoints

// Tablet/Mobile: Stacked layout
@media screen and (max-width: 1024px) {
  .oms-shared-return-task-list-item__review {
    grid-template-areas:
      "product infos"
      "unknown-comment actions";
  }
}

// Desktop: Three-column layout
@media (min-width: 1024px) {
  .oms-shared-return-task-list-item__review {
    grid-template-columns: 1fr 1fr minmax(20rem, auto);
  }
}

Theme Integration

The component uses ISA design system colors:

// Text colors
text-isa-secondary-900    // Main text
text-isa-accent-green     // Completed status

// Background colors
bg-white                  // Card backgrounds
bg-isa-secondary-100      // Info containers

// Border colors
border-isa-neutral-300    // Row separators

Custom Theme:

// Override in your global styles
.oms-shared-return-task-list-item__main {
  --card-bg: theme('colors.white');
  --info-bg: theme('colors.isa.secondary.100');

  background-color: var(--card-bg);

  .task-item-infos {
    background-color: var(--info-bg);
  }
}

E2E Testing Attributes

All interactive elements include data attributes for E2E testing:

<!-- Task list container -->
<div data-what="task-list" data-which="processing-comment">

<!-- Action buttons -->
<button data-what="button" data-which="complete">
<button data-what="button" data-which="resellable">
<button data-what="button" data-which="damaged">
<button data-what="button" data-which="print-receipt">

<!-- Status indicators -->
<span data-what="info" data-which="completed">

<!-- Components -->
<oms-shared-return-product-info data-what="component" data-which="return-product-info">

Using in Tests:

// Playwright example
await page.click('[data-what="button"][data-which="complete"]');
await expect(page.locator('[data-what="info"][data-which="completed"]')).toBeVisible();

Architecture Notes

Current Architecture

Components/Features (return-search, return-review)
       ↓
  ReturnTaskListComponent (orchestrator)
       ↓
├─→ ReturnTaskListItemComponent (presentation)
├─→ ReturnProductInfoComponent (product display)
├─→ ReturnTaskListStore (state management)
├─→ ReturnTaskListService (business logic)
├─→ PrintTolinoReturnReceiptService (printing)
└─→ Tab Context (process isolation)

Design Patterns

1. Container/Presenter Pattern

  • Container: ReturnTaskListComponent handles state, effects, and actions
  • Presenter: ReturnTaskListItemComponent handles display and emits events
  • Benefits: Clear separation of concerns, easier testing, reusable presenters

2. Signal-Based Reactivity

// Computed signals for derived state
taskListItems = computed(() => {
  const processId = this.processId();
  const appearance = this.appearance();

  // Filter and sort based on inputs
  return filteredAndSortedItems;
});

Benefits:

  • Automatic dependency tracking
  • Efficient change detection
  • Type-safe reactive data flow

3. Effect-Based Data Fetching

constructor() {
  effect(() => {
    const processId = this.processId();
    const appearance = this.appearance();

    if (processId) {
      const filter = appearance === 'review'
        ? { eob: true }
        : { completed: false };

      untracked(() =>
        this.#store.fetchTaskListItems({ processId, filter })
      );
    }
  });
}

Benefits:

  • Automatic refetch when dependencies change
  • Untracked execution prevents infinite loops
  • Declarative data synchronization

4. Optimistic UI Updates

async completeTask(taskId: number) {
  const result = await this.#service.completeTask(taskId);

  // Update store immediately (optimistic)
  if (result && processId) {
    this.#store.updateTaskListItem({ processId, taskListItem: result });
  }
}

Benefits:

  • Immediate UI feedback
  • Better perceived performance
  • Automatic rollback on error

5. Tab-Based Multi-Instance Support

processId = injectTabId();  // Signal from tab context

Benefits:

  • Multiple simultaneous return processes
  • Isolated state per tab
  • No cross-tab interference

Known Architectural Considerations

1. Component Review Needed (High Priority)

Issue: Component header comment indicates review needed

// TODO: Komponente und logik benötigt review

Current State:

  • Component works but may have architectural improvements pending
  • Logic patterns established but not formally reviewed
  • No known bugs or functional issues

Proposed Review Areas:

  • Error handling consistency
  • Store update patterns (optimistic vs pessimistic)
  • Effect dependency management
  • Template complexity reduction

Impact: Medium - component is functional, review would improve maintainability

2. Deferred Rendering Performance (Low Priority)

Current State:

@for (item of taskList; track item.id) {
  @defer (on viewport) {
    <oms-shared-return-task-list-item />
  } @placeholder {
    <!-- Spinner placeholder -->
  }
}

Observation:

  • Uses spinner as placeholder (TODO notes suggest skeleton loader)
  • Viewport-based loading may cause layout shifts
  • No prefetching strategy

Proposed Enhancements:

  • Replace spinner with skeleton loader matching item dimensions
  • Add @defer (on viewport; prefetch on idle) for better UX
  • Consider virtual scrolling for large task lists

Impact: Low - current implementation works, improvements would enhance UX

3. Filter Query Complexity (Medium Priority)

Current State:

const filter: Record<string, unknown> =
  appearance === 'review' ? { eob: true } : { completed: false };

Observation:

  • Simple boolean filter for review mode (eob: true)
  • Incomplete task filter for main mode (completed: false)
  • No pagination or advanced filtering

Potential Issues:

  • Large task lists not paginated (could impact performance)
  • Filter criteria hardcoded (not extensible)
  • No user-configurable filtering

Proposed Enhancements:

  • Extract filter logic to separate helper function
  • Add pagination support via QueryToken
  • Allow custom filter injection via component input
  • Add sort options (by date, product, type)

Impact: Medium - works for current use cases, scalability concerns

4. Error Recovery Strategy (Medium Priority)

Current State:

catch (error) {
  this.#logger.error('Error completing task', error);
  // No user feedback or retry mechanism
}

Observation:

  • Errors logged but not surfaced to UI
  • No retry mechanism for failed operations
  • User has no indication task completion failed
  • Store may be in inconsistent state after error

Proposed Enhancements:

  • Add toast notification on task action failure
  • Implement retry mechanism with exponential backoff
  • Show error indicator on failed task items
  • Revert optimistic updates on error

Impact: Medium - impacts user experience when errors occur

Performance Considerations

1. Viewport-Based Rendering

  • Only renders tasks visible in viewport
  • Reduces initial render time for large lists
  • Trade-off: Layout shifts on scroll

2. Change Detection Optimization

changeDetection: ChangeDetectionStrategy.OnPush
  • Only checks component when inputs change or events fire
  • Reduces unnecessary change detection cycles
  • Compatible with signal-based reactivity

3. Store Entity Management

// Efficient entity lookup by process ID
const tasks = this.#store.entityMap()[processId].data;
  • O(1) lookup time via object map
  • No array iteration for entity access
  • Normalized data structure

4. Task Item Tracking

@for (item of taskList; track item.id) {
  • Angular tracks items by ID for efficient DOM updates
  • Prevents unnecessary re-renders on list changes
  • Stable item references

Future Enhancements

Potential improvements identified:

  1. Skeleton Loading - Replace spinner placeholders with skeleton UI
  2. Virtual Scrolling - Add @angular/cdk virtual scroll for large lists
  3. Undo Functionality - Allow task completion undo within timeout
  4. Bulk Actions - Multi-select and bulk complete/update
  5. Advanced Filtering - User-configurable filters and sorting
  6. Pagination - Add pagination for large task lists
  7. Error UI - User-visible error states and retry buttons
  8. Accessibility - ARIA labels and keyboard navigation
  9. Analytics - Track task completion rates and times
  10. Export Functionality - Export task list to CSV/PDF

Dependencies

Required Libraries

Angular Core

  • @angular/core - Angular framework, signals, effects, dependency injection

OMS Domain

  • @isa/oms/data-access - Data services, stores, and models
    • ReturnTaskListService - Task CRUD operations
    • ReturnTaskListStore - NgRx Signals store
    • PrintTolinoReturnReceiptService - Receipt printing
    • ReceiptItemTaskListItem - Task data model
    • TaskActionType - Action type definitions
    • QueryTokenInput - Query parameters

OMS Shared

  • @isa/oms/shared/product-info - Product display component

UI Components

  • @isa/ui/buttons - Button components
    • ButtonComponent - Standard button
    • IconButtonComponent - Icon-only button
    • InfoButtonComponent - Info button with icon

Core Services

  • @isa/core/tabs - Tab context management
    • injectTabId() - Process ID from tab context
  • @isa/core/logging - Logging infrastructure
    • logger() - Logger instance injection
    • provideLoggerContext() - Logger context provider

Icons

  • @isa/icons - ISA icon set
    • isaActionCheck - Checkmark icon
    • isaActionPrinter - Printer icon
  • @ng-icons/core - Icon component infrastructure

State Management

  • @ngrx/signals - Signal-based state management
  • @ngrx/operators - RxJS operators for NgRx

Utilities

  • rxjs - Reactive programming
  • zod - Schema validation (indirect via services)

Generated API Clients

  • @generated/swagger/oms-api - OMS API client (indirect via services)

Development Dependencies

  • jest - Testing framework
  • jest-preset-angular - Angular-specific Jest configuration
  • @angular/compiler-cli - Angular compiler
  • typescript - TypeScript compiler

Path Alias

Import from: @isa/oms/shared/task-list

Example:

import { ReturnTaskListComponent } from '@isa/oms/shared/task-list';

Testing

The library uses Jest with Angular Testing Utilities for testing.

Running Tests

# Run tests for this library
npx nx test return-task-list --skip-nx-cache

# Run tests with coverage
npx nx test return-task-list --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test return-task-list --watch

# Run specific test file
npx nx test return-task-list --testFile=src/lib/return-task-list/return-task-list.component.spec.ts --skip-nx-cache

Testing ReturnTaskListComponent

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
import { ReturnTaskListComponent } from './return-task-list.component';
import { ReturnTaskListStore } from '@isa/oms/data-access';
import { signal } from '@angular/core';

describe('ReturnTaskListComponent', () => {
  let component: ReturnTaskListComponent;
  let fixture: ComponentFixture<ReturnTaskListComponent>;
  let mockStore: jest.Mocked<typeof ReturnTaskListStore>;

  beforeEach(async () => {
    // Mock store
    mockStore = {
      entityMap: jest.fn(() => ({})),
      fetchTaskListItems: jest.fn(),
      updateTaskListItem: jest.fn()
    } as any;

    await TestBed.configureTestingModule({
      imports: [ReturnTaskListComponent],
      providers: [
        { provide: ReturnTaskListStore, useValue: mockStore }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(ReturnTaskListComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should filter completed tasks in main mode', () => {
    // Arrange
    fixture.componentRef.setInput('appearance', 'main');
    const mockTasks = [
      { id: 1, completed: false },
      { id: 2, completed: true },
      { id: 3, completed: false }
    ];
    mockStore.entityMap.mockReturnValue({
      123: { data: mockTasks, status: 'Success', id: 123 }
    });

    // Act
    const tasks = component.taskListItems();

    // Assert
    expect(tasks).toHaveLength(2);
    expect(tasks.every(t => !t.completed)).toBe(true);
  });

  it('should show all tasks in review mode', () => {
    // Arrange
    fixture.componentRef.setInput('appearance', 'review');
    const mockTasks = [
      { id: 1, completed: false },
      { id: 2, completed: true }
    ];
    mockStore.entityMap.mockReturnValue({
      123: { data: mockTasks, status: 'Success', id: 123 }
    });

    // Act
    const tasks = component.taskListItems();

    // Assert
    expect(tasks).toHaveLength(2);
  });

  it('should complete task and update store', async () => {
    // Arrange
    const mockService = {
      completeTask: jest.fn(() => Promise.resolve({ id: 1, completed: true }))
    };
    component['#returnTaskListService'] = mockService as any;

    // Act
    await component.completeTask(1);

    // Assert
    expect(mockService.completeTask).toHaveBeenCalledWith(1);
    expect(mockStore.updateTaskListItem).toHaveBeenCalled();
  });
});

Testing ReturnTaskListItemComponent

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { describe, it, expect, beforeEach } from '@jest/globals';
import { ReturnTaskListItemComponent } from './return-task-list-item.component';
import { ReceiptItemTaskListItem } from '@isa/oms/data-access';

describe('ReturnTaskListItemComponent', () => {
  let component: ReturnTaskListItemComponent;
  let fixture: ComponentFixture<ReturnTaskListItemComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [ReturnTaskListItemComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(ReturnTaskListItemComponent);
    component = fixture.componentInstance;
  });

  it('should emit action on complete button click', () => {
    // Arrange
    const mockItem: ReceiptItemTaskListItem = {
      id: 123,
      type: 'OK',
      completed: false
    };
    fixture.componentRef.setInput('item', mockItem);

    let emittedAction;
    component.action.subscribe(action => emittedAction = action);

    // Act
    component.onActionClick({ type: 'OK' });

    // Assert
    expect(emittedAction).toEqual({
      taskId: 123,
      type: 'OK'
    });
  });

  it('should display UNKNOWN task action buttons', () => {
    // Arrange
    const mockItem: ReceiptItemTaskListItem = {
      id: 123,
      type: 'UNKNOWN',
      completed: false,
      processingComment: 'Prüfen Sie das Produkt'
    };
    fixture.componentRef.setInput('item', mockItem);

    // Act
    fixture.detectChanges();

    // Assert
    const buttons = fixture.nativeElement.querySelectorAll(
      '[data-what="button"]'
    );
    expect(buttons.length).toBe(2); // OK and NOK buttons
  });

  it('should show completed status when task is completed', () => {
    // Arrange
    const mockItem: ReceiptItemTaskListItem = {
      id: 123,
      type: 'OK',
      completed: true
    };
    fixture.componentRef.setInput('item', mockItem);

    // Act
    fixture.detectChanges();

    // Assert
    const completedInfo = fixture.nativeElement.querySelector(
      '[data-what="info"][data-which="completed"]'
    );
    expect(completedInfo).toBeTruthy();
    expect(completedInfo.textContent).toContain('Abgeschlossen');
  });

  it('should display Tolino print button when action exists', () => {
    // Arrange
    const mockItem: ReceiptItemTaskListItem = {
      id: 123,
      type: 'OK',
      completed: false,
      actions: [
        { key: 'PRINT_TOLINO_RETURN_RECEIPT', value: 'true' }
      ]
    };
    fixture.componentRef.setInput('item', mockItem);

    // Act
    fixture.detectChanges();

    // Assert
    const printButton = fixture.nativeElement.querySelector(
      '[data-which="print-receipt"]'
    );
    expect(printButton).toBeTruthy();
  });
});

Test Coverage Goals

  • Component Logic: 80%+ coverage
  • Template Rendering: Test all conditional paths (@if, @for)
  • User Interactions: Test all button clicks and events
  • Store Integration: Mock store methods and verify calls
  • E2E Attributes: Verify all data-what/data-which attributes exist

E2E Testing

The component includes comprehensive E2E testing attributes:

// Example Playwright test
import { test, expect } from '@playwright/test';

test.describe('Return Task List', () => {
  test('should complete task on button click', async ({ page }) => {
    // Navigate to page with task list
    await page.goto('/return-search');

    // Click complete button
    await page.click('[data-what="button"][data-which="complete"]');

    // Verify task marked as completed
    await expect(
      page.locator('[data-what="info"][data-which="completed"]')
    ).toBeVisible();
  });

  test('should update UNKNOWN task to OK', async ({ page }) => {
    await page.goto('/return-search');

    // Click "Ja verkaufsfähig" button
    await page.click('[data-what="button"][data-which="resellable"]');

    // Verify complete button appears (task type updated)
    await expect(
      page.locator('[data-what="button"][data-which="complete"]')
    ).toBeVisible();
  });

  test('should print Tolino receipt', async ({ page }) => {
    await page.goto('/return-search');

    // Click print receipt button
    const [download] = await Promise.all([
      page.waitForEvent('download'),
      page.click('[data-what="button"][data-which="print-receipt"]')
    ]);

    // Verify download started
    expect(download).toBeTruthy();
  });
});

Best Practices

1. Always Use Appearance Mode Appropriately

// ✅ GOOD: Main workflow for processing tasks
<oms-shared-return-task-list [appearance]="'main'"></oms-shared-return-task-list>

// ✅ GOOD: Review workflow for checking all tasks
<oms-shared-return-task-list [appearance]="'review'"></oms-shared-return-task-list>

// ❌ BAD: Using review mode in main workflow
// (Shows completed tasks when user expects only open tasks)

2. Rely on Tab Context for Process ID

// ✅ GOOD: Component automatically uses tab-based process ID
// No manual process ID management needed
<oms-shared-return-task-list [appearance]="'main'"></oms-shared-return-task-list>

// ❌ BAD: Trying to manually manage process IDs
// Component handles this internally via injectTabId()

3. Trust Optimistic Updates

// ✅ GOOD: Let component handle store updates
// User sees immediate feedback
await component.completeTask(123);

// ❌ BAD: Manual store updates after completion
// Creates race conditions and duplicate updates
await service.completeTask(123);
store.updateTaskListItem(...); // Component already does this

4. Use E2E Attributes in Custom Templates

// ✅ GOOD: Include data attributes for testing
<button
  (click)="completeTask()"
  data-what="button"
  data-which="custom-complete"
>
  Complete Task
</button>

// ❌ BAD: No testing attributes
<button (click)="completeTask()">
  Complete Task
</button>

5. Monitor Store State for Advanced Use Cases

// ✅ GOOD: Access store for derived data only
taskCount = computed(() => {
  const entity = this.#store.entityMap()[this.processId()];
  return entity?.data?.filter(item => !item.completed).length ?? 0;
});

// ❌ BAD: Direct store manipulation
this.#store.entityMap()[processId].data = newData; // Use updateTaskListItem()

6. Handle Appearance Mode Switching Carefully

// ✅ GOOD: Consider data refetch when switching modes
const appearance = signal<'main' | 'review'>('main');

effect(() => {
  // Component effect will refetch with appropriate filter
  console.log('Appearance changed to', appearance());
});

// ❌ BAD: Switching without understanding filter implications
// Review mode shows EOB tasks, main mode shows incomplete tasks

7. Respect Component Encapsulation

// ✅ GOOD: Use component as black box
<oms-shared-return-task-list [appearance]="mode()"></oms-shared-return-task-list>

// ❌ BAD: Trying to access internal component state
const tasks = component.taskListItems(); // Component manages this

8. Style with CSS Classes, Not Inline Styles

// ✅ GOOD: Override via CSS classes
.oms-shared-return-task-list-item__main {
  width: 20rem;
  padding: 2rem;
}

// ❌ BAD: Inline styles in template
<oms-shared-return-task-list
  style="width: 20rem;"
></oms-shared-return-task-list>

9. Leverage Deferred Rendering Performance

// ✅ GOOD: Component handles viewport-based loading
// Large task lists render efficiently
<oms-shared-return-task-list [appearance]="'main'"></oms-shared-return-task-list>

// ❌ BAD: Manual pagination when component handles it
// Trust the @defer implementation

10. Consider Accessibility

// ✅ GOOD: Wrap in semantic HTML
<main role="main">
  <h1>Aufgaben</h1>
  <oms-shared-return-task-list [appearance]="'main'"></oms-shared-return-task-list>
</main>

// ⚠️ CONSIDERATION: Component could add ARIA labels
// Future enhancement for screen reader support

11. Error Handling Pattern

// ✅ GOOD: Component logs errors automatically
// Monitor logs for task completion failures
// Consider adding error UI in future enhancement

// ⚠️ CURRENT LIMITATION: Errors not visible to users
// Plan for user feedback in production deployments

12. Testing Strategy

// ✅ GOOD: Test component behavior, not implementation
it('should complete task', async () => {
  const button = fixture.nativeElement.querySelector('[data-which="complete"]');
  button.click();

  await fixture.whenStable();

  expect(mockStore.updateTaskListItem).toHaveBeenCalled();
});

// ❌ BAD: Testing internal signals
it('should have taskListItems signal', () => {
  expect(component.taskListItems).toBeDefined(); // Too implementation-specific
});

License

Internal ISA Frontend library - not for external distribution.