# @isa/common/print

A comprehensive print management library for Angular applications providing printer discovery, selection, and unified print operations across label and office printers.

## Overview

The Common Print library provides a platform-aware print service that simplifies printer management and print operations in the ISA application. It supports both label printers (for shipping labels, product tags) and office printers (for documents, receipts), with intelligent fallback strategies and user-friendly printer selection dialogs. The library seamlessly integrates with the print API backend and provides reusable components for consistent print UX across the application.

## Table of Contents

- [Features](#features)
- [Quick Start](#quick-start)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [Usage Examples](#usage-examples)
- [Platform Behavior](#platform-behavior)
- [Component Reference](#component-reference)
- [Testing](#testing)
- [Architecture Notes](#architecture-notes)

## Features

- **Dual printer type support** - Label printers and office printers with dedicated endpoints
- **Platform-aware behavior** - Automatic direct printing on desktop, dialog on mobile
- **Smart printer selection** - Remembers default printer per type, fallback to dialog if needed
- **Reusable UI components** - Print button component with integrated state management
- **Print dialog** - User-friendly printer selection with error display and retry logic
- **Error handling** - Graceful fallback from direct print to dialog on failures
- **TypeScript type safety** - Strongly typed printer models and API interfaces
- **Angular CDK integration** - Platform detection for iOS/Android awareness
- **Standalone components** - Modern Angular architecture with explicit imports

## Quick Start

### 1. Import and Inject the Service

```typescript
import { Component, inject } from '@angular/core';
import { PrintService, PrinterType } from '@isa/common/print';

@Component({
  selector: 'app-shipping-label',
  template: '...'
})
export class ShippingLabelComponent {
  #printService = inject(PrintService);
}
```

### 2. Simple Direct Print (Platform-Aware)

```typescript
async printLabel(): Promise<void> {
  const result = await this.#printService.print({
    printerType: PrinterType.LABEL,
    printFn: async (printer) => {
      // Your print logic here
      await this.#labelPrintApi.print(printer.key, this.labelData);
    }
  });

  if (result.printer) {
    console.log(`Printed to: ${result.printer.value}`);
  } else {
    console.log('Print cancelled by user');
  }
}
```

### 3. Force Print Dialog

```typescript
async printWithDialog(): Promise<void> {
  const result = await this.#printService.print({
    printerType: PrinterType.OFFICE,
    printFn: async (printer) => {
      await this.#documentPrintApi.print(printer.key, this.document);
    },
    directPrint: false // Always show dialog
  });
}
```

### 4. Use the Print Button Component

```typescript
import { Component } from '@angular/core';
import { PrintButtonComponent, PrinterType } from '@isa/common/print';

@Component({
  selector: 'app-invoice',
  template: `
    <common-print-button
      [printerType]="PrinterType.OFFICE"
      [printFn]="printInvoice"
      [directPrint]="false">
      Rechnung drucken
    </common-print-button>
  `,
  imports: [PrintButtonComponent]
})
export class InvoiceComponent {
  PrinterType = PrinterType;

  printInvoice = async (printer: Printer) => {
    await this.#api.printInvoice(printer.key, this.invoiceId);
  };
}
```

## Core Concepts

### Printer Types

The library supports two distinct printer categories:

#### 1. Label Printers (`PrinterType.LABEL`)
- **Purpose**: Specialized thermal label printers for shipping labels, product tags, barcodes
- **API Endpoint**: `PrintLabelPrinters()`
- **Common Use Cases**: Shipping labels, warehouse labels, product tags, return labels
- **Typical Printers**: Zebra, Dymo label printers

#### 2. Office Printers (`PrinterType.OFFICE`)
- **Purpose**: Standard laser/inkjet printers for documents and receipts
- **API Endpoint**: `PrintOfficePrinters()`
- **Common Use Cases**: Invoices, receipts, reports, customer documents
- **Typical Printers**: HP, Canon, Epson office printers

### Printer Model

All printers share a common interface:

```typescript
interface Printer {
  /** Unique identifier for the printer (printer name/ID) */
  key: string;

  /** Display name shown to users */
  value: string;

  /** Whether this printer is currently selected as default */
  selected: boolean;

  /** Whether this printer is currently enabled for use */
  enabled: boolean;
}
```

### Platform-Aware Printing

The library automatically adapts behavior based on platform:

**Desktop (Windows, macOS, Linux)**:
- Attempts direct print to selected default printer
- Falls back to dialog only on errors
- Optimizes for speed and minimal user interaction

**Mobile (Android, iOS)**:
- Always shows printer selection dialog
- No direct print due to platform limitations
- Ensures user has control over print destination

**Platform Detection**:
```typescript
const isMobile = this.#platform.ANDROID || this.#platform.IOS;
const allowDirectPrint = !isMobile && selectedPrinter;
```

### Print Flow States

```
┌─────────────────────────────────────────────────────────────┐
│ Print Requested                                             │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
┌─────────────────────────────────────────────────────────────┐
│ Fetch Available Printers (Label or Office)                  │
└────────────────┬────────────────────────────────────────────┘
                 │
                 ▼
          ┌──────┴──────┐
          │             │
    Desktop?      Mobile?
          │             │
          ▼             ▼
  ┌──────────────┐  ┌─────────────────┐
  │ Direct Print │  │ Show Dialog     │
  │ (if selected)│  │ (always)        │
  └──────┬───────┘  └─────────┬───────┘
         │                    │
    Success?                  │
         │                    │
    ┌────┴────┐               │
    │         │               │
   Yes       No               │
    │         │               │
    │         └───────────────┘
    │                │
    ▼                ▼
  Return       ┌─────────────────┐
  Printer      │ User Selects    │
               │ & Prints        │
               └────────┬────────┘
                        │
                   ┌────┴────┐
                   │         │
              Success?    Cancel?
                   │         │
                   ▼         ▼
               Return     Return
               Printer    undefined
```

### Direct Print Control

The `directPrint` parameter offers fine-grained control:

| Value | Behavior |
|-------|----------|
| `undefined` (default) | Platform-aware: direct print on desktop with selected printer, dialog on mobile |
| `true` | **Always** attempt direct print (requires selected printer, throws error if none) |
| `false` | **Always** show dialog, even on desktop with selected printer |

## API Reference

### PrintService

Main service for printer discovery and print operations.

#### `labelPrinters(): Promise<Printer[]>`

Retrieves all available label printers from the print API.

**Returns:** Promise resolving to array of label printers

**Example:**
```typescript
const labelPrinters = await this.#printService.labelPrinters();
labelPrinters.forEach(printer => {
  console.log(`${printer.value} - ${printer.selected ? 'Default' : 'Available'}`);
});
```

#### `officePrinters(): Promise<Printer[]>`

Retrieves all available office printers from the print API.

**Returns:** Promise resolving to array of office printers

**Example:**
```typescript
const officePrinters = await this.#printService.officePrinters();
const defaultPrinter = officePrinters.find(p => p.selected);
```

#### `printers(printerType: PrinterType): Promise<Printer[]>`

Generic method to retrieve printers of a specific type.

**Parameters:**
- `printerType: PrinterType` - The type of printers to retrieve

**Returns:** Promise resolving to array of printers

**Example:**
```typescript
const printers = await this.#printService.printers(PrinterType.LABEL);
```

#### `print(options): Promise<{ printer?: Printer }>`

Initiates a print operation with intelligent platform-aware behavior.

**Parameters:**
- `options.printerType: PrinterType` - The type of printer to use
- `options.printFn: (printer: Printer) => PromiseLike<unknown>` - Function that performs the actual print
- `options.directPrint?: boolean | undefined` - Direct print control (see [Direct Print Control](#direct-print-control))

**Returns:** Promise resolving to object with selected printer (or undefined if cancelled)

**Throws:**
- Error from `printFn` if direct print fails and `directPrint: true`
- Errors propagate to dialog for retry if `directPrint` is undefined

**Example:**
```typescript
const result = await this.#printService.print({
  printerType: PrinterType.LABEL,
  printFn: async (printer) => {
    await this.#api.printLabel(printer.key, labelData);
  },
  directPrint: undefined // Platform-aware
});

if (result.printer) {
  this.#logger.info(`Label printed to ${result.printer.value}`);
}
```

### PrinterType

Enum-like object for printer type constants.

**Values:**
- `PrinterType.LABEL = 'label'` - Label printer type
- `PrinterType.OFFICE = 'office'` - Office printer type

**Type:**
```typescript
type PrinterType = 'label' | 'office';
```

**Example:**
```typescript
import { PrinterType } from '@isa/common/print';

if (printerType === PrinterType.LABEL) {
  // Handle label printing
}
```

## Usage Examples

### Basic Label Printing

```typescript
import { Component, inject } from '@angular/core';
import { PrintService, PrinterType } from '@isa/common/print';
import { logger } from '@isa/core/logging';

@Component({
  selector: 'app-shipping',
  template: `
    <button (click)="printShippingLabel()">
      Print Shipping Label
    </button>
  `
})
export class ShippingComponent {
  #printService = inject(PrintService);
  #logger = logger(() => ({ component: 'ShippingComponent' }));

  async printShippingLabel(): Promise<void> {
    try {
      const result = await this.#printService.print({
        printerType: PrinterType.LABEL,
        printFn: async (printer) => {
          await this.#shippingApi.printLabel({
            printerId: printer.key,
            orderId: this.orderId,
            trackingNumber: this.trackingNumber
          });
        }
      });

      if (result.printer) {
        this.#logger.info(`Shipping label printed to ${result.printer.value}`);
      }
    } catch (error) {
      this.#logger.error('Failed to print shipping label', error as Error);
    }
  }
}
```

### Invoice Printing with Forced Dialog

```typescript
async printInvoice(): Promise<void> {
  // Always show dialog to let user choose printer
  const result = await this.#printService.print({
    printerType: PrinterType.OFFICE,
    printFn: async (printer) => {
      await this.#documentApi.printInvoice(
        printer.key,
        this.invoiceId,
        { copies: 2 }
      );
    },
    directPrint: false // Force dialog even on desktop
  });

  if (result.printer) {
    this.showSuccessMessage(`Invoice printed to ${result.printer.value}`);
  }
}
```

### Checking Available Printers

```typescript
import { Component, OnInit, inject, signal } from '@angular/core';
import { PrintService, Printer, PrinterType } from '@isa/common/print';

@Component({
  selector: 'app-printer-status',
  template: `
    <h3>Available Label Printers</h3>
    @for (printer of labelPrinters(); track printer.key) {
      <div>
        {{ printer.value }}
        @if (printer.selected) {
          <span>(Default)</span>
        }
        @if (!printer.enabled) {
          <span>(Offline)</span>
        }
      </div>
    }
  `
})
export class PrinterStatusComponent implements OnInit {
  #printService = inject(PrintService);

  labelPrinters = signal<Printer[]>([]);

  async ngOnInit(): Promise<void> {
    const printers = await this.#printService.labelPrinters();
    this.labelPrinters.set(printers);
  }
}
```

### Print Button with Custom Styling

```typescript
import { Component } from '@angular/core';
import { PrintButtonComponent, PrinterType, Printer } from '@isa/common/print';

@Component({
  selector: 'app-receipt',
  template: `
    <div class="receipt-actions">
      <common-print-button
        [printerType]="PrinterType.OFFICE"
        [printFn]="printReceipt"
        [directPrint]="allowDirectPrint"
        class="receipt-print-button">
        <ng-icon name="isaActionPrinter" class="mr-2"></ng-icon>
        Bon drucken
      </common-print-button>
    </div>
  `,
  styles: [`
    .receipt-print-button {
      width: 100%;
      justify-content: center;
    }
  `],
  imports: [PrintButtonComponent]
})
export class ReceiptComponent {
  PrinterType = PrinterType;
  allowDirectPrint = true;

  printReceipt = async (printer: Printer): Promise<void> => {
    await this.#receiptApi.print({
      printerId: printer.key,
      receiptId: this.receiptId,
      format: 'thermal'
    });
  };
}
```

### Error Handling with Retry Logic

```typescript
async printWithRetry(maxRetries = 3): Promise<void> {
  let attempts = 0;

  while (attempts < maxRetries) {
    try {
      const result = await this.#printService.print({
        printerType: PrinterType.LABEL,
        printFn: async (printer) => {
          await this.#api.printLabel(printer.key, this.labelData);
        }
      });

      if (result.printer) {
        this.#logger.info(`Print successful on attempt ${attempts + 1}`);
        return;
      } else {
        // User cancelled
        this.#logger.info('Print cancelled by user');
        return;
      }
    } catch (error) {
      attempts++;
      this.#logger.warn(`Print attempt ${attempts} failed`, error as Error);

      if (attempts >= maxRetries) {
        this.showErrorMessage('Print failed after multiple attempts');
        throw error;
      }

      // Wait before retry
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}
```

### Conditional Printer Type Selection

```typescript
import { Component, computed, signal } from '@angular/core';
import { PrintButtonComponent, PrinterType } from '@isa/common/print';

@Component({
  selector: 'app-document',
  template: `
    <label>
      <input type="checkbox" [(ngModel)]="useLabel" />
      Print on label printer
    </label>

    <common-print-button
      [printerType]="printerType()"
      [printFn]="printDocument">
      Print Document
    </common-print-button>
  `,
  imports: [PrintButtonComponent]
})
export class DocumentComponent {
  useLabel = signal(false);

  printerType = computed(() =>
    this.useLabel()
      ? PrinterType.LABEL
      : PrinterType.OFFICE
  );

  printDocument = async (printer: Printer): Promise<void> => {
    await this.#api.print({
      printerId: printer.key,
      documentId: this.documentId,
      format: this.useLabel() ? 'label' : 'a4'
    });
  };
}
```

## Platform Behavior

### Desktop Platforms (Windows, macOS, Linux)

**Default Behavior** (`directPrint: undefined`):
1. Fetches available printers for the specified type
2. Identifies the default printer (where `selected: true`)
3. If a default printer exists:
   - Attempts direct print to that printer
   - On success: Returns immediately with the printer
   - On error: Shows dialog with error message for retry
4. If no default printer:
   - Shows printer selection dialog immediately

**Forced Direct Print** (`directPrint: true`):
- Throws error if no selected printer exists
- Does not show dialog on failure (error propagates to caller)

**Forced Dialog** (`directPrint: false`):
- Always shows dialog regardless of selected printer
- Useful for user confirmation or when switching printers

### Mobile Platforms (Android, iOS)

**All Cases**:
- Always shows printer selection dialog
- Direct print is never attempted (platform limitations)
- `directPrint` parameter is effectively ignored
- User must manually confirm printer selection

**Platform Detection**:
```typescript
// Angular CDK Platform service
const isMobile = this.#platform.ANDROID || this.#platform.IOS;
```

### Print Dialog Features

When the dialog is shown:
- **Printer List**: All available printers with selection state
- **Default Preselection**: Default printer is pre-selected
- **Status Indicators**: Shows enabled/disabled state
- **Error Display**: Shows errors from failed direct print attempts
- **Retry Logic**: Allows user to select different printer and retry
- **Cancel Option**: Returns `undefined` if user cancels

## Component Reference

### PrintButtonComponent

Reusable button component with integrated print functionality and state management.

**Selector:** `common-print-button`

**Inputs:**

| Input | Type | Required | Description |
|-------|------|----------|-------------|
| `printerType` | `PrinterType` | Yes | The type of printer to use (LABEL or OFFICE) |
| `printFn` | `(printer: Printer) => PromiseLike<void>` | Yes | Function that executes the print logic |
| `directPrint` | `boolean \| undefined` | No | Controls direct print behavior (default: platform-aware) |

**Outputs:** None (uses signals for state)

**Signals:**
- `printing: Signal<boolean>` - Indicates if a print operation is in progress

**Content Projection:**
- Supports transcluded content for button label
- Includes printer icon by default

**Example:**
```typescript
<common-print-button
  [printerType]="PrinterType.LABEL"
  [printFn]="myPrintFunction"
  [directPrint]="false">
  Custom Button Text
</common-print-button>
```

**Features:**
- Automatic loading state management via `printing` signal
- Error logging to ISA logging service
- Disables button during print operations
- Printer icon from `@isa/icons`

### PrintDialogComponent

Internal dialog component for printer selection (not typically used directly).

**Selector:** `common-print-dialog`

**Data Interface:**
```typescript
interface PrinterDialogData {
  printers: Printer[];
  error?: unknown;
  print: (printer: Printer) => PromiseLike<unknown>;
}
```

**Result Interface:**
```typescript
interface PrinterDialogResult {
  printer: Printer | undefined;
}
```

**Features:**
- Listbox for printer selection
- Default printer preselection
- Error display with formatting
- Print button with loading state
- Cancel button

**Usage (via PrintService):**
```typescript
// Dialog is automatically shown by PrintService
// No need to use directly
```

## Testing

The library uses **Jest** for testing.

### Running Tests

```bash
# Run tests for this library
npx nx test common-print --skip-nx-cache

# Run tests with coverage
npx nx test common-print --code-coverage --skip-nx-cache

# Run tests in watch mode
npx nx test common-print --watch
```

### Test Structure

The library includes unit tests covering:

- **Service Methods** - Printer fetching and print orchestration
- **Platform Detection** - Mobile vs desktop behavior
- **Direct Print Logic** - Default printer selection and fallback
- **Error Handling** - Dialog display on failures
- **Component State** - Print button loading states
- **Dialog Interaction** - Printer selection and print execution

### Example Test

```typescript
import { TestBed } from '@angular/core/testing';
import { Platform } from '@angular/cdk/platform';
import { PrintService } from './print.service';
import { PrintService as PrintApiService } from '@generated/swagger/print-api';

describe('PrintService', () => {
  let service: PrintService;
  let mockPrintApi: jest.Mocked<PrintApiService>;
  let mockPlatform: jest.Mocked<Platform>;

  beforeEach(() => {
    mockPrintApi = {
      PrintLabelPrinters: jest.fn(),
      PrintOfficePrinters: jest.fn()
    } as any;

    mockPlatform = {
      ANDROID: false,
      IOS: false
    } as any;

    TestBed.configureTestingModule({
      providers: [
        PrintService,
        { provide: PrintApiService, useValue: mockPrintApi },
        { provide: Platform, useValue: mockPlatform }
      ]
    });

    service = TestBed.inject(PrintService);
  });

  it('should fetch label printers', async () => {
    const mockPrinters = [
      { key: 'printer1', value: 'Label Printer 1', selected: true, enabled: true }
    ];

    mockPrintApi.PrintLabelPrinters.mockReturnValue(
      of({ result: mockPrinters })
    );

    const printers = await service.labelPrinters();

    expect(printers).toEqual(mockPrinters);
    expect(mockPrintApi.PrintLabelPrinters).toHaveBeenCalled();
  });

  it('should attempt direct print on desktop with selected printer', async () => {
    // Test implementation...
  });
});
```

## Architecture Notes

### Current Architecture

```
Components/Features
       ↓
  PrintButtonComponent (optional, reusable UI)
       ↓
  PrintService (main orchestration)
       ↓
├─→ PrintDialogComponent (printer selection UI)
├─→ Platform (CDK platform detection)
└─→ PrintApiService (generated API client)
```

### Design Patterns

#### 1. Strategy Pattern (Platform-Aware Behavior)

The service uses platform detection to choose the appropriate print strategy:

```typescript
const directPrintAllowed =
  directPrint === undefined
    ? selectedPrinter && !(this.#platform.ANDROID || this.#platform.IOS)
    : directPrint;
```

**Benefits:**
- Single API for all platforms
- Automatic optimization per platform
- Easy to test with mocked Platform service

#### 2. Facade Pattern (Unified Print API)

PrintService provides a simplified interface over complex printer management:

```typescript
// Simple API hides:
// - Printer discovery
// - Platform detection
// - Dialog management
// - Error recovery
await this.#printService.print({ printerType, printFn });
```

#### 3. Dependency Injection (Modern Angular)

Uses `inject()` function for clean service composition:

```typescript
#printService = inject(PrintApiService);
#printDialog = injectDialog(PrintDialogComponent, { title: 'Drucken' });
#platform = inject(Platform);
```

### Known Architectural Considerations

#### 1. Dialog Integration (Low Priority)

**Current State:**
- Dialog is injected via `injectDialog()` from `@isa/ui/dialog`
- Dialog component is tightly coupled to service

**Potential Improvement:**
- Consider injectable dialog strategy for testing
- Allows mocking dialog interactions in unit tests

**Impact:** Low - current design works well for production use

#### 2. Print Function Type Safety (Medium Priority)

**Current State:**
- `printFn` accepts any PromiseLike return type
- No type enforcement on print API contract

**Potential Improvement:**
```typescript
interface PrintFunction<TData> {
  (printer: Printer, data: TData): PromiseLike<void>;
}
```

**Impact:** Medium - would improve type safety at call sites

#### 3. Printer Caching (Low Priority)

**Current State:**
- Fetches printers on every print operation
- No caching layer

**Potential Improvement:**
- Cache printers for short duration (60 seconds)
- Invalidate on print errors (printer might be offline)
- Reduces API calls for rapid print operations

**Impact:** Low - current performance is acceptable

### Dependencies

#### Required Libraries

- `@angular/core` - Angular framework
- `@angular/cdk/platform` - Platform detection
- `@generated/swagger/print-api` - Generated print API client
- `@isa/ui/dialog` - Dialog system
- `@isa/ui/buttons` - Button components
- `@isa/ui/input-controls` - Listbox component
- `@isa/icons` - Icon library
- `@isa/core/logging` - Logging service
- `rxjs` - Reactive programming

#### Path Alias

Import from: `@isa/common/print`

### Performance Considerations

1. **Lazy Dialog Loading** - Dialog component only loaded when needed
2. **Platform Detection Caching** - Platform service caches detection results
3. **First Value Resolution** - Uses `firstValueFrom()` for clean Promise conversion
4. **Component OnPush** - All components use OnPush change detection

### Future Enhancements

Potential improvements identified:

1. **Print History** - Track recent print operations for audit trail
2. **Printer Status Polling** - Real-time printer availability updates
3. **Print Preview** - Optional preview before printing (office printers)
4. **Batch Printing** - Print multiple documents to same printer
5. **Print Queue** - Queue multiple print jobs for sequential processing
6. **Custom Paper Sizes** - Support for custom paper size selection (label printers)
7. **Print Templates** - Reusable print configurations per printer type

## License

Internal ISA Frontend library - not for external distribution.
