Files
ISA-Frontend/libs/common/print/README.md
Lorenz Hilpert 2b5da00249 feat(checkout): add reward order confirmation feature with schema migrations
- Add new reward-order-confirmation feature library with components and store
- Implement checkout completion orchestrator service for order finalization
- Migrate checkout/oms/crm models to Zod schemas for better type safety
- Add order creation facade and display order schemas
- Update shopping cart facade with order completion flow
- Add comprehensive tests for shopping cart facade
- Update routing to include order confirmation page
2025-10-21 14:28:52 +02:00

25 KiB

@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

  • 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

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)

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

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

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:

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:

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:

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:

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:

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)

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:

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:

type PrinterType = 'label' | 'office';

Example:

import { PrinterType } from '@isa/common/print';

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

Usage Examples

Basic Label Printing

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

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

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

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

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

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:

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

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

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

Result Interface:

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

// Dialog is automatically shown by PrintService
// No need to use directly

Testing

The library uses Jest for testing.

Running Tests

# 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

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:

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:

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

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

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.